0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CDK デプロイでログが止まる — S3 バケットポリシー全置換の罠

0
Posted at

はじめに

AI にコードを書いてもらえるようになり、IaC の導入ハードルは以前より大きく下がりました。

一方で、AWS サービスが裏側でどのようにリソースを構成・更新しているのかを理解していないと、生成されたコードによって既存の設定を意図せず壊してしまうことがあります。

実際に私も、cdk diff で差分がないことを確認したうえでデプロイしたにもかかわらず、翌朝になって S3 にログが届いていないことに気づきました。

調査した結果、原因は CDK が管理する S3 バケットポリシーをデプロイ時に「全置換」する挙動でした。

AWS サービスが API 経由でバケットポリシーに自動追加していたステートメントが、CDK デプロイによって静かに削除されていたのです。

Network Firewall や VPC Flow Logs、WAF など、S3 へのログ配信にあたってバケットポリシーへ権限を自動追加する AWS サービスでは、同じ問題が起こる可能性があります。

つまり、これは「ロギング設定ミス」というより、AWS サービスと CDK のリソース管理方法がぶつかることで発生する問題です。

この記事では、実際に発生した事象をもとに、以下を整理していきます。

  • なぜ AWS サービスが追加したポリシーが消えてしまうのか
  • なぜ cdk diff では問題に気づきにくいのか
  • どのようにしてこの問題を防ぐべきか

CDK (TypeScript) で S3 バケットを管理しており、addToResourcePolicy()grant*() でバケットポリシーを構成している環境が対象です。

何が起きたか

Network Firewall を例に、再現シナリオを示します(VPC Flow Logs や WAF でも同じ構造です)。

  1. CDK で S3 バケットをデプロイ(バケットポリシーにはアプリ用のステートメントのみ)
  2. Network Firewall のロギング設定を有効化 → AWS が自動で delivery.logs.amazonaws.com の書き込み許可をバケットポリシーに追加
  3. ログが正常に配信される(この時点ではバケットポリシーに CDK テンプレートにないステートメントが存在)
  4. 別の要件で bucket.grantRead(someLambda) を 1 行追加して cdk deploy
  5. CloudFormation が BucketPolicy を更新 → テンプレートの内容でバケットポリシー全体を置換
  6. Network Firewall 用のステートメントが消失 → ログ配信が停止

怖いのは「バケットポリシーに無関係な変更を加えないデプロイでは発生しない」ことで、何度もデプロイを繰り返して安心していたのに、ある日突然ログが止まるわけです。(ログが止まったことは、通知などの設定を入れておかないと気づきようがない。)

原因: CloudFormation の BucketPolicy は全置換

AWS サービスによる自動付与

AWS 公式ドキュメントにはこう書かれています。

If the user creating the log owns the bucket, the service automatically attaches the following policy to the bucket to give the log permission to publish logs to it.

つまり Network Firewall のロギングを有効化すると、AWS が PutBucketPolicy API を呼んで既存のバケットポリシーにステートメントをマージします。
コンソールからの設定だけでログが届き始めるのはこの仕組みのおかげです。

CloudFormation は「テンプレートが正」

一方、CloudFormation の AWS::S3::BucketPolicy宣言的リソースです。
デプロイ時に CloudFormation が行うのは「テンプレートに書かれたポリシードキュメントで PutBucketPolicy を呼ぶ」こと。
テンプレートに書かれていないステートメントは存在しないものとして扱われます。

これは CloudFormation のバグではなく正常動作です。
CloudFormation は「テンプレートこそが真実」という設計思想で動いています。

なぜ cdk diff で気づけないか

cdk diffCloudFormation に記録されているデプロイ済みスタックのテンプレートと、今回 cdk synth したテンプレートの比較です。
実リソースの現在の状態(ドリフト)は見ていません。

今回の例で言うと、Network Firewall が追加したステートメントはテンプレートに一度も書かれたことがないため、cdk diff の比較対象に登場しません。
開発者から見ると「差分なし = 安全」に見えますが、実際にはドリフトが存在している状態です。

さらに厄介なのが、AWS::S3::BucketPolicy は CloudFormation のドリフト検出に非対応であることです。

確認手段 BucketPolicy で使えるか 検出できること
cdk diff テンプレート間の差分(新規追加・変更・削除)
CloudFormation ドリフト検出 非対応
aws s3api get-bucket-policy 実リソースの現在の状態

CloudFormation ドキュメントの対応リソース一覧AWS::S3::BucketPolicy を確認すると、Drift detection は No になっています。つまり「ドリフト検出という逃げ道すら無い」状態です。

トリガー条件の整理

全置換が発生するのは、CloudFormation が BucketPolicy リソースを更新する時だけです。

  • BucketPolicy に変更がないデプロイ → CloudFormation は BucketPolicy を触らない → 自動付与されたステートメントは残る
  • BucketPolicy に 1 ステートメントでも追加/変更/削除 → CloudFormation がテンプレート全体で PutBucketPolicy → 自動付与分が消える

BucketPolicy が更新されるトリガーは addToResourcePolicy() の直接呼び出しだけではありません。

  • bucket.grantRead(lambda) / bucket.grantWrite(role) — 内部で addToResourcePolicy が呼ばれる
  • 既存バケットの props に後から enforceSSL: true を追加 — aws:SecureTransport の Deny ステートメントが追加される。セキュリティ強化目的の 1 行が引き金になる
  • S3BucketOrigin.withOriginAccessControl() — OAC 用ステートメントが追加される

つまり「今まで何回もデプロイしてるけど問題なかった」は安全の証拠になりません。
バケットポリシーに変更を加えた瞬間に初めて顕在化します。

対策: 全ステートメントを CDK で明示する

結論はシンプルです。CDK で管理するバケットには、すべてのステートメントを CDK で書く。AWS サービスの自動付与に依存しない。

lib/logs-bucket-stack.ts
import { Aws, Stack, StackProps } from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';

export class LogsBucketStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const logsBucket = new s3.Bucket(this, 'LogsArchive', {
      bucketName: `logs-archive-${Aws.ACCOUNT_ID}`,
      encryption: s3.BucketEncryption.S3_MANAGED,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
    });

    // Network Firewall ログ配信許可(自動付与に依存しない明示的定義)
    logsBucket.addToResourcePolicy(
      new iam.PolicyStatement({
        sid: 'AWSLogDeliveryWrite',
        effect: iam.Effect.ALLOW,
        principals: [new iam.ServicePrincipal('delivery.logs.amazonaws.com')],
        actions: ['s3:PutObject'],
        resources: [
          `${logsBucket.bucketArn}/nfw-flowlogs/AWSLogs/${Aws.ACCOUNT_ID}/*`,
        ],
        conditions: {
          StringEquals: {
            's3:x-amz-acl': 'bucket-owner-full-control',
            'aws:SourceAccount': Aws.ACCOUNT_ID,
          },
          ArnLike: {
            'aws:SourceArn': `arn:aws:logs:${Aws.REGION}:${Aws.ACCOUNT_ID}:*`,
          },
        },
      })
    );

    logsBucket.addToResourcePolicy(
      new iam.PolicyStatement({
        sid: 'AWSLogDeliveryAclCheck',
        effect: iam.Effect.ALLOW,
        principals: [new iam.ServicePrincipal('delivery.logs.amazonaws.com')],
        actions: ['s3:GetBucketAcl'],
        resources: [logsBucket.bucketArn],
        conditions: {
          StringEquals: {
            'aws:SourceAccount': Aws.ACCOUNT_ID,
          },
          ArnLike: {
            'aws:SourceArn': `arn:aws:logs:${Aws.REGION}:${Aws.ACCOUNT_ID}:*`,
          },
        },
      })
    );
  }
}

CDK で書くステートメントが、AWS が自動付与したものと機能的に等価であることが前提です。
特に resources の prefix パス(nfw-flowlogs/AWSLogs/...)はロギング設定時に指定した prefix と一致させる必要があります。
ズレていると CDK デプロイ後にサイレントにログ配信が止まります。

自動付与する AWS サービス一覧

同様の注意が必要なサービスをまとめます。「自動付与の条件」はサービスごとに異なるため、各公式ドキュメントで確認してください。

サービス Principal 自動付与の条件 ドキュメント
Network Firewall delivery.logs.amazonaws.com バケット所有者がロギング設定 + s3:PutBucketPolicy 権限あり Developer Guide
VPC Flow Logs delivery.logs.amazonaws.com 同上 User Guide
WAF delivery.logs.amazonaws.com 同上(バケット名は aws-waf-logs- プレフィックス必須) Developer Guide
CloudTrail cloudtrail.amazonaws.com コンソールで新規バケット作成時は自動付与。既存バケット指定時は手動 User Guide
Config config.amazonaws.com コンソールで新規バケット作成時は自動付与。既存バケット指定時は手動 Developer Guide

裏を返すと、クロスアカウントの集約ログバケット(ログアカウント所有)に対してワークロードアカウントからロギングを設定する場合は、ワークロードアカウントはバケット所有者ではないため自動付与が行われません。
この場合は最初からバケット側で手動設定する運用になるため、この記事の問題は起きにくいです。

ALB / NLB のアクセスログはこの問題の対象外です。 ELB はサービス側がバケットポリシーを自動付与しません。CDK の logAccessLogs() が内部で addToResourcePolicy() を呼んでテンプレートにステートメントを追加するため、最初から「全部 CDK で書く」が実現されています。
これはこの記事が推奨するパターンの模範例です。

GuardDuty もこの問題の対象外です。
GuardDuty は S3 エクスポート設定時にバケットポリシーを自動付与せず、手動でポリシーを付与する手順になっています。CDK で書いていれば消える心配はありません。

事後確認手段

すでに運用中の環境で「テンプレート外のステートメントが存在しないか」を確認する方法をいくつかご紹介します。

1. aws s3api get-bucket-policy による直接確認

最も確実な方法です。実リソースのバケットポリシーと cdk synth で出力されるテンプレートを比較します。

# 実リソースのバケットポリシーを取得
aws s3api get-bucket-policy \
  --bucket logs-archive-123456789012 \
  --query Policy --output text | jq .

テンプレートに書かれていないステートメントがあれば、それは次に BucketPolicy が更新された時点で消えます。

2. AWS Config リソースタイムライン

対象バケットのリソースタイムラインで BucketPolicy の変更履歴を確認できます。
Before / After の diff でステートメントの増減がわかります。

3. CloudTrail

PutBucketPolicy イベントの requestParameters.bucketPolicy に設定後のポリシー全文が記録されています。
CloudFormation による更新と AWS サービスによる自動付与の両方が記録されるため、時系列で追えます。

まとめ

教訓を 3 点にまとめます。

  1. CDK 管理下のバケットポリシーは宣言的。テンプレートにないものは消える
    AWS サービスの「自動付与」は便利ですが、IaC で管理するリソースでは明示的に書くのが安全です
  2. cdk diff で差分なし ≠ 安全
    テンプレート比較とドリフト検出は別物。そして AWS::S3::BucketPolicy はドリフト検出にすら非対応です。
    バケットポリシーを変更する PR では aws s3api get-bucket-policy で実リソースを確認してからマージしましょう
  3. 「今まで問題なかった」は安全の証拠にならない
    BucketPolicy に変更を加えないデプロイでは顕在化しません。
    grantRead() を 1 行足しただけで、ある日突然ログが止まります

「AWS がよしなにやってくれる」と「IaC で宣言的に管理する」は相性が悪い場面がある。その境界を意識しておくと、同じ種類の事故を未然に防げます。

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?