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

## はじめに
久しぶりに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
:::note warn
最低限必要なプロパティのみ設定しています。
:::
```typescript
// 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(webSiteBucket, { 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に変更する
:::note warn
最低限必要なプロパティのみ設定しています。
:::
```typescript
// 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(webSiteBucket)
    },
})

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を使って定義します。
```diff_typescript
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ではサービスを対象とします。
```diff_typescript
principals: [
-    new cdk.aws_iam.CanonicalUserPrincipal(originAccessIdentity.cloudFrontOriginAccessIdentityS3CanonicalUserId)
+    new cdk.aws_iam.ServicePrincipal('cloudfront.amazonaws.com')
],
```

### AWS:SourceArnの設定
CloudFrontのArnからのアクセスに対応するよう設定を追加します。
```diff_typescript
+ bucketPolicyStatement.addCondition('StringEquals', {
+     'AWS:SourceArn': `arn:aws:cloudfront::${cdk.Stack.of(this).account}:distribution/${distribution.distributionId}`
+ })
```

### CloudFrontのオリジン設定を変更
OAIを使うときの設定がデフォルトで行われるため、OACに適応したテンプレートに上書きします。
```typescript
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を使った設定でテンプレートが作成されるので、上書きする必要があります。
```typescript
cfnDistribution.addPropertyOverride('DistributionConfig.Origins.0.DomainName', s3.bucketRegionalDomainName)
```

### Exactly one of CustomOriginConfig and S3OriginConfig must be specified
`S3OriginConfig`と`CustomOriginConfig`はOACにおいて不要なので、上書き及び削除の対応が必要になります。
```typescript
cfnDistribution.addOverride('Properties.DistributionConfig.Origins.0.S3OriginConfig.OriginAccessIdentity', "")
cfnDistribution.addPropertyDeletionOverride('DistributionConfig.Origins.0.CustomOriginConfig')
```

## おわりに
OACがL2 Constructで設定できるようになると、この辺の対応は不要になるかなと。
CDKでどのようにテンプレートが作られているか、深く知ることができたので自分にとっては大きな収穫でした。
S3+CloudFrontでOACを使った構成を考えている方の参考になれば幸いです。