【AWS CDK】S3バケットポリシーが正しいのにcdk deploy時にALB/WAFのログ設定で権限エラーが発生する場合の対処法

# TL;DR
- 問題の本質は**リソースの作成順序**にあります
- CDKの`addDependency()`メソッドを使用して、バケットポリシーが完全に作成された後にログ設定を行うように依存関係を追加することで解決できます
- この問題は、ALBのアクセスログとAWS WAFのログ設定の両方で発生する可能性があります

# はじめに
AWS CDKでインフラを構築する際、Application Load Balancer（ALB）のアクセスログや
AWS WAFのログをS3に保存する設定をよく行います。
その際、ログを書き込むためのS3バケットポリシーを適切に設定しているにもかかわらず、`cdk deploy`実行時に権限エラーが発生してスタック作成に失敗することがあります。

本記事では、この問題の原因と解決方法をご紹介します。

# エラー内容

## パターン①： ALBのアクセスログの書き込み権限エラー

### 発生条件
以下のような構成でスタックを作成/更新しようとした際に発生します：
- ALB
- S3バケット（ALBのアクセスログ保存用）
  - バケットポリシー（ログの書き込みを許可する権限）

### エラーメッセージ
`cdk deploy`実行時、以下のようなエラーが発生します：

```shell
$ cdk deploy

~~ 略 ~~

13:32:03 | UPDATE_FAILED | AWS::ElasticLoadBalancingV2::LoadBalancer | BackendAlb0D5FE5C5
Resource handler returned message: 
"Access Denied for bucket: hoge-alb-logsbucket9c4d8843-obvxovhjzaw3. 
Please check S3bucket permission 
(Service: ElasticLoadBalancingV2, Status Code: 400, 
Request ID: ba59f2fe-b021-41bd-8661-4d7a47f3e4e1)" 
(RequestToken: 38cfa46b-9a87-02e2-2623-0d31152fd8ef, HandlerErrorCode: InvalidRequest)
```

このエラーは、S3バケットへのログ書き込み権限が不足していることを示しています。

## パターン②： AWS WAFのログの書き込み権限エラー

### 発生条件
以下のような構成でスタックを作成/更新しようとした際に発生します：
- AWS WAF（ALBにアタッチするためのWeb ACL）
- S3バケット（Web ACLのログ保存用）
  - バケットポリシー（ログの書き込みを許可する権限）

### エラーメッセージ
`cdk deploy`実行時、以下のようなエラーが発生します：

```shell
$ cdk deploy

~~ 略 ~~

Failed resources:
Hoge-Stack | 17:55:13 | CREATE_FAILED | AWS::WAFv2::LoggingConfiguration | AlbWafLogConfig
Resource handler returned message: 
"Unable to deliver logs to the configured destination. 
You might need to grant log delivery permissions for the destination. 
If you're using S3 as your log destination, you might have exceeded your bucket limit. 
(Service: Wafv2, Status Code: 400, Request ID: cf35ddec-6352-43af-9d93-5e859e1d1db1)" 
(RequestToken: b3d18979-abfd-04ab-d91b-f6cc2fdb3c27, HandlerErrorCode: GeneralServiceException)
```

このエラーも、S3バケットへのログ書き込み権限が不足していることを示しています。

# 原因：リソース作成の順序依存によるエラー

公式ドキュメントの通りにバケットポリシーを設定しているにもかかわらず、なぜ`cdk deploy`が失敗するのでしょうか？

実は、この問題はリソース作成の順序に起因しています。
例えば、ALBのアクセスログ設定でエラーが発生した場合のデプロイ順序は以下のようになっていました。

```
04:32:00 S3 バケット作成
04:32:01 対象の ALB の作成開始
04:32:03 対象の ALB の作成失敗 (🚨本事象)
04:32:03 S3 バケットポリシー 作成開始
04:32:03 S3 バケットポリシー 作成中止
```

つまり、バケットポリシーが作成される前にALBの作成が開始されてしまい、S3へのログ書き込み権限がない状態でエラーが発生していたのです。

# 解決策：addDependency()を使用してリソース作成順序を制御

この問題は、CDKの`addDependency()`メソッドを使用することで解決できます。
（CloudFormationの場合は`DependsOn`で同様の制御が可能です）

参考：[AWS CDK Documentation - Dependencies](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib-readme.html#dependencies)

以下に、TypeScriptで`addDependency()`を使用した修正例を示します。
**コードを1行追加するだけで**、`cdk deploy`実行時の権限エラーは発生しなくなります。

## パターン①：ALBとS3バケットのコード修正例

```typescript
    // ログバケット
    const logsBucket = new s3.Bucket(this, "LogsBucket", {
      accessControl: s3.BucketAccessControl.PRIVATE,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      encryption: s3.BucketEncryption.S3_MANAGED,
      enforceSSL: true,
    });
    
    // ALB
    const backendAlb = new elbv2.ApplicationLoadBalancer(this, "BackendAlb", {
      vpc,
      internetFacing: true,
      dropInvalidHeaderFields: true,
      deletionProtection: true,
      securityGroup: backendAlbSecurityGroup,
      vpcSubnets: vpc.selectSubnets({
        subnetGroupName: "Public",
      }),
    });

    // ⭐️⭐️⭐️⭐️　コード追加箇所 ⭐️⭐️⭐️⭐️
    // ---------------------------------------------------------------
    // ALBのアクセスログ設定を行う前に、バケットポリシーが作成されるように依存関係を追加しました。
    // ---------------------------------------------------------------
    backendAlb.node.addDependency(logsBucket);

    // ALBアクセスログ用S3バケットの設定
    backendAlb.setAttribute("access_logs.s3.enabled", "true");
    backendAlb.setAttribute("access_logs.s3.bucket", logsBucket.bucketName);

    // ---------------------------------------------------------------
    // アクセスログ用のS3バケットに対するバケットポリシー設定
    // 参考：
    //   - AWS公式ドキュメント: 
    //     https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/enable-access-logging.html#attach-bucket-policy
    //   - BLEAのサンプルコード: 
    //     https://github.com/aws-samples/baseline-environment-on-aws/blob/2cbe058a33a8017de7bd22656c73042443eaa0d2/usecases/blea-guest-ecs-app-sample/lib/construct/ecsapp.ts#L90
    // ---------------------------------------------------------------
    logsBucket.addToResourcePolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        actions: ["s3:PutObject"],
        principals: [
          new iam.AccountPrincipal(
            ri.RegionInfo.get(Stack.of(this).region).elbv2Account,
          ),
        ],
        resources: [
          logsBucket.arnForObjects(`AWSLogs/${Stack.of(this).account}/*`),
        ],
      }),
    );
    logsBucket.addToResourcePolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        actions: ["s3:PutObject"],
        principals: [new iam.ServicePrincipal("delivery.logs.amazonaws.com")],
        resources: [
          logsBucket.arnForObjects(`AWSLogs/${Stack.of(this).account}/*`),
        ],
        conditions: {
          StringEquals: {
            "s3:x-amz-acl": "bucket-owner-full-control",
          },
        },
      }),
    );
    logsBucket.addToResourcePolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        actions: ["s3:GetBucketAcl"],
        principals: [new iam.ServicePrincipal("delivery.logs.amazonaws.com")],
        resources: [logsBucket.bucketArn],
      }),
    );
```

## パターン②：AWS WAFとS3バケットのコード修正例

```typescript
    // ALB用WAF WebACL
    const albWebAcl = new wafv2.CfnWebACL(this, "AlbWebACL", {
      defaultAction: { allow: {} },
      scope: "REGIONAL",
      visibilityConfig: {
        // ~~ 略 ~~
      },
      rules: [
        // ~~ 略 ~~
      ],
    });

    // WAF用ログバケット
    const albWafLogsBucket = new s3.Bucket(this, "AlbWafLogsBucket", {
      bucketName: `aws-waf-logs-${this.account}-${albWebAcl.node.id.toLowerCase()}`,
      versioned: false,
      lifecycleRules: [
        {
          id: "alb-waf-log-expiration",
          enabled: true,
          expiration: Duration.days(90),
        },
      ],
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      encryption: s3.BucketEncryption.S3_MANAGED,
      enforceSSL: true,
    });

    // WAFログバケット用のポリシー
    // 参考 https://repost.aws/ja/knowledge-center/waf-turn-on-logging
    albWafLogsBucket.addToResourcePolicy(
      new iam.PolicyStatement({
        sid: "AWSLogDeliveryAclCheck",
        effect: iam.Effect.ALLOW,
        principals: [new iam.ServicePrincipal("delivery.logs.amazonaws.com")],
        actions: ["s3:GetBucketAcl"],
        resources: [albWafLogsBucket.bucketArn],
        conditions: {
          StringEquals: {
            "aws:SourceAccount": [this.account]
          },
          ArnLike: {
            "aws:SourceArn": [`arn:aws:logs:${this.region}:${this.account}:*`]
          }
        }
      })
    );
    albWafLogsBucket.addToResourcePolicy(
      new iam.PolicyStatement({
        sid: "AWSLogDeliveryWrite",
        effect: iam.Effect.ALLOW,
        principals: [new iam.ServicePrincipal("delivery.logs.amazonaws.com")],
        actions: ["s3:PutObject"],
        resources: [albWafLogsBucket.arnForObjects("AWSLogs/*")],
        conditions: {
          StringEquals: {
            "s3:x-amz-acl": "bucket-owner-full-control",
            "aws:SourceAccount": [this.account]
          },
          ArnLike: {
            "aws:SourceArn": [`arn:aws:logs:${this.region}:${this.account}:*`]
          }
        }
      })
    );

    // WAFログ設定
    const wafLogConfig = new wafv2.CfnLoggingConfiguration(this, "AlbWafLogConfig", {
      logDestinationConfigs: [albWafLogsBucket.bucketArn],
      resourceArn: albWebAcl.attrArn,
      loggingFilter: {
        DefaultBehavior: "DROP",
        Filters: [
          // ~~ 略 ~~
        ],
      },
    });

    // ⭐️⭐️⭐️⭐️　コード追加箇所 ⭐️⭐️⭐️⭐️
    // ---------------------------------------------------------------
    // WAFログ設定を行う前に、バケットポリシーが作成されるように依存関係を追加しました。
    // ---------------------------------------------------------------
    wafLogConfig.node.addDependency(albWafLogsBucket);
```

# まとめ
- ALBやAWS WAFのログ設定時に発生する権限エラーは、リソースの作成順序に起因することがあります
- CDKの`addDependency()`メソッドを使用して、バケットポリシーが完全に作成された後にログ設定を行うように依存関係を追加することで解決できます
- この対応は、ALBのアクセスログとAWS WAFのログ設定の両方に有効です