AWS WAFのログをAmazon Data FirehoseでS3に配信しAthenaでクエリしてみた
はじめに
こんにちは。クラウド事業本部の木村です。
前回の記事で、WAFの出力先としてコスト観点でCloudWatch Logs、S3、Data Firehoseの比較を行いました。アップデートでCloudWatch Logs、S3に無料枠が追加されたことでData Firehoseに以前ほどのコスト優位性はなくなってしまいましたが、ログのレコード平均サイズ次第ではコスト最適になることがある点を確認しました。
本記事では実際にData Firehoseを使ってWAFログをS3に配信しAthenaでクエリするところまで案内する機会がありましたので、本ブログでまとめて手順をご紹介したいと思います。
Data Firehoseのサービス詳細については以下をご参照ください。
前提条件
- WAF WebACL が作成済み
- WAFログの出力先となるS3バケットが作成済み
- CloudFrontが作成済み
今回はCloudFrontにアタッチしたWAF WebACLを対象に手順を進めます。
構成の概要
今回構築する構成は以下のとおりです。

実際にやってみた
では実際に構築を進めます。
Firehose ストリームを作成する
今回CloudFrontにアタッチされているWAFのログを取得します。
CloudFrontはグローバルリソースとしてバージニア北部リージョン(us-east-1)に作成されます。Data Firehoseのリソースも同一リージョンである必要があるためバージニア北部で作成する必要があります。
Data Firehoseを作成する際には、リージョンをバージニア北部に切り替えてから作成します。

Data Firehoseのコンソールに移動して、作成します。

まずソースと配信先を選択します。
WAFから直接Firehoseに送信する構成のため、ソースはDirect PUTを選択します。

続いてストリーム名を指定します。この際ストリーム名は必ず aws-waf-logs- で始める必要があります。
これはWAF側の仕様であり、この命名規則に従っていないストリームはWAFログ設定画面に表示されないため、WAF側で設定できなくなってしまいます。
設定後に名前の変更はできないため、名前は間違えないように指定しましょう。

レコードを変換および転換については今回は利用しないため全てチェックを外した状態で進めます。

続いて送信先を設定します。
送信先の S3 バケットを選択し、S3 プレフィックスを設定します。
送信先のS3バケットは作成している任意のS3バケットを指定してください。
プレフィックスはAthenaでパーテーションを設定するために以下を指定します。
- S3 バケットプレフィックス
aws-waf-logs/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/hour=!{timestamp:HH}/
- S3 バケットエラー出力プレフィックス
aws-waf-logs-errors/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/hour=!{timestamp:HH}/!{firehose:error-output-type}/
これらはレコードの書き出し時刻から Firehose が自動的に値を埋め込む静的プレフィックスで、追加料金なしで利用可能です。
タイムゾーンは任意で設定してください。今回は東京を選択しています。

続いてバッファリングの設定です。
Firehose はレコードをバッファリングしてからまとめてS3に書き出します。
今回はデフォルトの推奨設定を利用します。

このバッファサイズまたはバッファ時間のいずれかの条件を満たすとS3に書き出しが行われます。
インシデント対応の際などにニアリアルタイムでログを確認したいなどの要件がある場合にはバッファ間隔を短く設定してください。ただし、ファイル生成回数が増えるためS3のPUTリクエスト数が増えるため誤差と言える範囲内ですがS3側のコストが上昇します。
続いて、S3への書き出し時の圧縮形式を設定します。
デフォルトは無効ですが、今回はgzipを設定します。
圧縮設定することで、S3ストレージコストを削減することができます。特別な理由がない限りコストになりますので圧縮しておきましょう。

この内容でストリームを作成します。
WAFでログを設定
WAF コンソール(us-east-1)から対象の WebACL を開き、ログ記録送信先の設定を選択します。

その後有効化からログ記録送信先を選択します。

送信先として、Data Firehoseを選択して先ほど作成したストリームを指定してください。

表示されない場合には作成したいストリームがaws-waf-logs- で始まるかを再度確認してください。
問題なければこの内容で保存します。
ログ設定後、実際にリクエストが流れるとバッファ時間経過後に S3 にファイルが配信されます。
設定後配信されていることが確認できれば、ログ配信の設定は完了です。
Athena でテーブルを作成する
Athenaコンソールのクエリエディタで以下のDDLを実行してテーブルを作成します。
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: STRING>,
nonTerminatingMatchingRules: ARRAY<STRUCT<ruleId: STRING, action: 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>
>>
>>,
requestHeadersInserted 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: BIGINT>,
challengeResponse STRUCT<responseCode: STRING, solveTimestamp: BIGINT>,
ja3Fingerprint STRING
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
LOCATION 's3://{バケット名}/aws-waf-logs/'
TBLPROPERTIES (
'projection.enabled' = 'true',
'projection.year.type' = 'integer',
'projection.year.range' = '2020,2099',
'projection.month.type' = 'integer',
'projection.month.range' = '1,12',
'projection.month.digits' = '2',
'projection.day.type' = 'integer',
'projection.day.range' = '1,31',
'projection.day.digits' = '2',
'projection.hour.type' = 'integer',
'projection.hour.range' = '0,23',
'projection.hour.digits' = '2',
'storage.location.template' = 's3://{バケット名}/aws-waf-logs/year=${year}/month=${month}/day=${day}/hour=${hour}/'
);
{バケット名}の部分は対象としたバケット名に置き換えてください。
クエリしてみる
では実際にクエリして試してみます。
事前に自分のIPを指定してBLOCKするルールを設定して、時間帯をずらしながらCloudFrontにアクセスしてみています。
BLOCKされたログを確認できるかクエリしてみます。
SELECT
from_unixtime(timestamp / 1000) AS request_time,
terminatingRuleId,
httpRequest.clientIp,
httpRequest.country,
action
FROM waf_logs
WHERE year = 2026
AND month = 4
AND day = 13
AND action = 'BLOCK'
ORDER BY timestamp DESC
LIMIT 100;

無事クエリできるところまで確認できました。
Athenaは対象となる容量に応じてコストが発生します。
今回はこのテスト用に作ったログを対象にクエリするので、全量を対象にしてもコストがほぼ発生しません。(サンプルとしてパーテーション指定しています。)
しかし実環境ではコスト観点で移行を考えるレベルでログ量が多い場合に全量スキャンしてしまうとかなりの費用が発生してしまいます。
クエリするときには調査したい範囲を絞って、日時をパーティション条件として指定してスキャンを行いましょう。
日時を指定することでスキャン量を削減することができます。
まとめ
今回はAmazon Data Firehoseを使ってログを配信してクエリするところまで試してみました。
前回の記事で比較した通り、レコード当たりの容量が大きいケースでは無料枠追加後もコスト的に有利ですのでログの配信量が多く費用が発生している場合にはData Firehoseを使って配信してみると大きな削減につながるかもしれません。
今回の記事が参考になれば幸いです。
以上、クラウド事業本部の木村がお届けしました。
参考






