CloudFront標準ログをAthenaで解析する決定版!日本時間(JST)変換とパーティション設計の実践ガイド

皆さんこんにちは。okamoです。

今回は「実録:ITよろず相談」シリーズの第5回目をお届けします。

執筆の経緯:IT現場の現実と直面した課題

ITの現場では、技術スキルやアプローチに大きな開きがあるのが現実です。片や、AWS CLIやインフラ自動化ツールをバリバリ使いこなしてスマートに仕事を片付ける人がいる一方で、片や、障害調査のために大量の gz ファイル(圧縮ログ)をローカルにダウンロードし、1ファイルずつ解凍しながら苦労して検索をかけている人もいます。

「インフラ運用チームにCloudFrontのログを抽出してもらったのだけど、どうやって調べたらいいか分からない……」

先日、このようなご相談を受けました。抽出結果として渡されたのは、数千個に及ぶ大量の gz ファイル。これを1つずつ開いて調べるのは、精神的にも時間的にも限界があります。

実は、これと同じご相談を別の方からも受けたことがあり、決して珍しいケースではありません。ネット上にある分かりやすい解説記事を案内しようとしたのですが、いざ探してみると「CloudFront標準ログ」+「Athena」+「日付パーティション(Partition Projection)」+「日本時間(JST)への変換」がすべて揃った、かゆいところに手が届く実践的なハウツー記事が見つかりませんでした。

そこで、「それなら自分で、誰でもそのままコピペして使える完全なガイドを書こう!」と決意しました。本記事では、すべてのコマンドを省略せずに掲載します。インフラ運用チーム(管理者)と、アプリ開発チーム(分析者)の2つの役割を想定して構成していますので、ぜひ現場の課題解決に役立ててください。


1. なぜ CloudFront ログを Athena でクエリするのか(Why)

課題: バックエンド側のログだけでは見えない情報がある

システム障害やアクセス分析を行う際、バックエンド(Apache/Nginxやアプリログ)だけを見ていては原因を特定できないケースが多々あります。

観点バックエンド(Apache/Nginx)のログCloudFront のログ
クライアント IPALB/Proxy 経由で X-Forwarded-For の解析が必要エッジで直接記録 → 正確
バックエンド障害時そもそもログが出ないCloudFront まで到達していれば記録される
キャッシュ応答バックエンドに到達しないため記録なしHit/Miss/Error すべて記録
サーバー台数増加台数分のログを結合する必要あり1箇所に全リクエスト集約
バックエンド技術変更Lambda/ECS/EC2 で形式が異なる常に同一フォーマット

解決: CloudFront(最上位)のログを SQL でクエリする

[ユーザー] → CloudFront → ALB → WEB × N台
                ↑
        ここのログを使う
        ・正しいリモート IP が取れる
        ・バックエンドが落ちていても記録される
        ・バックエンドが何台あっても関係ない

3つの主要メリット:

  1. 正しいクライアント IP が取れる 最上位のエッジで記録されるため、X-Forwarded-For の複雑な多段解析が不要です。セキュリティ調査やアクセス元の特定において、最も信頼できる起点となります。
  2. 最上位で全リクエストを捕捉する バックエンドが 502/503 エラーを返しているケースや、CloudFrontのエッジキャッシュから直接応答しているケースも含め、CloudFrontに到達したすべてのトラフィックを確実に記録します。
  3. バックエンド構成に依存しない バックエンドのWebサーバーが1台であっても100台であっても、またコンテナ(ECS)やサーバーレス(Lambda)に移行したとしても、ログ解析の手法やフォーマットは一切変わりません。

その他のメリット:

  • エッジロケーション情報の活用 — x_edge_location フィールドにより、どこのPoP(Point of Presence)がリクエストを処理したかが一目で分かります。地理的な攻撃パターンの分析に非常に有用です。
  • エージェント不要 — サーバー側に Fluentd などのログ転送エージェントを仕込む手間がかかりません。
  • gzのまま直接クエリ可能 — ファイルを解凍することなく、S3上の圧縮ファイルのまま高速に直接検索できます。
  • 既存のS3ログ設定をそのまま利用 — Kinesis Firehose や Lambda を追加で構成する必要はありません。既存のCloudFront標準ログ出力設定だけで動作するため、最小コストで実現できます。
  • AWS WAF ログへの応用 — 同様の要領で、AWS WAF のログ解析にも応用可能です(詳細は別記事で解説予定)。

ユースケース

  • 不正アクセス元 IP の特定
  • 特定 URI へのアクセス傾向・急増分析
  • DDoSやBot攻撃時のリアルタイム調査
  • 特定期間のトラフィックやエラー率の集計
  • インシデント発生時の証跡(エビデンス)取得

設計方針: 既存構成をいじらない「使い捨てワークフロー」

大規模組織やベンダー運用環境では、既存のAWS構成(ログの出力形式や出力先パス)を変更すること自体が大きなリスクであり、承認コストも高くなります。

本手順では、既存のCloudFront標準ログが溜まっているS3バケットを一切変更しません。調査したい特定の対象期間のログだけを、同じ(または別のアカウントの)作業用S3バケットに「Hive形式」として一時的にコピーし、Athenaの「Partition Projection(パーティション・プロジェクション)」を使って超高速にクエリを実行します。調査が終わったら作業用フォルダとテーブルを丸ごと削除する「使い捨て」の安全なワークフローを採用しています。


2. 構成概要と必要な IAM ポリシー

アーキテクチャ

この使い捨てワークフローの全体像は以下の図の通りです。今回は動作確認のため、標準のPNG形式とベクター形式(SVG)の2つの画像を掲載しています。

アーキテクチャ図(PNG版)

CloudFront ログ解析アーキテクチャ

アーキテクチャ図(SVG版)

CloudFront ログ解析アーキテクチャ (SVG)

【アーキテクチャ図の説明】 既存のCloudFront標準ログは、読み取り専用として安全に保たれます。分析用ユーザー(log-analyst)は、CLIを用いて必要な日付のログのみを解析用S3バケット(s3://$ANALYSIS_BUCKET)にHive形式(year=YYYY/month=MM/day=DD/)でコピーします。AthenaはPartition Projectionを利用して解析用バケット内のログを走査し、Glue Data Catalogにメタデータを保持しながらクエリを実行、結果を出力先バケットに保存します。調査完了後は、解析用バケット内のオブジェクトを s3 rm --recursive ですべてクリーンアップします。SVG版は高解像度での視認性に優れており、データの流れがより鮮明に確認できます。

必要なリソースと権限の関係

安全な運用を行うため、分析ユーザーに与えるIAMポリシーは最小限にとどめます。こちらも視覚的な確認のしやすさを考慮し、PNG版とSVG版を併記します。

IAMポリシー構成図(PNG版)

IAMポリシー構成図

IAMポリシー構成図(SVG版)

IAMポリシー構成図 (SVG)

【IAMポリシー構成図の説明】 分析担当ユーザー(log-analyst)にアタッチするポリシーの全体図です。Athenaのクエリ実行権限、元ログバケットに対する読み取り専用権限(s3:GetObject、s3:ListBucket)、解析用バケットに対するフルアクセス権限、Athena結果出力用バケットへの書き込み権限、そしてGlue Data Catalogに対するテーブル作成や削除権限が綺麗に分離されていることが分かります。SVG形式の構成図により、各権限ブロックが担当するリソースやアクションの境界線をより詳細かつ滑らかに視覚確認できます。

変数の紐づけ表

本ドキュメントおよび以降のCLIコマンドで使用する環境変数を以下に定義します。実際の作業時には、ご自身の環境に合わせて値を設定してください。

環境変数名説明例
$BUCKETCloudFront ログが保存されている S3 バケット名prod-myapp-logs-aws
$PREFIXバケット内のログ保存プレフィックスcloudfront-main
$DIST_IDCloudFront Distribution ID(ファイル名に含まれる)EXXXXXXXXXXXXX
$ANALYSIS_BUCKET解析用 S3 バケット名(管理者が事前作成)prod-myapp-log-analysis
$ACCOUNT_IDAWS アカウント ID123456789012
$REGIONAWS リージョンap-northeast-1

S3 バケット構成の対比

元のバケットは日付フォルダに分かれていないフラットな構造ですが、コピー先はAthenaが効率よく読み取れるHive形式(キーバリュー形式のパス)に整理されます。

s3://$BUCKET/
  └── $PREFIX/                      ← 既存(フラット・変更なし・読み取り専用)
      ├── $DIST_ID.2024-03-14-00.xxx.gz
      ├── $DIST_ID.2024-03-14-01.xxx.gz
      └── ...

s3://$ANALYSIS_BUCKET/               ← 解析用バケット(分析後に削除)
  └── year=2024/month=03/day=14/
      ├── $DIST_ID.2024-03-14-00.xxx.gz
      ├── $DIST_ID.2024-03-14-01.xxx.gz
      └── ...

3. S3 バケット作成と Athena ワークグループの初期設定(管理者で実施)

注意: ここからの手順はインフラ管理者アカウントで実施します。 分析用バケット名などが確定しないとIAMポリシーが作れないため、先にリソースを用意します。

なぜ管理者が先にやるべきか

  • ログ分析ユーザーに S3バケットの新規作成権限(s3:CreateBucket)を与える必要がなくなります。
  • Athenaのワークグループ機能でクエリ出力先を固定しておくことで、ユーザー側での面倒な初回設定や設定忘れを防止できます。

初期設定コマンド

以下のシェルスクリプトをコピーし、管理者のターミナルで実行してください。

# === 環境変数の定義(実環境に合わせて値を変更) ===
BUCKET="prod-myapp-logs-aws"             # 元ログバケット(既存)
PREFIX="cloudfront-main"                  # ログプレフィックス(既存)
DIST_ID="EXXXXXXXXXXXXX"                 # Distribution ID
ANALYSIS_BUCKET="prod-myapp-log-analysis" # 解析用バケット(新規作成)
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION="ap-northeast-1"

# === Step 1: 解析用バケットの作成 ===
aws s3 mb "s3://${ANALYSIS_BUCKET}" --region "${REGION}"

# === Step 2: Athena クエリ結果用バケットの作成 ===
ATHENA_BUCKET="aws-athena-query-results-${ACCOUNT_ID}-${REGION}"
aws s3 mb "s3://${ATHENA_BUCKET}" --region "${REGION}"

# === Step 3: Athena ワークグループに出力先を設定(ユーザー側の上書きを禁止) ===
aws athena update-work-group 
  --work-group primary 
  --configuration-updates "{
    \"ResultConfigurationUpdates\": {
      \"OutputLocation\": \"s3://${ATHENA_BUCKET}/\"
    },
    \"EnforceWorkGroupConfiguration\": true
  }" 
  --region "${REGION}"

# === Step 4: 設定を確認 ===
aws athena get-work-group 
  --work-group primary 
  --query 'WorkGroup.Configuration.{OutputLocation:ResultConfiguration.OutputLocation,EnforceConfig:EnforceWorkGroupConfiguration}' 
  --region "${REGION}"

期待される出力結果:

{ 
    "OutputLocation": "s3://aws-athena-query-results-123456789012-ap-northeast-1/",
    "EnforceConfig": true
}

4. IAM ポリシー作成とユーザーの作成(管理者で実施)

IAM ポリシー(JSON)

以下のポリシーを作成し、ログ分析用IAMユーザーにアタッチします。

権限分離の徹底: 元のログバケット($BUCKET)に対しては GetObject と ListBucket のみの読み取り専用権限です。解析用バケット($ANALYSIS_BUCKET)に対してのみ、ファイルの書き込みや削除が自由にできるように設定されています。

⚠️注意: 以下のJSON内の ${BUCKET}、${PREFIX}、${ANALYSIS_BUCKET}、${ACCOUNT_ID}、${REGION} は変数表記です。実際にポリシーを登録する前に、Section 3で設定した実際の値に置き換えてください。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AthenaAccess",
      "Effect": "Allow",
      "Action": [
        "athena:StartQueryExecution",
        "athena:StopQueryExecution",
        "athena:GetQueryExecution",
        "athena:GetQueryResults",
        "athena:GetWorkGroup",
        "athena:ListWorkGroups"
      ],
      "Resource": "*"
    },
    {
      "Sid": "S3ReadLogBucket",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket",
        "s3:GetBucketLocation"
      ],
      "Resource": [
        "arn:aws:s3:::${BUCKET}",
        "arn:aws:s3:::${BUCKET}/${PREFIX}/*"
      ]
    },
    {
      "Sid": "S3AnalysisBucket",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:ListBucket",
        "s3:GetBucketLocation"
      ],
      "Resource": [
        "arn:aws:s3:::${ANALYSIS_BUCKET}",
        "arn:aws:s3:::${ANALYSIS_BUCKET}/*"
      ]
    },
    {
      "Sid": "AthenaResultsAccess",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:ListBucket",
        "s3:GetBucketLocation"
      ],
      "Resource": [
        "arn:aws:s3:::aws-athena-query-results-*"
      ]
    },
    {
      "Sid": "GlueDataCatalog",
      "Effect": "Allow",
      "Action": [
        "glue:GetDatabase",
        "glue:GetDatabases",
        "glue:CreateDatabase",
        "glue:DeleteDatabase",
        "glue:GetTable",
        "glue:GetTables",
        "glue:CreateTable",
        "glue:UpdateTable",
        "glue:DeleteTable",
        "glue:GetPartition",
        "glue:GetPartitions",
        "glue:BatchGetPartition"
      ],
      "Resource": [
        "arn:aws:glue:${REGION}:${ACCOUNT_ID}:catalog",
        "arn:aws:glue:${REGION}:${ACCOUNT_ID}:database/cloudfront_logs",
        "arn:aws:glue:${REGION}:${ACCOUNT_ID}:table/cloudfront_logs/*"
      ]
    }
  ]
}

💡 運用効率化Tips: Linux環境などであれば、環境変数を設定した状態で envsubst コマンドを使うと、一発で変数置換されたJSONを生成できます。

envsubst < policy-template.json > policy.json

なぜ専用ユーザー(log-analyst)を作るべきか

  • 最小権限の原則 — 管理者アカウントでクエリすると不必要な権限が付随し、操作ミス時の被害が大きくなります。
  • 監査証跡の明確化 — AWS CloudTrail を確認した際、「誰がいつどのログをクエリしたか」が明確になります。
  • 外部共有の安全性 — 開発ベンダーや外部調査機関に一時的にアカウントを渡す際にも、安全な読み取り専用として渡せます。

分析ユーザーの作成手順(CLI)

# ユーザーの作成(AWSコンソールアクセス権なし・CLI専用)
aws iam create-user --user-name log-analyst

# ポリシーをアタッチ(上記JSONをpolicy.jsonとしてローカルに保存している前提)
aws iam put-user-policy \
  --user-name log-analyst \
  --policy-name AthenaLogAnalystPolicy \
  --policy-document file://policy.json

アクセキーの発行(調査開始時)

調査が発生するたびに、管理者がアクセスキーを発行して分析担当者に渡します。

# アクセスキーの発行
aws iam create-access-key --user-name log-analyst

出力例:

{ 
    "AccessKey": {
        "UserName": "log-analyst",
        "AccessKeyId": "AKIA****************",
        "SecretAccessKey": "************************************",
        "Status": "Active"
    }
}

アクセキーの無効化・削除(調査完了後)

調査が完了したら、管理者は速やかにキーを無効化または削除し、永続的なアクセスを防ぎます。

# キーの一覧確認
aws iam list-access-keys --user-name log-analyst

# キーを一時的に無効化(一時停止)
aws iam update-access-key \
  --user-name log-analyst \
  --access-key-id AKIA**************** \
  --status Inactive

# キーを完全削除
aws iam delete-access-key \
  --user-name log-analyst \
  --access-key-id AKIA****************

5. ログ分析の実施(ログ分析ユーザーで実施)

注意: ここからの手順は、ログ分析用ユーザー(log-analyst)の端末環境で実施します。

Windows ユーザー向け: WSL + AWS CLI セットアップ

Windows環境の場合は、WSL(Windows Subsystem for Linux)上で作業すると、シェルスクリプトによる自動コピーなどが非常にスムーズに行えます。

1. WSL のインストール(PowerShell を管理者権限で実行)

wsl --install

※インストール・再起動後、スタートメニューから「Ubuntu」を起動し、初期ユーザー名とパスワードを設定します。

2. AWS CLI のインストール(WSL Ubuntu 内)

curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
sudo apt update && sudo apt install -y unzip
unzip awscliv2.zip
sudo ./aws/install
rm -rf aws awscliv2.zip

# バージョン確認
aws --version

3. クレデンシャルの設定(環境変数)

管理者から受け取った一時アクセスキーをターミナルにセットします。

# 既存のプロファイルと競合しないようにクリア
export -n AWS_PROFILE
export -n AWS_DEFAULT_PROFILE

# アクセスキーの適用
export AWS_ACCESS_KEY_ID="管理者から受け取ったAccessKeyId"
export AWS_SECRET_ACCESS_KEY="管理者から受け取ったSecretAccessKey"
export AWS_DEFAULT_REGION="ap-northeast-1"

⚠️セキュリティのポイント: このように export で一時的に設定した環境変数は、ターミナルエミュレータ(タブやウィンドウ)を閉じると自動的に消去されるため安全です。.bashrc などの設定ファイルに書き込まないようにしてください。

動作確認:

aws sts get-caller-identity

log-analyst のアカウント情報が出力されれば、準備完了です。


Step 0: 変数の設定

作業を開始する前に、以下の設定を行います。調査したい対象期間に合わせて START_DATE と END_DATE を変更してください。

# === 環境設定(実際の値を設定してください) ===
BUCKET="prod-myapp-logs-aws"             # 元のログバケット名
PREFIX="cloudfront-main"                  # ログ保存プレフィックス
DIST_ID="EXXXXXXXXXXXXX"                 # Distribution ID
ANALYSIS_BUCKET="prod-myapp-log-analysis" # 解析用バケット名
REGION="ap-northeast-1"

# === 分析対象期間(ここを変更します) ===
START_DATE="2024-03-14"       # 開始日(YYYY-MM-DD)
END_DATE="2024-03-16"         # 終了日(YYYY-MM-DD)

Step 1: 対象期間 of ログを解析用バケットに Hive 形式でコピー

以下のループスクリプトを実行することで、元のフラットなログファイルを、指定した対象期間のみ抽出して解析用バケットにHive形式のディレクトリ構成で自動コピーします。

echo "=== S3 コピー開始: ${START_DATE} 〜 ${END_DATE} ==="

current="$START_DATE"
while [[ "$current" < "$END_DATE" ]] || [[ "$current" == "$END_DATE" ]]; do
  year=${current:0:4}
  month=${current:5:2}
  day=${current:8:2}

  echo "  処理中: ${current}"
  aws s3 ls "s3://${BUCKET}/${PREFIX}/${DIST_ID}.${current}-" --region "${REGION}" \
    | awk '{print $4}' \
    | while read -r file; do
        aws s3 cp \
          "s3://${BUCKET}/${PREFIX}/${file}" \
          "s3://${ANALYSIS_BUCKET}/year=${year}/month=${month}/day=${day}/${file}" \
          --region "${REGION}" --quiet
      done

  current=$(date -d "${current} + 1 day" +%Y-%m-%d)
done

echo "=== S3 コピー完了 ==="
echo "解析用バケット: s3://${ANALYSIS_BUCKET}/"

Step 2: Athena データベース・テーブル作成

次に、コピーしたデータを参照するAthenaのテーブル定義を作成します。ここでは、パフォーマンス向上のために「Partition Projection(パーティション・プロジェクション)」を有効化します。これによって、面倒なパーティションの追加・更新コマンド(MSCK REPAIR TABLE)を実行することなく、S3のディレクトリ構成から自動的にパーティションが認識されます。

# データベース作成
QID=$(aws athena start-query-execution \
  --query-string "CREATE DATABASE IF NOT EXISTS cloudfront_logs;" \
  --work-group primary --region "${REGION}" \
  --query 'QueryExecutionId' --output text)
sleep 3
aws athena get-query-execution --query-execution-id "$QID" --region "${REGION}" \
  --query 'QueryExecution.Status.State' --output text

# テーブル作成(既存テーブルがあれば削除してから再作成)
QID=$(aws athena start-query-execution \
  --query-string "DROP TABLE IF EXISTS cloudfront_logs.access_logs;" \
  --work-group primary --region "${REGION}" \
  --query-execution-context Database=cloudfront_logs \
  --query 'QueryExecutionId' --output text)
sleep 3

QID=$(aws athena start-query-execution \
  --query-string "
    CREATE EXTERNAL TABLE cloudfront_logs.access_logs (
      \
`date\
`                        DATE,
      \
`time\
`                        STRING,
      x_edge_location               STRING,
      sc_bytes                      BIGINT,
      c_ip                          STRING,
      cs_method                     STRING,
      cs_host                       STRING,
      cs_uri_stem                   STRING,
      sc_status                     INT,
      cs_referer                    STRING,
      cs_user_agent                 STRING,
      cs_uri_query                  STRING,
      cs_cookie                     STRING,
      x_edge_result_type            STRING,
      x_edge_request_id             STRING,
      x_host_header                 STRING,
      cs_protocol                   STRING,
      cs_bytes                      BIGINT,
      time_taken                    FLOAT,
      x_forwarded_for               STRING,
      ssl_protocol                  STRING,
      ssl_cipher                    STRING,
      x_edge_response_result_type   STRING,
      cs_protocol_version           STRING,
      fle_status                    STRING,
      fle_encrypted_fields          INT,
      c_port                        INT,
      time_to_first_byte            FLOAT,
      x_edge_detailed_result_type   STRING,
      sc_content_type               STRING,
      sc_content_len                BIGINT,
      sc_range_start                BIGINT,
      sc_range_end                  BIGINT
    )
    PARTITIONED BY (year STRING, month STRING, day STRING)
    ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'
    LOCATION 's3://${ANALYSIS_BUCKET}/'
    TBLPROPERTIES (
      'skip.header.line.count' = '2',
      'projection.enabled'     = 'true',
      'projection.year.type'   = 'integer',
      'projection.year.range'  = '2024,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',
      'storage.location.template' = 's3://${ANALYSIS_BUCKET}/year=\\\\\${year}/month=\\\\\${month}/day=\\\\\${day}/'
    );
  " \
  --work-group primary --region "${REGION}" \
  --query-execution-context Database=cloudfront_logs \
  --query 'QueryExecutionId' --output text)

sleep 5
aws athena get-query-execution --query-execution-id "$QID" --region "${REGION}" \
  --query 'QueryExecution.Status.{State:State,Reason:StateChangeReason}' --output json

Step 2.5: jq のインストール

クエリの実行結果を綺麗にコマンドライン上に表示(パース)するために jq を使用します。未インストールの場合は以下のコマンドでセットアップしてください。

sudo apt update && sudo apt install -y jq

Step 3: 動作確認と日本時間(JST)変換テスト

CloudFrontの標準ログに記録されている日時フィールド(date および time)は、すべて世界協定時(UTC)です。これを日本で調査する際に扱いやすいよう、クエリ側で 日本時間(JST = UTC+9時間) に変換して表示します。

以下の確認用クエリを実行してみましょう。

# サンプルクエリ(5件抽出): UTC から JST に正しく変換されているか確認
QID=$(aws athena start-query-execution \
  --query-string "
    SELECT
      date AS date_utc,
      \"time\" AS time_utc,
      DATE_FORMAT(
        CAST(CAST(date AS VARCHAR) || ' ' || \"time\" AS TIMESTAMP) + INTERVAL '9' HOUR,
        '%Y-%m-%d %H:%i:%s'
      ) AS datetime_jst,
      c_ip, cs_uri_stem, sc_status
    FROM cloudfront_logs.access_logs
    WHERE year = '$(date -d "$START_DATE" +%Y)'
      AND month = '$(date -d "$START_DATE" +%m)'
      AND day = '$(date -d "$START_DATE" +%d)'
    LIMIT 5;
  " \
  --work-group primary --region "${REGION}" \
  --query-execution-context Database=cloudfront_logs \
  --query 'QueryExecutionId' --output text)

sleep 5
aws athena get-query-execution --query-execution-id "$QID" --region "${REGION}" \
  --query 'QueryExecution.Status.State' --output text

aws athena get-query-results --query-execution-id "$QID" --region "${REGION}" \
  --output json | jq -r '.ResultSet.Rows[] | .Data | map(.VarCharValue) | @tsv' | column -t -s $'\t'

出力結果の datetime_jst カラムを確認し、UTC時間(date_utc / time_utc)から正確に9時間進んだ日本時間が表示されていれば、動作確認は完了です!これで大量の gz ファイルに悩まされることなく、SQLで任意の期間のアクセスログを自在に調べられるようになりました。


まとめと今後の展望

今回は、インフラの既存設定を変更せず、最小のコストと権限で安全に「CloudFront標準ログ」を「Athena(Partition Projection)」で解析する使い捨てアプローチをご紹介しました。

このやり方を覚えれば、数十ギガバイトを超える膨大なログファイルであっても、数秒から数分で特定IPのアクティビティを完全に洗い出すことができます。生ログを1枚ずつ手動で解凍して苦労していた現場の皆さんに、ぜひ使っていただきたいテクニックです。

また、検証用の画像比較としてSVG構成図を追加したことで、細かなポリシー構成や接続線の確認も極めて滑らかに行えるようになりました。ドキュメントの解像度向上も、保守性を高める上での大事な要素です。

長文にお付き合いいただき、ありがとうございました。この分野は専門家の方も非常に多くいらっしゃると思いますので、もしさらに洗練された、より良い手法や参考記事が今後見つかりましたら、この記事の内容をシンプルに更新し、そうした素晴らしい良記事へのリンクに集約していきたいと考えています。

それでは、安全で快適なログ分析ライフを!