echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【AWS CDK】S3サーバーアクセスログを有効にすると、「The bucket does not allow ACLs」エラーが発生する

本日のお題

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テンプレートを出力します。

ただ、このテンプレートは、AccessControlObjectOwnership(=オブジェクト所有者) が ObjectWriter だったりすることからも分かる通り「ACLAccess 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 を削除する
  • ObjectOwnershipBucketOwnerEnforced (=バケット所有者の強制)にする、または 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

それでは、今回はこの辺で

*1:2023年4月より、ACLはデフォルトで「無効」となります

*2:ObjectOwnershipのデフォルト値がBucketOwnerEnforcedなので、OwnershipControlsを削除することでもObjectOwnershipをBucketOwnerEnforcedに出来ます