CloudFront + S3 は定番構成ですが、S3(ap-northeast-1) と CloudFront 関連スタック(us-east-1) を分けると、CDK のデフォルト挙動だけでは詰まりやすいポイントがありました。
CloudFront 自体はグローバルサービスですが、独自ドメインで ACM 証明書を使う場合、CloudFrontにWAFをアタッチする場合は、証明書及びWAFは us-east-1 に置く必要があります。そこで「S3 は東京リージョン、CloudFront 関連は us-east-1 のスタックに分離する」という構成は普通に起こります。
この記事では、AWS 公式ドキュメントと AWS CDK 公式ドキュメントを根拠に、実際にハマった点と対処を整理します。
この記事の前提
- AWS CDK v2 / TypeScript
- CloudFront のオリジンは S3 static website endpoint ではなく、通常の S3 bucket origin
- CloudFront から S3 へのアクセスは OAC を使う
前提として、CDK 公式では S3Origin は deprecated で、通常の S3 オリジンには S3BucketOrigin と withOriginAccessControl() が案内されています。また、OAC は static website endpoint ではなく通常の S3 bucket origin で使います。
先に結論
- S3 バケットを CloudFront スタックへ持ち込むときは
fromBucketAttributes({ region: 'ap-northeast-1' })を使う - imported bucket では OAC 用のバケットポリシーは自動付与に頼らず、明示的に追加する
- 存在しないオブジェクトを 404 にしたいなら
s3:ListBucketも許可する - S3 origin へ viewer の
Hostヘッダーを転送しない
1. fromBucketArn() だけだと S3 のリージョン解決が CloudFront スタック側に寄る
CloudFront の S3 origin では、AWS 公式は bucket-name.s3.region.amazonaws.com 形式のリージョナルドメイン名を推奨しています。
一方で CDK 公式には、imported bucket の region は「現在のスタックのリージョンが既定値」であり、リージョン依存のプロパティを正しく扱いたいなら明示指定するよう書かれています。
そのため、CloudFront スタックが us-east-1 で、実際の S3 バケットが ap-northeast-1 にあるのに fromBucketArn() や fromBucketName() だけで取り込むと、bucketRegionalDomainName のリージョン部分がus-east-1になり、オリジンへの接続ができなくなります。
NG
const siteBucket = s3.Bucket.fromBucketArn(
this,
'SiteBucket',
'arn:aws:s3:::example-bucket',
);
OK
const siteBucket = s3.Bucket.fromBucketAttributes(this, 'SiteBucket', {
bucketArn: 'arn:aws:s3:::example-bucket',
bucketName: 'example-bucket',
region: 'ap-northeast-1',
});
この形にしておくと、デプロイ時にS3オリジンのオリジンドメイン名がbucket-name.s3.ap-northeast-1.amazonaws.comになり、正しくオリジンに接続できます。
2. imported bucket では OAC 用バケットポリシーが自動で付かない
S3BucketOrigin.withOriginAccessControl() は便利ですが、imported bucket では自動付与に頼れません。
CDK 公式には、imported S3 bucket で OAC を使う場合は「S3 bucket policy を手動で更新する必要がある」と明記されています。さらに S3 construct library 側でも、existing/imported bucket に対する addToResourcePolicy() は何もしないと説明されています。
つまり、CloudFront スタック側で fromBucketAttributes() した bucket に対しては、OAC 用ポリシーが勝手に入る前提で組まない方が安全です。バケットを所有している側のスタックで、CloudFront distribution を条件にしたポリシーを明示的に追加します。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontReadOnly",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::example-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::<account-id>:distribution/<distribution-id>"
}
}
}
]
}
CDK で書くならこうです。
bucket.addToResourcePolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
principals: [new iam.ServicePrincipal('cloudfront.amazonaws.com')],
actions: ['s3:GetObject'],
resources: [bucket.arnForObjects('*')],
conditions: {
StringEquals: {
'AWS:SourceArn':
'arn:aws:cloudfront::<account-id>:distribution/<distribution-id>',
},
},
}),
);
参考
- CloudFront 公式の OAC 用 bucket policy 例:
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html - CDK 公式: imported S3 bucket で OAC を使う場合は policy を手動更新:
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudfront_origins-readme.html - CDK 公式: imported/existing bucket への
addToResourcePolicy()は何もしない:
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3-readme.html#permissions
3. s3:ListBucket がないと 404 ではなく 403 になる
(こちらについてはスタック分割は関係ないです。)
これは S3 の公式仕様です。
S3 は、存在しないオブジェクトを GetObject したとき、呼び出し元に s3:ListBucket があれば 404 Not Found を返し、なければ 403 Access Denied を返します。
そのため、「CloudFront 経由で存在しないファイルにアクセスしたときは 404 にしたい」という場合は、s3:GetObject だけでなく s3:ListBucket も必要です。
CDK では originAccessLevels に LIST を加える考え方になります。
const origin = origins.S3BucketOrigin.withOriginAccessControl(siteBucket, {
originAccessLevels: [
cloudfront.AccessLevel.READ,
cloudfront.AccessLevel.LIST,
],
});
ただし imported bucket の場合は、これも自動ポリシー追加に頼れないので、バケット側で s3:ListBucket を明示的に許可します。
bucket.addToResourcePolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
principals: [new iam.ServicePrincipal('cloudfront.amazonaws.com')],
actions: ['s3:ListBucket'],
resources: [bucket.bucketArn],
conditions: {
StringEquals: {
'AWS:SourceArn':
'arn:aws:cloudfront::<account-id>:distribution/<distribution-id>',
},
},
}),
);
s3:ListBucket はオブジェクト ARN ではなく、バケット ARN に付ける点も注意です。
参考
- S3 公式:
ListBucketがあると未存在オブジェクトは 404、ないと 403:
https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html
4. S3 origin へ viewer の Host ヘッダーを転送しない
(こちらについてもスタック分割は関係ないです。)
S3 origin では Host ヘッダーの扱いも要注意です。
CloudFront 公式には、origin request には Host ヘッダーが自動的に含まれるとあります。また、managed origin request policy の AllViewerExceptHostHeader では viewer の Host を除外したうえで、CloudFront が origin domain name の Host を追加します。
自分の環境でも、Host を転送対象に含めた結果、S3 オリジンドメイン向けの Host に置き換わらずアクセスできなくなりました。
S3 origin では、必要なヘッダーだけを明示的に転送するのが安全です。(やたらめったら転送する人はいないとは思いますが...)CORS 用なら managed origin request policy の CORS_S3_ORIGIN で十分なことが多いです。
new cloudfront.Distribution(this, 'Distribution', {
defaultRootObject: 'index.html',
defaultBehavior: {
origin,
originRequestPolicy: cloudfront.OriginRequestPolicy.CORS_S3_ORIGIN,
},
});
根拠
- CloudFront 公式: origin request には
Hostが自動的に含まれる:
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/controlling-origin-requests.html - CloudFront 公式:
AllViewerExceptHostHeaderは viewer のHostを除外し、CloudFront が origin 用のHostを追加する:
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html
まとめ
いかがだったでしょうか?
今回挙げた4点は私が過去担当したプロジェクトで鮮やかに引っかかった問題です。
特にs3:ListBucketについては404エラーが発生する状況にならないと気づけないため、運が悪ければリリースまで気づかずに...なんてこともありそうだなと怖くなりました。