こんにちは、広野です。
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 が主流になっていますし、ロギングもそうなるのかな、という想像です。
本記事が皆様のお役に立てれば幸いです。