AWS CDKのクロススタック構成でCloudFront OACのバケットポリシーが自動設定できない問題と回避策
こんにちは。サービス開発室の武田です。
AWS CDKでCloudFront + S3の構成を作っていて、スタックを分離しようとしたらハマりました。同一スタックなら自動設定してくれるバケットポリシーが、クロススタックだとできません。今回はこの問題と回避策についてまとめます。
やりたかったこと
やりたかったのは次のような構成です。
- S3バケット: 静的コンテンツを保存
- CloudFront Distribution: S3のコンテンツを配信
- Origin Access Control(OAC): CloudFrontからS3への安全なアクセス
- スタック分離: S3とCloudFrontを別々のスタックで管理
スタックを分離する理由はいろいろ考えられますが、ひとまず今回は分けました、というお話です。
なお、このクロススタックの問題は同一リージョンでも異なるリージョンの場合も発生します。リージョンの違いによるものではなく、スタックの分離そのものが原因です。
同一スタックなら問題ない
まず、CloudFormationの制約をおさらいしておきます。
リージョナルリソースの制約:
- CloudFormation stackは、デプロイするリージョン内のリージョナルリソース(EC2、RDS、VPCなど)しか作成できない
- つまり、同一スタック内のリージョナルリソース = 必ず同一リージョン
- 異なるリージョンのリージョナルリソース = 必然的に異なるスタック(クロススタック)
グローバルサービスの例外:
- グローバルサービス(CloudFront、IAM、Route 53など)は、どのリージョンのスタックからでも作成可能
- ただし、CloudFront用のACM証明書やLambda@Edgeはus-east-1に配置する必要があるため、これらを使う場合はマルチリージョン構成が必要
同一スタック内でS3とCloudFrontを作成する場合、こう書けば動きます。
// ✅ 同一スタック内なら自動設定が機能する
const bucket = new s3.Bucket(this, 'MyBucket', {
  // ... 設定
});
const distribution = new cloudfront.Distribution(this, 'MyDistribution', {
  defaultBehavior: {
    origin: origins.S3BucketOrigin.withOriginAccessControl(bucket),
    // ... その他の設定
  },
});
この場合、CDKが自動的に次の処理をしてくれます。
- Origin Access Control(OAC)の作成
- S3バケットポリシーに次のようなステートメントを追加
{
  "Effect": "Allow",
  "Principal": {
    "Service": "cloudfront.amazonaws.com"
  },
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::my-bucket/*",
  "Condition": {
    "StringEquals": {
      "AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/EDFDVBD632BHDS5"
    }
  }
}
同一スタックなら自動設定が機能しますが、クロススタックだと機能しません。
問題:インポートされたバケットは読み取り専用
クロススタック構成では、CloudFrontStackからStorageStackのバケットを参照する必要があります。
// CloudFrontStack
const bucket = s3.Bucket.fromBucketAttributes(this, 'ImportedBucket', {
  bucketName: props.bucketName,
  region: props.bucketRegion, // ← 明示的なリージョン指定が必要
});
const distribution = new cloudfront.Distribution(this, 'MyDistribution', {
  defaultBehavior: {
    origin: origins.S3BucketOrigin.withOriginAccessControl(bucket),
    // ... その他の設定
  },
});
この場合、次の警告が発生します。
[WARNING] @aws-cdk/aws-cloudfront-origins:updateImportedBucketPolicyOac
なぜこの警告が出るのか
AWS CDKには重要な制約があります。Bucket.fromBucketAttributes()で作成したインポートされたバケットは、CDKから変更できません。
公式ドキュメントにもこう書いてあります。
When using an imported bucket for your S3 Origin with OAC, you need to update the S3 bucket policy manually to allow the OAC to access the S3 origin, as CDK apps cannot modify the configuration of imported resources.
整理すると次のようになります。
- withOriginAccessControl()は、自分が管理しているバケットにしかポリシーを追加できない
- 別スタックで作成されたバケットは「外部リソース」扱いで、CDKから変更不可
- CloudFrontStackからStorageStackのバケットポリシーを更新しようとしても、スタック間の依存関係が循環参照になる
- CloudFrontStack → StorageStack(バケット名を参照)
- StorageStack → CloudFrontStack(Distribution ARNを参照)
 
既知の問題
実はこの問題は、AWS CDKのGitHubに既知の課題として報告されています(Issue #31462)。
回避策
制約がわかったので、回避策を実装していきます。
1. StorageStackで事前にバケットポリシーを設定
まず、StorageStackでバケットポリシーを事前に設定しておきます。
// StorageStack
export class StorageStack extends cdk.Stack {
  public readonly bucket: s3.IBucket;
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    this.bucket = new s3.Bucket(this, 'MyBucket', {
      versioned: true,
      encryption: s3.BucketEncryption.S3_MANAGED,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      // ... その他の設定
    });
    // CloudFront OACからのアクセスを手動で事前に許可
    this.bucket.addToResourcePolicy(
      new cdk.aws_iam.PolicyStatement({
        sid: 'AllowCloudFrontServicePrincipal',
        effect: cdk.aws_iam.Effect.ALLOW,
        principals: [new cdk.aws_iam.ServicePrincipal('cloudfront.amazonaws.com')],
        actions: ['s3:GetObject'],
        resources: [this.bucket.arnForObjects('*')],
        conditions: {
          StringEquals: {
            'AWS:SourceAccount': cdk.Stack.of(this).account,
          },
        },
      })
    );
  }
}
2. CloudFrontStackでバケットをインポートし、警告を抑制
次に、CloudFrontStackでバケットをインポートします。
// CloudFrontStack
export class CloudFrontStack extends cdk.Stack {
  public readonly distribution: cloudfront.Distribution;
  constructor(scope: Construct, id: string, props: CloudFrontStackProps) {
    super(scope, id, props);
    // S3バケットをリージョン情報付きで参照
    const bucket = s3.Bucket.fromBucketAttributes(this, 'ImportedBucket', {
      bucketName: props.bucketName,
      region: props.bucketRegion, // ← これがないとPermanentRedirectエラー
    });
    // Origin Access Control(OAC)の作成
    const oac = new cloudfront.S3OriginAccessControl(this, 'OAC', {
      signing: cloudfront.Signing.SIGV4_NO_OVERRIDE,
    });
    // CloudFront Distributionの作成
    this.distribution = new cloudfront.Distribution(this, 'ReportDistribution', {
      defaultBehavior: {
        origin: origins.S3BucketOrigin.withOriginAccessControl(bucket, {
          originAccessControl: oac,
        }),
        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        // ... その他の設定
      },
    });
    // 警告を抑制(バケットポリシーはStorageStackで手動設定済み)
    cdk.Annotations.of(this.distribution).acknowledgeWarning(
      '@aws-cdk/aws-cloudfront-origins:updateImportedBucketPolicyOac'
    );
  }
}
3. スタックのデプロイ順序
最後に、デプロイ順序を設定します。
// bin/app.ts
const storageStack = new StorageStack(app, 'MyApp-Storage', {
  env: { region: 'ap-northeast-1' },
  crossRegionReferences: true,
});
const cloudFrontStack = new CloudFrontStack(app, 'MyApp-CloudFront', {
  env: { region: 'us-east-1' },
  crossRegionReferences: true,
  bucketName: storageStack.bucket.bucketName,
  bucketRegion: 'ap-northeast-1',
});
デプロイ順序はStorageStack → CloudFrontStackです。
この実装の注意点
セキュリティスコープの考慮事項
本当は、特定のCloudFront DistributionのARNだけを許可したいところです。
{
  "Condition": {
    "StringEquals": {
      "AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/EDFDVBD632BHDS5"
    }
  }
}
しかし、StorageStackの時点ではまだDistributionが作られていないため、アカウント全体のCloudFrontからのアクセス を許可するしかありません。
{
  "Condition": {
    "StringEquals": {
      "AWS:SourceAccount": "123456789012"
    }
  }
}
これだと、同じAWSアカウント内のすべてのCloudFront Distributionからこのバケットへのアクセスを許可する設定になります。
CDKの自動設定が使えない
withOriginAccessControl()の自動バケットポリシー設定が使えず、手動で設定する必要があります。
警告を手動で抑制する必要がある
cdk.Annotations.of(this.distribution).acknowledgeWarning()で警告を明示的に抑制しないと、CDKデプロイ時に警告が表示され続けます。
まとめ
AWS CDKでCloudFront OACをクロススタック構成で使おうとすると、インポートされたバケットが読み取り専用なため、自動バケットポリシー設定が効きません。循環参照の問題もあります。
回避策としてはこうしました。
- StorageStackで事前にServicePrincipalベースのバケットポリシーを設定
- CloudFrontStackでインポートしたバケットを使用し、警告を抑制
- ただし、アカウントレベルの許可になるため、セキュリティはやや緩くなる
推奨は同じスタックにまとめることです。やんごとなき理由などで、クロススタック構成でCloudFront OACを使う際の参考になれば幸いです。








