こんにちは
おでんは断然、もち巾着派のSREチーム程です。 大根とこんにゃくと日本酒もついでに好きです。 おでんと日本酒好きな方はぜひご一緒におでん屋探しの旅に出かけましょう。
初投稿となるので緊張で指が震えてTYPO祭りになる気がしていますが、 頑張って書いていくのでよろしくおねがいします。 SREチームの人間なので、クラウド関連の内容で最近取り組んだ業務についてご紹介したいと思います。
こんな人向け
作りたいもの
こんな人向け
でなんとなく「あーCDKでなんか作るのね」ということは察していただけるかと思いますが
今回は、AthenaのリソースをCDKで作成していくことが目標です。
モチベーション
上記のような背景より、ログのインポートや、クエリによる分析が簡単に実行できるAthenaをCDKで作ろうという流れになりました。
予備知識
Athena
Athenaは、S3に存在するファイルに対して、RDBのようにクエリを投げてデータ分析することができるAWSのマネージドサービスです。
サーバーレスで動作するので、自分で分析エンジンをインストールしたサーバーを用意するなどの必要がなく、セットアップ次第すぐに利用可能です。
セットアップでは、RDSのようにデータベース
とテーブル定義
を作成することでクエリ分析が可能となります。
その他、Athenaを動かすのに必要な基礎知識は別記事をご参照ください。
本題
さてそれでは本題に入っていきましょう。さっそく、AthenaをCDKで作っていきます。 実際のコード例を載せながら紹介していきます。なお、今回はTypeScript言語で書きました。 僕自身、TypeScriptを書くのはこれが初めてでしたので、コードの綺麗さ等は今後に期待という感じで何卒よろしくおねがいします。
プロジェクト作成
こちらのリンクと同じなので割愛します。
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.format
に yyyy/MM/dd
という日付のフォーマット設定を記述しています。
実は、このyyyy/MM/dd
は yyyy-MM-dd
など /
以外の文字だとどうやら動作しなくなるようなのです。
これは、S3上でのファイルの階層構造が /
で階層化しているため、そこに合わせてあげる必要があるというのが僕の推測です。
パーティションプロジェクションに関する情報が少なかった
パーティションプロジェクション自体は、2020年に初めて使えるようになった比較的新しい機能でして CDK自体が対応したのは、またそれ以後ということなので、パーティションプロジェクションのCDK実装に関するドキュメントが少なかったのも、苦労した要因の1つでした。 とはいえ、試行錯誤の末に動かすことができたので、動いたときの達成感はすごかったです!
クエリ分析できるようになった
こうして無事、リクエストログのクエリ分析ができるようになりました。(以下Athenaでクエリ実行したときの画面) 実行結果がResults パートに表示されています。(結果はマスキングしています)
画面左側を見ると、作成したALBログテーブルの詳細なカラム設定がCDKで記述したとおりに反映されていることがわかります。