システムへのリクエストログをAthenaで分析できるようにした話

こんにちは

おでんは断然、もち巾着派のSREチーム程です。 大根とこんにゃくと日本酒もついでに好きです。 おでんと日本酒好きな方はぜひご一緒におでん屋探しの旅に出かけましょう。

初投稿となるので緊張で指が震えてTYPO祭りになる気がしていますが、 頑張って書いていくのでよろしくおねがいします。 SREチームの人間なので、クラウド関連の内容で最近取り組んだ業務についてご紹介したいと思います。

こんな人向け

  • クラウド触ったことあります、触ってます
  • AWS好きです
  • CDK好きです

作りたいもの

こんな人向けでなんとなく「あーCDKでなんか作るのね」ということは察していただけるかと思いますが 今回は、AthenaのリソースをCDKで作成していくことが目標です。

モチベーション

  • CloudFrontとApplication Load Balancerで採取できるリクエストログを簡単に分析できるようにしたい
  • クラウドのリソースはなるべくコードで管理したい

上記のような背景より、ログのインポートや、クエリによる分析が簡単に実行できるAthenaをCDKで作ろうという流れになりました。

予備知識

Athena

Athenaは、S3に存在するファイルに対して、RDBのようにクエリを投げてデータ分析することができるAWSのマネージドサービスです。 サーバーレスで動作するので、自分で分析エンジンをインストールしたサーバーを用意するなどの必要がなく、セットアップ次第すぐに利用可能です。 セットアップでは、RDSのようにデータベーステーブル定義を作成することでクエリ分析が可能となります。 その他、Athenaを動かすのに必要な基礎知識は別記事をご参照ください。

qiita.com

本題

さてそれでは本題に入っていきましょう。さっそく、AthenaをCDKで作っていきます。 実際のコード例を載せながら紹介していきます。なお、今回はTypeScript言語で書きました。 僕自身、TypeScriptを書くのはこれが初めてでしたので、コードの綺麗さ等は今後に期待という感じで何卒よろしくおねがいします。

プロジェクト作成

こちらのリンクと同じなので割愛します。

dev.classmethod.jp

ALBのログテーブルをCDKで用意する

CDKでやってること

大きく分けて次の3点だけです。(全体のコードは下段に記載のコード抜粋を参照)

  • Athenaデータベースを作成する
new glue.Database(stack, "LogDatabase", {
    databaseName: "SomeDatabaseName"
});
  • テーブルの作成時に、データベースと紐付けする
new glue.Table(stack, "MyTable", {
    databaseName: "SomeDataBaseName"
}
  • glue.Tableのコンストラクタにテーブルの設定を追加する
new glue.Table(stack, "MyTable", {
    databaseName: "SomeDataBaseName",
    config1 : value1,
    config2 : value2
 });

また、テーブルのカラム定義は以下のように記述している部分になります。 名前( name )と 型( type ) の定義をしています。

    tableInput: {
        storageDescriptor: {
            columns: [
                {
                    "name": "type",
                    "type": "string"
                },
                {
                    "name": "time",
                    "type": "string"
                },
                {
                    "name": "elb",
                    "type": "string"
                },
          ]
     }

なお、上記では glue.Table を利用していますが、最終的に今回は glue.CfnTable を使用しています(下記「コード全体」を参照) 。 これは、パーティションの管理手法である「パーティションプロジェクション」の設定が glue.Table では対応していなかったため、glue.CfnTable を使用することといたしました。(パーティションプロジェクションとは、パーティションAWS側が自動管理してくれる機能のことです。詳細は公式ドキュメントをご覧ください。)

コード抜粋

    //データベースの作成
    const logDB = new glue.Database(this, "LogDatabase", {
        databaseName: "logs"
    });

    //ALBログのログテーブルを作成
    new glue.CfnTable(this, "AlbLogTable", {
        databaseName: "logs",
        catalogId: "catalog",
        tableInput: {
            name: "alb
            tableType: "EXTERNAL_TABLE",
            parameters: {
                "projection.enabled": true,
                "projection.date_day.type": "date",
                "projection.date_day.range": "2020/10/01,NOW",
                "projection.date_day.format": "yyyy/MM/dd",
                "projection.date_day.interval": "1",
                "projection.date_day.interval.unit": "DAYS",
                "serialization.encoding": "utf-8",
                "storage.location.template": "s3://alb-log-bucket/" + "${date_day}",
            },
            storageDescriptor: {
                columns: [
                    {
                        "name": "type",
                        "type": "string"
                    },
                    {
                        "name": "time",
                        "type": "string"
                    },
                    {
                        "name": "elb",
                        "type": "string"
                    },
                    {
                        "name": "client_ip",
                        "type": "string"
                    },
                    {
                        "name": "client_port",
                        "type": "int"
                    },
                    {
                        "name": "target_ip",
                        "type": "string"
                    },
                    {
                        "name": "target_port",
                        "type": "int"
                    },
                    {
                        "name": "request_processing_time",
                        "type": "double"
                    },
                    {
                        "name": "target_processing_time",
                        "type": "double"
                    },
                    {
                        "name": "response_processing_time",
                        "type": "double"
                    },
                    {
                        "name": "elb_status_code",
                        "type": "string"
                    },
                    {
                        "name": "target_status_code",
                        "type": "string"
                    },
                    {
                        "name": "received_bytes",
                        "type": "bigint"
                    },
                    {
                        "name": "sent_bytes",
                        "type": "bigint"
                    },
                    {
                        "name": "request_verb",
                        "type": "string"
                    },
                    {
                        "name": "request_url",
                        "type": "string"
                    },
                    {
                        "name": "request_proto",
                        "type": "string"
                    },
                    {
                        "name": "user_agent",
                        "type": "string"
                    },
                    {
                        "name": "ssl_cipher",
                        "type": "string"
                    },
                    {
                        "name": "ssl_protocol",
                        "type": "string"
                    },
                    {
                        "name": "target_group_arn",
                        "type": "bigint"
                    },
                    {
                        "name": "trace_id",
                        "type": "string"
                    },
                    {
                        "name": "domain_id",
                        "type": "string"
                    },
                    {
                        "name": "chosen_cert_arn",
                        "type": "string"
                    },
                    {
                        "name": "matched_rule_priority",
                        "type": "string"
                    },
                    {
                        "name": "request_creation_time",
                        "type": "string"
                    },
                    {
                        "name": "actions_executed",
                        "type": "string"
                    },
                    {
                        "name": "redirect_url",
                        "type": "string"
                    },
                    {
                        "name": "lambda_error_reason",
                        "type": "string"
                    },
                    {
                        "name": "target_port_list",
                        "type": "string"
                    },
                    {
                        "name": "target_status_code_list",
                        "type": "string"
                    },
                    {
                        "name": "classification",
                        "type": "string"
                    },
                    {
                        "name": "classification_reason",
                        "type": "string"
                    },
                ],
                inputFormat: "org.apache.hadoop.mapred.TextInputFormat",
                outputFormat: "org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat",
                serdeInfo: {
                    name: "alb_serde",
                    parameters: {
                        format: 1,
                        "input.regex": '([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*):([0-9]*) ([^ ]*)[:-]([0-9]*) ([-.0-9]*) ([-.0-9]*) ([-.0-9]*) (|[-0-9]*) (-|[-0-9]*) ([-0-9]*) ([-0-9]*) \"([^ ]*) ([^ ]*) (- |[^ ]*)\" \"([^\"]*)\" ([A-Z0-9-]+) ([A-Za-z0-9.-]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^\"]*)\" ([-.0-9]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^ ]*)\" \"([^\s]+?)\" \"([^\s]+)\" \"([^ ]*)\" \"([^ ]*)\"',
                    },
                    serializationLibrary: "org.apache.hadoop.hive.serde2.RegexSerDe",
                },
                location: "s3://athena-result-bucket/",
            },
            partitionKeys: [
                {
                    "name": "date_day",
                    "type": "string"
                },
            ]
        }
    })

つまづいたポイント

パーティションの設定

Athenaでは、事前にS3上のパスをパーティションとして設定しておき、クエリ実行時にパーティションを指定することで、ここのファイルだけ読み込んでくださいねとAthenaに指示することができます。 パーティションを指定することで、クエリ実行時に読み込むデータ量を削減することができ、コストの削減に繋がります。

さて、パーティションの設定周りのパラメータを以下に抜粋しました。 date_day という名前でpartitionKeys を設定していますね。 この date_day という名前を使って、パーティションプロジェクションの設定を上段の parameters で記述しています。 実は、上段で記述しているdate_day は下段で設定した partitionKeys の名前と同じ名前の文字列でないとだめなんですね。 そこに気づくのに結構時間を要しました。

parameters: {
    "projection.date_day.type": "date",
    "projection.date_day.range": "2020/10/01,NOW",
    "projection.date_day.format": "yyyy/MM/dd",
    "projection.date_day.interval": "1",
    "projection.date_day.interval.unit": "DAYS",
},
partitionKeys: [
    {
        "name": "date_day",
        "type": "string"
    },
]

パーティションのフォーマット設定

projection.date_day.formatyyyy/MM/dd という日付のフォーマット設定を記述しています。 実は、このyyyy/MM/ddyyyy-MM-dd など / 以外の文字だとどうやら動作しなくなるようなのです。 これは、S3上でのファイルの階層構造が / で階層化しているため、そこに合わせてあげる必要があるというのが僕の推測です。

パーティションプロジェクションに関する情報が少なかった

パーティションプロジェクション自体は、2020年に初めて使えるようになった比較的新しい機能でして CDK自体が対応したのは、またそれ以後ということなので、パーティションプロジェクションのCDK実装に関するドキュメントが少なかったのも、苦労した要因の1つでした。 とはいえ、試行錯誤の末に動かすことができたので、動いたときの達成感はすごかったです!

クエリ分析できるようになった

こうして無事、リクエストログのクエリ分析ができるようになりました。(以下Athenaでクエリ実行したときの画面) 実行結果がResults パートに表示されています。(結果はマスキングしています)

f:id:tei-krmx:20210416192902p:plain
実際のAthena画面

画面左側を見ると、作成したALBログテーブルの詳細なカラム設定がCDKで記述したとおりに反映されていることがわかります。

f:id:tei-krmx:20210416194548p:plain
albログテーブルのカラム

まとめ

  • Athena上でリクエストログ(CFログとALBログ)をクエリ分析できるようにした
  • CDKを使用し、Athena環境をAWSアカウント上に構築した
  • パーティションプロジェクションを設定するために試行錯誤した。
  • これからAthena環境を構築する人はぜひご参考にしていただけたら嬉しいです