💡

【AWSCDK】CloudFront+S3でOACを使いたい

2023/05/29に公開

https://zenn.dev/thyt_lab/articles/385f00cc1ea503

はじめに

久しぶりにAWSCDKを使って、静的アセットのデプロイを行っていたところ、OAI(Origin Access Identify)がLegacy扱いとなっていたため、OAC(Origin Access Control)に変更したときの備忘録です。

https://aws.amazon.com/jp/blogs/news/amazon-cloudfront-introduces-origin-access-control-oac/

OAIを使った時のCDK

// s3
const s3 = new cdk.aws_s3.Bucket(this, 'bucket-id', {
    websiteIndexDocument: 'index.html',
    publicReadAccess: true
})

// OAI
const originAccessIdentity = new cdk.aws_cloudfront.OriginAccessIdentity(this, 'OriginAccessIdentity')

// cloudfront(distribution)
const distribution = new cdk.aws_cloudfront.Distribution(this, 'distribution-id', {
    defaultRootObject: 'index.html',
    defaultBehavior: {
        origin: new cdk.aws_cloudfront_origins.S3Origin(s3, { originAccessIdentity })
    },
})

const bucketPolicyStatement = new cdk.aws_iam.PolicyStatement({
    actions: ['s3:GetObject'],
    effect: cdk.aws_iam.Effect.ALLOW,
    principals: [
        new cdk.aws_iam.CanonicalUserPrincipal(originAccessIdentity.cloudFrontOriginAccessIdentityS3CanonicalUserId)
    ],
    resources: [`${s3.bucketArn}/*`]
})

s3.addToResourcePolicy(bucketPolicyStatement)

new cdk.aws_s3_deployment.BucketDeployment(this, 'CDKReactDeployPractice', {
    sources: [cdk.aws_s3_deployment.Source.asset('<appPath>')],
    destinationBucket: s3,
    distribution,
    distributionPaths: ['/*']
})

OACに変更する

// s3
const s3 = new cdk.aws_s3.Bucket(this, 'bucket-id', {
    websiteIndexDocument: 'index.html',
    blockPublicAccess: cdk.aws_s3.BlockPublicAccess.BLOCK_ALL
})

// OAC
const cfnOriginAccessControl = new cdk.aws_cloudfront.CfnOriginAccessControl(this, 'OriginAccessControl', {
    originAccessControlConfig: {
        name: 'OriginAccessControlForContentsBucket',
        originAccessControlOriginType: 's3',
        signingBehavior: 'always',
        signingProtocol: 'sigv4',
        description: 'Access Control',
    },
});

// cloudfront(distribution)
const distribution = new cdk.aws_cloudfront.Distribution(this, 'distribution-id', {
    defaultRootObject: 'index.html',
    defaultBehavior: {
        origin: new cdk.aws_cloudfront_origins.S3Origin(s3)
    },
})

const bucketPolicyStatement = new cdk.aws_iam.PolicyStatement({
    actions: ['s3:GetObject'],
    effect: cdk.aws_iam.Effect.ALLOW,
    principals: [
        new cdk.aws_iam.ServicePrincipal('cloudfront.amazonaws.com')
    ],
    resources: [`${s3.bucketArn}/*`]
})
bucketPolicyStatement.addCondition('StringEquals', {
    'AWS:SourceArn': `arn:aws:cloudfront::${cdk.Stack.of(this).account}:distribution/${distribution.distributionId}`
})

s3.addToResourcePolicy(bucketPolicyStatement)

const cfnDistribution = distribution.node.defaultChild as cdk.aws_cloudfront.CfnDistribution
cfnDistribution.addPropertyOverride('DistributionConfig.Origins.0.OriginAccessControlId', cfnOriginAccessControl.getAtt('Id'))
cfnDistribution.addPropertyOverride('DistributionConfig.Origins.0.DomainName', s3.bucketRegionalDomainName)
cfnDistribution.addOverride('Properties.DistributionConfig.Origins.0.S3OriginConfig.OriginAccessIdentity', "")
cfnDistribution.addPropertyDeletionOverride('DistributionConfig.Origins.0.CustomOriginConfig')

new cdk.aws_s3_deployment.BucketDeployment(this, 'CDKReactDeployPractice', {
    sources: [cdk.aws_s3_deployment.Source.asset('<appPath>')],
    destinationBucket: s3,
    distribution,
    distributionPaths: ['/*']
})

OACの作成

L2 Constructではまだ用意されていないので、L1 Constructを使って定義します。

const cfnOriginAccessControl = new cdk.aws_cloudfront.CfnOriginAccessControl(this, 'OriginAccessControl', {
    originAccessControlConfig: {
        name: 'OriginAccessControlForContentsBucket',
        originAccessControlOriginType: 's3',
        signingBehavior: 'always',
        signingProtocol: 'sigv4',
        description: 'Access Control',
    },
});

principalの設定

S3ポリシーのprincipalはOACではサービスを対象とします。

principals: [
-    new cdk.aws_iam.CanonicalUserPrincipal(originAccessIdentity.cloudFrontOriginAccessIdentityS3CanonicalUserId)
+    new cdk.aws_iam.ServicePrincipal('cloudfront.amazonaws.com')
],

AWS:SourceArnの設定

CloudFrontのArnからのアクセスに対応するよう設定を追加します。

+ bucketPolicyStatement.addCondition('StringEquals', {
+     'AWS:SourceArn': `arn:aws:cloudfront::${cdk.Stack.of(this).account}:distribution/${distribution.distributionId}`
+ })

CloudFrontのオリジン設定を変更

OAIを使うときの設定がデフォルトで行われるため、OACに適応したテンプレートに上書きします。

const cfnDistribution = distribution.node.defaultChild as cdk.aws_cloudfront.CfnDistribution
// OACのidを指定する
cfnDistribution.addPropertyOverride('DistributionConfig.Origins.0.OriginAccessControlId', cfnOriginAccessControl.getAtt('Id'))
// デフォルトではs3のWebsiteURLが設定されてエラーとなるため、S3のドメイン名を設定する
cfnDistribution.addPropertyOverride('DistributionConfig.Origins.0.DomainName', s3.bucketRegionalDomainName)
// OAIの設定は不要のため、空文字で上書き
cfnDistribution.addOverride('Properties.DistributionConfig.Origins.0.S3OriginConfig.OriginAccessIdentity', "")
// OACではCustomOriginConfigは不要
cfnDistribution.addPropertyDeletionOverride('DistributionConfig.Origins.0.CustomOriginConfig')

S3のドメイン名はs3.bucketDomainNameでも良いが、CloudFrontに反映されるまで時間を要するため、理由がない限りはリージョンを含めたドメイン名を設定するのがよいかと。

トラブルシューティング

OACに変更する際に発生したエラーと解消方法を以下に記載しておきますので、参考になれば幸いです。

The origin type and OAC origin type differ

OACで設定するS3Originの値はWebsiteURLではなく、バケットのドメイン名を設定する必要があります。デフォルトではOAIを使った設定でテンプレートが作成されるので、上書きする必要があります。

cfnDistribution.addPropertyOverride('DistributionConfig.Origins.0.DomainName', s3.bucketRegionalDomainName)

Exactly one of CustomOriginConfig and S3OriginConfig must be specified

S3OriginConfigCustomOriginConfigはOACにおいて不要なので、上書き及び削除の対応が必要になります。

cfnDistribution.addOverride('Properties.DistributionConfig.Origins.0.S3OriginConfig.OriginAccessIdentity', "")
cfnDistribution.addPropertyDeletionOverride('DistributionConfig.Origins.0.CustomOriginConfig')

おわりに

OACがL2 Constructで設定できるようになると、この辺の対応は不要になるかなと。

https://github.com/aws/aws-cdk/issues/21771

世界中の天才が作ってくれています。感謝感謝。
CDKでどのようにテンプレートが作られているか、深く知ることができたので自分にとっては大きな収穫でした。
S3+CloudFrontでOACを使った構成を考えている方の参考になれば幸いです。

Discussion