Techfirm Cloud Architect Blog

テックファーム株式会社クラウドインフラグループのブログ

パーティション射影を使用したAthenaでの AWS WAF ログ分析

はじめに

AWS WAFのログ分析を手軽に実現したいと検討していたところ、AWS公式ドキュメント上にAWS WAF ログのクエリ - Amazon Athenaを見つけて試してみました。
ポイントはパーティション射影を利用している点です。

前提

AWS WAFと紐づけ先のCloudFront、ログ出力先のS3はすでに構築済みの状態として本記事は記載しています。
Athenaのクエリ保存先の設定やデータベースの設定についても省略しています。

パーティション射影?

パーティション分割(たとえば今回で言えば日付で分割)は、クエリによってスキャンされるデータ量を制限できるようになり、パフォーマンス向上と、コスト削減ができるAthenaの重要な機能です。
一言でパーティション射影とは、パーティション管理を自動化(パーティションを手動で指定する必要がない)してくれる優れモノです。
詳細はAmazon Athena でのパーティション射影 - Amazon Athenaを参照してください。

パーティション射影を使用した Athena での AWS WAF ログ用のテーブルの作成

それでは、AWS公式ドキュメントに記載されているSQL(CREATE TABLE)文を参考にAthenaのテーブルを作成していきたいと思います。
https://docs.aws.amazon.com/ja_jp/athena/latest/ug/waf-logs.html#create-waf-table-partition-projection

参考にしたSQL文から変更した箇所は以下になります。
LOCATIONおよびstorage.location.templateは、<>で囲った日本語箇所を自身の環境値に置き換えます。
projection.region.values は、私が試した環境ではcloudfrontでしたが、ここも自身の利用しているリージョンに置き換えます。
projection.day.rangeは、2023/08/01を使用を開始する日として置き換えました。

CREATE EXTERNAL TABLE `waf_logs`(
  `timestamp` bigint,
  `formatversion` int,
  `webaclid` string,
  `terminatingruleid` string,
  `terminatingruletype` string,
  `action` string,
  `terminatingrulematchdetails` array <
                                    struct <
                                        conditiontype: string,
                                        sensitivitylevel: string,
                                        location: string,
                                        matcheddata: array < string >
                                          >
                                     >,
  `httpsourcename` string,
  `httpsourceid` string,
  `rulegrouplist` array <
                      struct <
                          rulegroupid: string,
                          terminatingrule: struct <
                                              ruleid: string,
                                              action: string,
                                              rulematchdetails: array <
                                                                   struct <
                                                                       conditiontype: string,
                                                                       sensitivitylevel: string,
                                                                       location: string,
                                                                       matcheddata: array < string >
                                                                          >
                                                                    >
                                                >,
                          nonterminatingmatchingrules: array <
                                                              struct <
                                                                  ruleid: string,
                                                                  action: string,
                                                                  overriddenaction: string,
                                                                  rulematchdetails: array <
                                                                                       struct <
                                                                                           conditiontype: string,
                                                                                           sensitivitylevel: string,
                                                                                           location: string,
                                                                                           matcheddata: array < string >
                                                                                              >
                                                                                       >
                                                                    >
                                                             >,
                          excludedrules: string
                            >
                       >,
`ratebasedrulelist` array <
                         struct <
                             ratebasedruleid: string,
                             limitkey: string,
                             maxrateallowed: int
                               >
                          >,
  `nonterminatingmatchingrules` array <
                                    struct <
                                        ruleid: string,
                                        action: string,
                                        rulematchdetails: array <
                                                             struct <
                                                                 conditiontype: string,
                                                                 sensitivitylevel: string,
                                                                 location: string,
                                                                 matcheddata: array < string >
                                                                    >
                                                             >,
                                        captcharesponse: struct <
                                                            responsecode: string,
                                                            solvetimestamp: string
                                                             >
                                          >
                                     >,
  `requestheadersinserted` array <
                                struct <
                                    name: string,
                                    value: string
                                      >
                                 >,
  `responsecodesent` string,
  `httprequest` struct <
                    clientip: string,
                    country: string,
                    headers: array <
                                struct <
                                    name: string,
                                    value: string
                                      >
                                 >,
                    uri: string,
                    args: string,
                    httpversion: string,
                    httpmethod: string,
                    requestid: string
                      >,
  `labels` array <
               struct <
                   name: string
                     >
                >,
  `captcharesponse` struct <
                        responsecode: string,
                        solvetimestamp: string,
                        failureReason: string
                          >
)
PARTITIONED BY ( 
`region` string, 
`date` string) 
ROW FORMAT SERDE 
  'org.openx.data.jsonserde.JsonSerDe' 
STORED AS INPUTFORMAT 
  'org.apache.hadoop.mapred.TextInputFormat' 
OUTPUTFORMAT 
  'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION
  's3://<WAFログ格納先S3バケット名>/AWSLogs/<AWSアカウント ID>/WAFLogs/${region}/<Web ACLs名>/'
TBLPROPERTIES(
 'projection.enabled' = 'true',
 'projection.region.type' = 'enum',
 'projection.region.values' = 'cloudfront',
 'projection.date.type' = 'date',
 'projection.date.range' = '2023/08/01,NOW',
 'projection.date.format' = 'yyyy/MM/dd',
 'projection.date.interval' = '1',
 'projection.date.interval.unit' = 'DAYS',
 'storage.location.template' = 's3://<WAFログ格納先S3バケット名>/AWSLogs/<AWSアカウント ID>/WAFLogs/${region}/<Web ACLs名>/${date}/') 

パーティション射影の確認

パーティション射影が効いているか簡単に確認してみます。
WHEREで日付指定した際は入力行、入力バイトが制限されている事が確認できました。

SELECT * FROM "waf_logs"

クエリ統計1

SELECT * FROM "waf_logs"
WHERE "date" = '2023/08/06'

クエリ統計2

AWS WAFログ分析

AWS WAFログのクエリ例も、AWS公式ドキュメント上に色々と記載されています。
https://docs.aws.amazon.com/ja_jp/athena/latest/ug/waf-logs.html#query-examples-waf-logs

実際のログ分析も、上記のクエリ例を参考にするとよさそうです。

おわりに

以上、非常に簡単な操作でAthenaを利用したAWS WAFのログ分析が可能になりました。
パーティションを手動で指定する必要がないのは運用上も大変便利で使いやすいですね。