Amazon AthenaにPartition Projection(パーティション射影)という機能があります。
ざっくりいうとパーティション管理を自動化して、高速にクエリが実行でき、お財布にも優しいというものです。個人的にはめちゃくちゃ便利だなと思い、特にログの調査に活用しています。
ログ調査対象のサービスの内、大体どのプロジェクトでも使っているものがいくつかあります(ALB、VPCフローログ、CloudTrail....)。
これまではPartition Projectionの設定を行うCREATE
文を毎回実行していたのですが、少し面倒なのでAWS CDKで実装し使いまわせるようにしました。
今回の実装の全体像は以下です。
1. 概要
対象のログ
今回Partition Projectionを実装する対象としたのは、以下のサービスのログです。
Partition Projectionsの設定をする際、対応するGlue Database/Tableを作成する形になります。ここではALBのアクセスログとAWS WAFのトラフィックログは1つのDatabaseにするなど、ある程度グルーピングしています。
No | Services | Log Types | Glue Database |
---|---|---|---|
1 | Application Load Balancer | Access Logs | application_logs_database |
2 | AWS WAF | Web Acl Traffic Logs | application_logs_database |
3 | Amazon VPC | VPC Flow Logs | network_logs_database |
4 | Amazon Route53 | Query Logs | network_logs_database |
5 | AWS CloudTrail | CloudTrail Logs | security_logs_database |
6 | AWS Config | History, Snapshot | security_logs_database |
実装方法
今回の実装は、以下の記事を参考にさせていただきました。
Partition ProjectionとしてAWS::Glue::Table
を実装していく形になります。
2023/3現在、AWS GlueのL2 Constructは存在しないため、L1 Constructを使用する、もしくはAlpha版のモジュールを使う必要があります。Alpha版のモジュールの場合は個別にインストールが必要です。
今回は実装したものを今後使い回していきたいのが目的のため、安定しているL1 Constructを元に実装していきます(将来的に正式なL2 Constructが制定されたタイミングで移行を考えます)。
なおAlpha版でPartition Projectionを実装したい場合は以下が参考になります。
2. 実装詳細
今回実装したものは以下に置いてあります。
プロジェクト構成
以下lib
配下(StackとConstructの実装箇所)の構成を示します。例のごとくConstructをファイルごとに分割して実装し、構造化しています。
. ├── construct │ ├── sample │ │ ├── alb.ts │ │ ├── athena.ts │ │ ├── cloudTrail.ts │ │ ├── config.ts │ │ ├── route53.ts │ │ ├── vpc.ts │ │ └── waf.ts │ ├── applicationLogsTable.ts │ ├── networkLogsTable.ts │ └── securityLogsTable.ts ├── appTableStack.ts └── securityTableStack.ts
lib/construct/sample/
配下はログ出力のためのサンプルを簡易実装しています。xxxxxLogsTable.ts
は今回のメインです。Partition Projectionの設定(Glue Database/Table)を実装しています。詳細は後述します。- スタックは以下の2つに分けています。
appTablseStack.ts
:application_logs_database(ALB, WAF)とnetwork_logs_database(VPC, Route53)をまとめています。1アカウント内に複数存在する可能性がリソースになります。securityTableStack.ts
:security_logs_database(CloudTrail, Config)をまとめています。これらは基本1アカウントに1つのため、上記から分離しています。
実装の流れ
以下lib/construct/applicationLogsTable.ts
を元に、実装内容を簡単に説明します。
なお主要なログに対してPartition Projectionを設定するCREATE文は大体公式にあります(AWS WAFの場合)。こちらをCDKに落としてく形で進めると比較的楽に実装できます。
入力のインタフェース
バケット名や、Web ACL名などリソース依存の値はstring
で受け取るようにしました。
ログの出力はCDK外で設定されていることも多いため、Partition ProjectionのみCDKで実装して、別の手段で作成したリソースの名称等をstring
で渡して生成できるようにすることを意識しています。
export interface ApplicationLogsTableProps { albAccessLogsBucketName: string wafTrafficLogsBucketName: string webAclName: string }
Glue データベースを作成
ここは特に難しいところはありません。カタログIDはアカウントIDを設定します。
// Glue データベースを作成 const applicationLogsDatabase = new glue.CfnDatabase(scope, 'ApplicationLogsDatabase', { catalogId: cdk.Stack.of(scope).account, databaseInput: { name: 'application_logs_database', }, });
Glue テーブルを作成
メインの箇所になります。以下AWS WAFのログにおける定義例を示します。
// Glue Table (WAF Traffic Logs) with Partition Projection const wafTrafficLogsTable = new glue.CfnTable(scope, "WafTrafficLogsTable", { databaseName: 'application_logs_database', catalogId: cdk.Stack.of(scope).account, tableInput: { name: 'waf_traffic_logs_tables', tableType: "EXTERNAL_TABLE", parameters: { "projection.enabled": true, "projection.date.type": "date", "projection.date.range": "NOW-1YEARS, NOW+9HOUR", "projection.date.format": "yyyy/MM/dd", "projection.date.interval": "1", "projection.date.interval.unit": "DAYS", "storage.location.template": `s3://${props.wafTrafficLogsBucketName}/AWSLogs/${cdk.Stack.of(scope).account}/WAFLogs/${cdk.Stack.of(this).region}/${props.webAclName}/` + "${date}", }, storageDescriptor: { columns: [ { "name": "timestamp", "type": "bigint" }, { "name": "formatversion", "type": "int" }, { "name": "webaclid", "type": "string" }, { "name": "terminatingruleid", "type": "string" }, { "name": "terminatingruletype", "type": "string" }, { "name": "action", "type": "string" }, { "name": "terminatingrulematchdetails", "type": "array<struct<conditiontype:string,location:string,matcheddata:array<string>>>" }, { "name": "httpsourcename", "type": "string" }, { "name": "httpsourceid", "type": "string" }, { "name": "rulegrouplist", "type": "array<struct<rulegroupid:string,terminatingrule:struct<ruleid:string,action:string,rulematchdetails:string>,nonterminatingmatchingrules:array<struct<ruleid:string,action:string,rulematchdetails:array<struct<conditiontype:string,location:string,matcheddata:array<string>>>>>,excludedrules:array<struct<ruleid:string,exclusiontype:string>>>>" }, { "name": "ratebasedrulelist", "type": "array<struct<ratebasedruleid:string,limitkey:string,maxrateallowed:int>>" }, { "name": "nonterminatingmatchingrules", "type": "array<struct<ruleid:string,action:string>>" }, { "name": "requestheadersinserted", "type": "string" }, { "name": "responsecodesent", "type": "string" }, { "name": "httprequest", "type": "struct<clientip:string,country:string,headers:array<struct<name:string,value:string>>,uri:string,args:string,httpversion:string,httpmethod:string,requestid:string>" }, { "name": "labels", "type": "array<struct<name:string>>" }, { "name": "captcharesponse", "type": "struct<responsecode:string,solvetimestamp:string,failureReason:string>" } ], inputFormat: "org.apache.hadoop.mapred.TextInputFormat", outputFormat: "org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat", serdeInfo: { serializationLibrary: "org.openx.data.jsonserde.JsonSerDe", parameters: { 'serialization.format': '1' } }, location: `s3://${props.wafTrafficLogsBucketName}/AWSLogs/${cdk.Stack.of(scope).account}/WAFLogs/${cdk.Stack.of(this).region}/${props.webAclName}`, }, partitionKeys: [ { "name": "date", "type": "string" }, ] } })
データベース名
データベース名は作成したcfnDatabase
の名称と一致させる必要があります。
databaseName: 'application_logs_database',
Partition Projectionの設定
tableInput
のparameters
で、 パーティションキーの設定を行なっていきます。
ここではdate
をパーティションキーとするよう設定しています。なお範囲については適当に広範囲にしてしまうと、大量のスキャンが走りパフォーマンスの悪化や、料金増大の恐れがあるので注意が必要です。
今回の実装では基本全て、過去1年分から現在(タイムゾーンを考慮して+9H)の範囲としています。
またstorage.location.template
においてパスを指定していきます。バケット名やリージョン名はテンプレートリテラルを使用して埋め込んでいます。
ただし注意点としてパーティションキーは${key}
の形式にする必要があり、そのままテンプレートリテラルに記載すると変数としてみなされるので、エスケープする or 例のように文字列として結合する等の対処が必要です。
tableInput: { // 中略 parameters: { "projection.enabled": true, "projection.date.type": "date", "projection.date.range": "NOW-1YEARS, NOW+9HOUR", "projection.date.format": "yyyy/MM/dd", "projection.date.interval": "1", "projection.date.interval.unit": "DAYS", "storage.location.template": `s3://${props.wafTrafficLogsBucketName}/AWSLogs/${cdk.Stack.of(scope).account}/WAFLogs/${cdk.Stack.of(this).region}/${props.webAclName}/` + "${date}", },
パーティションキーに関してはpartitionKeys
配下にも記載します。ここのname
は上記の名称(projection.xxxx.yyyyy
のxxxx
)と一致させる必要があります。
partitionKeys: [ { "name": "date", "type": "string" }, ]
ALB, WAF, VPC, Route53は上記の通りdate
をパーティションキーとしていますが、CloudTrailとConfigは追加で設定をしています。
まず、CloudTrailはdate
だけでなくregion
もパーティションキーとしています。enum
でリージョンを定義しています。
parameters: { "projection.enabled": "true", "projection.region.type": "enum", "projection.region.values": "us-east-1,us-east-2,us-west-1,us-west-2,af-south-1,ap-east-1,ap-south-1,ap-northeast-2,ap-southeast-1,ap-southeast-2,ap-northeast-1,ca-central-1,eu-central-1,eu-west-1,eu-west-2,eu-south-1,eu-west-3,eu-north-1,me-south-1,sa-east-1", // region "projection.date.type": "date", "projection.date.range": "NOW-1YEARS,NOW+9HOUR", "projection.date.format": "yyyy/MM/dd", "projection.date.interval": "1", "projection.date.interval.unit": "DAYS", "storage.location.template": `s3://${props.cloudTrailLogsBucketName}/AWSLogs/${cdk.Stack.of(scope).account}/` + "CloudTrail/${region}/${date}", "classification": "cloudtrail", "compressionType": "gzip", "typeOfData": "file", },
ConfigもCloudTrailと同様にregion
をパーティションキーに追加しています。
またdate
もパーティションキーとしていますが、yyyy/M/d
とM
やd
になっている点が異なります。これはログの出力パスが数字一桁の場合は0埋めがされていないためです。
Config独自のパーティションキーとしてログの種類を示すtype
を追加しています。変更ログとスナップショットの2種類が存在するためです。
tableInput: { name: 'config_logs', tableType: "EXTERNAL_TABLE", parameters: { "projection.enabled": "true", "projection.region.type": "enum", "projection.region.values": "us-east-1,us-east-2,us-west-1,us-west-2,af-south-1,ap-east-1,ap-south-1,ap-northeast-2,ap-southeast-1,ap-southeast-2,ap-northeast-1,ca-central-1,eu-central-1,eu-west-1,eu-west-2,eu-south-1,eu-west-3,eu-north-1,me-south-1,sa-east-1", // region "projection.date.type": "date", "projection.date.range": "NOW-1YEARS,NOW+9HOUR", "projection.date.format": "yyyy/M/d", // date "projection.date.interval": "1", "projection.date.interval.unit": "DAYS", "projection.type.type": "enum", "projection.type.values": "ConfigHistory,ConfigSnapshot", // type "storage.location.template": `s3://${props.configLogsBucketName}/AWSLogs/${cdk.Stack.of(scope).account}/Config/` + "${region}/${date}/${type}" },
Columnsの設定
ログの出力項目に合わせ、Columnを定義していきます。
array
やstruct
を含む項目は、例のように1行かつ間のスペース無しで定義します。
columns: [ { "name": "timestamp", "type": "bigint" }, // 中略 { "name": "terminatingrulematchdetails", "type": "array<struct<conditiontype:string,location:string,matcheddata:array<string>>>" }, //以下略
Glue DatabaseとTableの依存関係を定義
Glue Database, Tableを実装したのみの状態でデプロイすると、並列でデプロイしようとし失敗する可能性があります(Databaseを作成→Tableを作成となる必要あり)。
そのため依存関係を明示します。
albAccessLogsTable.addDependency(applicationLogsDatabase) wafTrafficLogsTable.addDependency(applicationLogsDatabase)
3. 動作確認
CDKデプロイ後に、以下のようにパーティションキーを指定してAthenaからクエリを発行すれば対象のログを取得できます。
-- ALBアクセスログなど SELECT * FROM application_logs_database.alb_access_logs_table WHERE date = '2023/03/01'; -- CludTrail SELECT * FROM security_logs_database.cloudtrail_logs_tables WHERE region = 'ap-northeast-1' -- region and date = '2023/03/01'; -- Config SELECT * From security_logs_database.config_logs WHERE region = 'ap-northeast-1' and date = '2023/3/1' -- yyyy/M/d and type = 'ConfigHistory' -- ログの種類 ;
以下はApplication Load Balancer のアクセスログでの確認結果です。
おわりに
今後はCDKの実装を使いまわして楽にPartition Projectionの設定ができそうです。CloudFront等、別のサービスのログもそのうち追加で実装していこうと思います。