Amazon CloudFront のアクセスログを Amazon S3 にパーティション分けして保存する [AWS CloudFormation 使用]

こんにちは、広野です。

Amazon CloudFront のアクセスログをパーティション分けして Amazon S3 に保存できるようになるアップグレードがありましたので、既存アプリの構成を変更しました。AWS CloudFormation でデプロイしているので、テンプレートベースで説明します。

作成した構成

以下のように、2つの Amazon CloudFront ディストリビューションのアクセスログを 1つの共用 Amazon S3 バケットに保存する構成です。バケットの中でフォルダ分け (パーティション分け) します。これができないと後々 Amazon Athena での検索が高負荷になってしまいます。

AWS マネジメントコンソールからの設定は簡単で、以下公式ドキュメントの通りです。Amazon CloudFront ディストリビューションのロギング設定画面からできます。従来のロギング設定はコンソール画面上でも Legacy と表示されているので、間違えないかと思います。

AWS CloudFormation によるデプロイ

AWS CloudFormation でデプロイするときは、Amazon CloudFront のリソースとして定義するのではなく、Amazon CloudWatch Logs の設定として定義します。これは、上記ドキュメントにも書いてありましたが Amazon CloudWatch を内部的に使用しているからです。

なお、Amazon CloudFront は本体がバージニア北部リージョンにあるため、AWS CloudFormation でスタックを作成するときにはバージニア北部でテンプレートを流す必要があります。

概念的には以下の図のようになります。

今回の設計では、ログ配信先は共用の Amazon S3 バケットにしたかったので、その定義は 1つになります。

ログソースは Amazon CloudFront ディストリビューションが 2つありますので、定義は 2つになります。ログソースとログ配信先を紐づける定義が必要になるので、やはりそれぞれに必要になります。

AWS CloudFormation テンプレート

実際の例は以下になります。前提として、Amazon CloudFront ディストリビューションは完成していて、ID を知っている状況です。

細かい説明はテンプレート内にコメントします。

AWSTemplateFormatVersion: 2010-09-09
Description: The CloudFormation template that creates CloudFront access logging configurations. You had better delete the stack and create it again if you update this configurations.

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
  SubName:
    Type: String
    Description: System sub name of example. (e.g. prod or test)
    Default: test
    MaxLength: 10
    MinLength: 1

# 1つ目のCloudFrontのIDをパラメータにしています。
  CloudFrontDistributionIdImg:
    Type: String
    Description: The CloudFront distribution id for img.
    Default: XXXXXXXXXXXXX
    MaxLength: 15
    MinLength: 12

# 2つ目のCloudFrontのIDをパラメータにしています。
  CloudFrontDistributionIdAppsync:
    Type: String
    Description: The CloudFront distribution id for appsync.
    Default: XXXXXXXXXXXXX
    MaxLength: 15
    MinLength: 12

Resources:
# ------------------------------------------------------------#
#  CloudWatch Logs (for CloudFront)
# ------------------------------------------------------------#
# ログ配信先の定義です。1つのS3バケットを共用します。フォルダのプレフィックスに cloudfrontAccesslog を設定しています。
# 出力フォーマットは JSON にしていますが、実際には GZIP 圧縮されます。
  LogsDeliveryDestination:
    Type: AWS::Logs::DeliveryDestination
    Properties:
      Name: !Sub s3-example-${SubName}-logs
      DestinationResourceArn: !Sub "arn:aws:s3:::example-${SubName}-logs/cloudfrontAccesslog"
      OutputFormat: json
      Tags:
        - Key: Cost
          Value: !Sub example-${SubName}

# 1つ目のログソース定義です。
  LogsDeliverySourceImg:
    Type: AWS::Logs::DeliverySource
    Properties:
      Name: !Sub example-${SubName}-cloudfront-img
      ResourceArn: !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistributionIdImg}"
      LogType: ACCESS_LOGS
      Tags:
        - Key: Cost
          Value: !Sub example-${SubName}

# 1つ目のログソース、ログ配信先の紐づけ定義です。
  LogsDeliveryImg:
    Type: AWS::Logs::Delivery
    Properties:
      DeliverySourceName: !Sub example-${SubName}-cloudfront-img
      DeliveryDestinationArn: !GetAtt LogsDeliveryDestination.Arn
      # 記録する項目はデフォルトです。
      RecordFields:
        - timestamp
        - DistributionId
        - date
        - time
        - x-edge-location
        - sc-bytes
        - c-ip
        - cs-method
        - cs(Host)
        - cs-uri-stem
        - sc-status
        - cs(Referer)
        - cs(User-Agent)
        - cs-uri-query
        - cs(Cookie)
        - x-edge-result-type
        - x-edge-request-id
        - x-host-header
        - cs-protocol
        - cs-bytes
        - time-taken
        - x-forwarded-for
        - ssl-protocol
        - ssl-cipher
        - x-edge-response-result-type
        - cs-protocol-version
        - fle-status
        - fle-encrypted-fields
        - c-port
        - time-to-first-byte
        - x-edge-detailed-result-type
        - sc-content-type
        - sc-content-len
        - sc-range-start
        - sc-range-end
        - timestamp(ms)
        - origin-fbl
        - origin-lbl
        - asn
      # ここで年月日と時間でパーティション分けしています。
      S3SuffixPath: img/{yyyy}/{MM}/{dd}/{HH}
      S3EnableHiveCompatiblePath: false
      Tags:
        - Key: Cost
          Value: !Sub example-${SubName}
    DependsOn:
      - LogsDeliveryDestination
      - LogsDeliverySourceImg

  # 2つ目のログソース定義です。
  LogsDeliverySourceAppsync:
    Type: AWS::Logs::DeliverySource
    Properties:
      Name: !Sub example-${SubName}-cloudfront-appsync
      ResourceArn: !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistributionIdAppsync}"
      LogType: ACCESS_LOGS
      Tags:
        - Key: Cost
          Value: !Sub example-${SubName}

  # 2つ目のログソース、ログ配信先定義です。基本的な設定は1つ目と全く同じです。
  LogsDeliveryAppsync:
    Type: AWS::Logs::Delivery
    Properties:
      DeliverySourceName: !Sub example-${SubName}-cloudfront-appsync
      DeliveryDestinationArn: !GetAtt LogsDeliveryDestination.Arn
      RecordFields:
        - timestamp
        - DistributionId
        - date
        - time
        - x-edge-location
        - sc-bytes
        - c-ip
        - cs-method
        - cs(Host)
        - cs-uri-stem
        - sc-status
        - cs(Referer)
        - cs(User-Agent)
        - cs-uri-query
        - cs(Cookie)
        - x-edge-result-type
        - x-edge-request-id
        - x-host-header
        - cs-protocol
        - cs-bytes
        - time-taken
        - x-forwarded-for
        - ssl-protocol
        - ssl-cipher
        - x-edge-response-result-type
        - cs-protocol-version
        - fle-status
        - fle-encrypted-fields
        - c-port
        - time-to-first-byte
        - x-edge-detailed-result-type
        - sc-content-type
        - sc-content-len
        - sc-range-start
        - sc-range-end
        - timestamp(ms)
        - origin-fbl
        - origin-lbl
        - asn
      S3SuffixPath: appsync/{yyyy}/{MM}/{dd}/{HH}
      S3EnableHiveCompatiblePath: false
      Tags:
        - Key: Cost
          Value: !Sub example-${SubName}
    # 1つ目のロギング設定が完了していないのに2つ目をデプロイしようとするとエラーになります。
    # そのため DependsOn を使用して1つ目の設定が完成後に2つ目をデプロイするよう制御しています。
    DependsOn:
      - LogsDeliverySourceAppsync
      - LogsDeliveryImg

 

また、本件の構成ではログ配信先 Amazon S3 バケットに以下のバケットポリシーが必要です。

  S3BucketLogs:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub example-${SubName}-logs
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      Tags:
        - Key: Cost
          Value: !Sub example-${SubName}
  S3BucketPolicyLogs:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3BucketLogs
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action: s3:PutObject
            Principal:
              Service: delivery.logs.amazonaws.com
            Resource: !Sub "${S3BucketLogs.Arn}/*"
            Condition:
              StringEquals:
                aws:SourceAccount: !Ref AWS::AccountId
                s3:x-amz-acl: bucket-owner-full-control
              ArnLike:
                aws:SourceArn: !Sub "arn:aws:logs:us-east-1:${AWS::AccountId}:delivery-source:example-${SubName}-*"

注意事項

  • 繰り返しになりますが、AWS CloudFormation テンプレートはバージニア北部リージョン (us-east-1) で実行します。
  • 今回使用している AWS CloudFormation のログ配信先リソース定義 AWS::Logs::DeliveryDestination は、単純に変更しようとすると Name を変更しなさい、というエラーが発生します。なので変更をかける都度、Name を動的に変更したいのですが AWS CloudFormation テンプレートの記述のみでは実現できないので、もし変更する際には一度スタックを削除する方が確実です。カスタムリソースで AWS Lambda 関数と組み合わせたり、CI/CD ツールの機能や CDK とかであれば実現は可能と考えます。
  • また、ログソースと配信先を紐づける定義 AWS::Logs::Delivery は同じ種類のログ配信 (S3 なら S3、Firehose なら Firehose) を 1つのログソースに複数設定できないので、AWS CloudFormation で変更をかけようとするとこれまたエラーになります。上述、一度スタックを削除した方が確実と申し上げたのはそのような理由もあります。これらの課題は AWS さんによる今後の改善を期待します。
  • 従来、Amazon CloudFront から Amazon S3 にロギングするときには、対象の Amazon S3 バケットに ACL が必要でした。新しいロギングではそれが不要になり、代わりにバケットポリシーが必要になっています。既存のロギング用バケットに AWS CloudFormation による変更をかける際には、先に ACL を手動削除しないとエラーになることがあります。

まとめ

いかがでしたでしょうか?

なんとなく、今後 AWS リソースのロギング設定は今回のように Amazon CloudWatch Logs 側での設定に集約されていくのかな?と感じました。実際、イベントトリガーのアクションについては Amazon EventBridge が主流になっていますし、ロギングもそうなるのかな、という想像です。

本記事が皆様のお役に立てれば幸いです。

著者について
広野 祐司

AWS サーバーレスアーキテクチャを駆使して社内クラウド人材育成アプリとコンテンツづくりに勤しんでいます。React で SPA を書き始めたら快適すぎて、他の言語には戻れなくなりました。サーバーレス & React 仲間を増やしたいです。AWSは好きですが、それよりもフロントエンド開発の方が好きでして、バックエンド構築を簡単にしてくれたAWSには感謝の気持ちの方が強いです。
取得資格:AWS 認定は13資格、ITサービスマネージャ、ITIL v3 Expert 等
2020 - 2024 Japan AWS Top Engineer 受賞
2022 - 2024 AWS Ambassador 受賞
2023 当社初代フルスタックエンジニア認定
好きなAWSサービス:AWS Amplify / AWS AppSync / Amazon Cognito / AWS Step Functions / AWS CloudFormation

広野 祐司をフォローする

クラウドに強いによるエンジニアブログです。

SCSKクラウドサービス(AWS)は、企業価値の向上につながるAWS 導入を全面支援するオールインワンサービスです。AWS最上位パートナーとして、多種多様な業界のシステム構築実績を持つSCSKが、お客様のDX推進を強力にサポートします。

AWSクラウド
シェアする
タイトルとURLをコピーしました