本日のお題
AWS CDK(以下「CDK」と記載)において、既存のS3バケットを別バケットの「サーバーアクセスログ」の送信先に設定すると、CloudFormation(以下「CFn」と記載)にデプロイした際にエラーになる
詳細
(Webホスティングを行っているS3バケットなど)Public Readに設定しているS3バケットにおいて、サーバーアクセスログを有効にしてアクセス監視・解析をするということは、運用上よくあると思います。(定義については「参考情報:AWS CDKでアクセスログを有効にする方法」を参照)
ところが、サーバーアクセスログの送信先に既存のバケットを指定した場合、これをデプロイすると、CloudFormationでなぜか以下のエラーが発生する場合があります。
Resource handler returned message: "The bucket does not allow ACLs (Service: S3, Status Code: 400, Request ID:..(中略)..., HandlerErrorCode: InvalidRequest)
本日はこの問題についてのお話です。
いきなり結論
先に結論から言ってしまうと、「サーバーアクセスログを有効にした場合、CDKが『ACLを有効にするCFnテンプレート』を出力する」ことがエラーの原因です。
サーバーアクセスログを有効にした場合、CDKは送信先バケットのプロパティとして、下記CFnテンプレートを出力します。
ただ、このテンプレートは、AccessControl
や ObjectOwnership
(=オブジェクト所有者) が ObjectWriter
だったりすることからも分かる通り「ACL(Access Control List)が有効」の時のテンプレートになっています。
{ "AccessControl": "LogDeliveryWrite", "BucketName": "fortune-tmm-auth0-logo-bucket-dev-accesslog", "OwnershipControls": { "Rules": [ { "ObjectOwnership": "ObjectWriter" } ] }, }
しかしS3のデフォルトは「ACL無効」なので*1、既存バケット(=ACLが無効)に対してこの設定を適用しようとすると、「ACL無効のバケットにACL有効時のプロパティを設定しようとしている」となり、エラーになってしまいます。
なお、これはあくまで「既存バケット」の場合であり、新規作成するバケットならエラーは発生しないかもしれませんが、現在はS3バケットのアクセス制御はバケットポリシーを使用することが推奨されているため、「ACL有効」のプロパティを出力するのはあまりよろしくないです。
プロパティを(強引に)書き換える
上記の現象は「ACL有効の設定を強制的に書き換える」ことで対応できます。
具体的には下記の対応をします。
AccessControl
を削除するObjectOwnership
をBucketOwnerEnforced
(=バケット所有者の強制)にする、またはOwnershipControls
を削除する*2
ただしL2 Constructでは上記を実行できないので、node.defaultChild
を使用してL1 Constructに変換した後で実行します。
具体的には、先述のソースの末尾に下記ソースを追加します。
const cfnLogBucket = logBucket.node.defaultChild as s3.CfnBucket; // ObjectOwnershipの上書き。 // もちろんaddPropertyDeletionOverride('OwnershipControls') でもOK cfnLogBucket.addPropertyOverride('OwnershipControls.Rules.0.ObjectOwnership', 'BucketOwnerEnforced'); // AccessControlの削除 cfnLogBucket.addPropertyDeletionOverride('AccessControl');
なお、addProperty
系メソッドでプロパティに配列のキーを指定する方法は、以下のCDK公式ドキュメントを参照してください。
addOverride(path, value)
参考情報:AWS CDKでアクセスログを有効にする方法
AWS CDKでサーバーアクセスログを有効にする場合、下記コードを記載します。
import * as cdk from 'aws-cdk-lib'; import { aws_iam as iam, aws_s3 as s3 } from 'aws-cdk-lib'; // アクセスログを有効にするバケットのバケット名 const departureBucketName = `departure`; // アクセスログの送信先Bucket const logBucket = new s3.Bucket(this, `LogBucket`, { bucketName: 'destinationLogBucket' , removalPolicy: cdk.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE, }); // アクセスログの送信先Bucketのバケットポリシー logBucket.addToResourcePolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, principals: [new iam.ServicePrincipal('logging.s3.amazonaws.com')], actions: ['s3:PutObject'], resources: [`arn:aws:s3:::destinationLogBucket/*`], conditions: { ArnLike: { 'aws:SourceArn': `arn:aws:s3:::${departureBucketName}`, }, StringEquals: { 'aws:SourceAccount': <アカウント番号>, }, }, }), ); // アクセスログを有効にするBucket const departureBucket = new s3.Bucket(this, `DepartureBucket`, { // blockPublicAccessは無くてもいいかも blockPublicAccess: s3.BlockPublicAccess.BLOCK_ACLS, bucketName: departureBucketName, publicReadAccess: true, removalPolicy: cdk.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE, serverAccessLogsBucket: logBucket, serverAccessLogsPrefix: 'logs/', }); // アクセスログを有効にするBucketのバケットポリシー // iam.StarPrincipal()は「Principal: '*'」という定義を作成するメソッド departureBucket .addToResourcePolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, principals: [new iam.StarPrincipal()], actions: ['s3:GetObject'], resources: [`arn:aws:s3:::${departureBucketName}/*`], }), );
なお、サーバーアクセスログで送信先バケットに必要なバケットポリシーについては、下記のAWS公式ドキュメントを参考にしてください。
docs.aws.amazon.com
それでは、今回はこの辺で