【CloudFormation】CloudFront の OAI を OAC に移行する

## はじめに
2022/8/25 に Amazon CloudFront で Origin Access Control (OAC) が使用可能になりました。OAC は CloudFront で S3 バケットをオリジンとする際の新たなアクセス制御方法です。

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

これまでオリジンの S3 バケットへのアクセスを CloudFront のみに限定するために Origin Access Identity (OAI) という機能が提供されていました。OAC は OAI と比較し、以下の点が強化されています。

* セキュリティ
より短期間のクレデンシャルおよびローテーション、リソースベースポリシーなどのベストプラクティスが実装されています。

* SSE-KMS を使用したサーバー側の暗号化のサポート
SSE-KMS で暗号化された S3 オブジェクトのダウンロードとアップロードをサポートします。これにより IAM ポリシーやキーポリシーによる追加のアクセス制御や暗号化マテリアルのローテーション、CloudTrail による証跡取得などの多くのメリットを追加で得ることができます。

* S3 への動的リクエスト
GET だけではなく、POST や PUT など包括的な HTTP メソッドをサポートしています。

* すべてリージョンでサポート
既存のリージョンでは OAI も引き続きサポートされますが、2022 年 12 月以降に開設される新規リージョンについては OAC のみがサポートされる予定です。

現在 OAI は **Legacy** 扱いとなっており、セキュリティのベストプラクティスや最新リージョンへの対応の観点から OAI から OAC の移行が推奨されています。以降では OAI を使用している既存の CloudFormation テンプレートを OAC に移行する際の修正点を紹介します。

サンプルのテンプレートは GitHub にもあげていますのでご参考まで。

https://github.com/hayao-k/CFn-sample-cloudfront-oac

## CloudFormation テンプレートの修正例
既存の CloudFormation テンプレートを OAI から OAC に移行するには、最低限以下の修正が必要です。

* OAC の作成
* バケットポリシーの修正
* Distribution Origin の修正

SSE-KMS によるサーバー側の暗号化に対応させるためには追加の手順が必要になりますが、本記事では触れません。

### OAC の作成
`AWS::CloudFront::OriginAccessControl` を作成します。OriginAccessControlConfig の Description は任意項目です。

```yaml
Resources:
  CloudFrontOriginAccessControl:
    Type: AWS::CloudFront::OriginAccessControl
    Properties: 
      OriginAccessControlConfig:
        Description: Default Origin Access Control
        Name: !Ref AWS::StackName
        OriginAccessControlOriginType: s3
        SigningBehavior: always
        SigningProtocol: sigv4
```

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudfront-originaccesscontrol.html

移行後は `AWS::CloudFront::CloudFrontOriginAccessIdentity` は不要になりますので削除します。

### バケットポリシーの修正
```yaml:before
  # S3 bucket policy to allow access from CloudFront OAI
  AssetsBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref AssetsBucket
      PolicyDocument:
        Statement:
        - Action: s3:GetObject
          Effect: Allow
          Resource: !Sub ${AssetsBucket.Arn}/*
          Principal:
            AWS: !Sub arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity}
```

```yaml:after
  # S3 bucket policy to allow access from CloudFront OAC
  AssetsBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref AssetsBucket
      PolicyDocument:
        Statement:
        - Action: s3:GetObject
          Effect: Allow
          Resource: !Sub ${AssetsBucket.Arn}/*
          Principal:
            Service: cloudfront.amazonaws.com
          Condition:
            StringEquals:
              AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${AssetsDistribution}
```

ここでは OAI 用のポリシーを削除していますが、OAI と OAC 両方のポリシーを記載することが推奨される移行手順です。これにより OAC への移行中に CloudFront が S3 バケットへのアクセスを失うこと防ぐことができます。必要に応じて対応してください。

https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html

その際は以下のような流れになります。

* OAI および OAC 両方のバケットポリシーを設定したテンプレートで更新
* Distribution の変更完了後、OAI 用のポリシーを削除したテンプレートで再度更新


### Distribution Origin の修正
OAI は Distribution Origin の S3OriginConfig を指定しますが、OAC では OriginAccessControlId を指定します。

```yaml:before
  AssetsDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
        - Id: S3Origin
          DomainName: !GetAtt AssetsBucket.DomainName
          S3OriginConfig:
            OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}
```

```yaml:after
  AssetsDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
        - Id: S3Origin
          DomainName: !GetAtt AssetsBucket.DomainName
          S3OriginConfig:
            OriginAccessIdentity: ''
          OriginAccessControlId: !GetAtt CloudFrontOriginAccessControl.Id
```

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-origin.html

:::note warn
2022/9/14 時点では S3OriginConfig で空の OriginAccessIdentity を指定する必要がありました。
:::

S3OriginConfig を削除すると、スタック更新時に以下のようなエラーとなります。

```
Resource handler returned message: "Invalid request provided: Exactly one of CustomOriginConfig and S3OriginConfig must be specified"
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/fea594a0-d1da-e832-9e2c-814cf3fcd687.png)



### 修正後のテンプレート例

<details><summary>クリックして全体を表示</summary><div>

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: Static contents distribution using S3 and CloudFront.

Parameters:
  CachePolicy:
    Description: Change this if you want to specify a cache policy.
    Type: String
    Default: CachingOptimized
    AllowedValues:
      - CachingOptimized
      - CachingDisabled
      - CachingOptimizedForUncompressedObjects
      - Elemental-MediaPackage
      - Amplify

Mappings: 
  CachePolicyIds:
    CachingOptimized:
      Id: 658327ea-f89d-4fab-a63d-7e88639e58f6
    CachingDisabled:
      Id: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad
    CachingOptimizedForUncompressedObjects:
      Id: b2884449-e4de-46a7-ac36-70bc7f1ddd6d
    Elemental-MediaPackage:
      Id: 08627262-05a9-4f76-9ded-b50ca2e3a84f
    Amplify:
      Id: 2e54312d-136d-493c-8eb9-b001f22f67d2

Resources:
  # S3 bucket contains static contents
  AssetsBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration: 
          - ServerSideEncryptionByDefault: 
              SSEAlgorithm: AES256
      PublicAccessBlockConfiguration: 
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      LoggingConfiguration:
        DestinationBucketName: !Ref AccessLogBucket
        LogFilePrefix: origin/

  # S3 bucket policy to allow access from CloudFront OAI
  AssetsBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref AssetsBucket
      PolicyDocument:
        Statement:
        - Action: s3:GetObject
          Effect: Allow
          Resource: !Sub ${AssetsBucket.Arn}/*
          Principal:
            Service: cloudfront.amazonaws.com
          Condition:
            StringEquals:
              AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${AssetsDistribution}
        - Effect: Deny
          Principal: '*'
          Action: 's3:*'
          Resource: 
            - !Sub ${AssetsBucket.Arn}/*
            - !GetAtt AssetsBucket.Arn
          Condition:
            Bool: 
              aws:SecureTransport: false

  AccessLogBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Properties:
      AccessControl: LogDeliveryWrite
      BucketEncryption:
        ServerSideEncryptionConfiguration: 
          - ServerSideEncryptionByDefault: 
              SSEAlgorithm: AES256
      LifecycleConfiguration:
        Rules:
          - Id: Retain2yrs
            Status: Enabled
            ExpirationInDays: 730
            Transitions:
              - StorageClass: STANDARD_IA
                TransitionInDays: 30
      PublicAccessBlockConfiguration: 
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

  AccessLogBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref AccessLogBucket
      PolicyDocument:
        Statement:
          - Effect: Deny
            Principal: '*'
            Action: 's3:*'
            Resource: 
              - !Sub ${AccessLogBucket.Arn}/*
              - !GetAtt AccessLogBucket.Arn
            Condition:
              Bool: 
                aws:SecureTransport: false

  # CloudFront Distribution for contents delivery
  AssetsDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
        - Id: S3Origin
          DomainName: !GetAtt AssetsBucket.DomainName
          S3OriginConfig:
            OriginAccessIdentity: ''
          OriginAccessControlId: !GetAtt CloudFrontOriginAccessControl.Id
        Enabled: true
        DefaultRootObject: index.html
        Comment: !Sub ${AWS::StackName} distribution
        DefaultCacheBehavior:
          CachePolicyId: !FindInMap [ CachePolicyIds, !Ref CachePolicy , Id ]
          TargetOriginId: S3Origin
          ViewerProtocolPolicy: redirect-to-https
        HttpVersion: http2
        ViewerCertificate:
          CloudFrontDefaultCertificate: true
        IPV6Enabled: false
        Logging:
          Bucket: !GetAtt AccessLogBucket.DomainName
          IncludeCookies: false
          Prefix: cloudfront/

  CloudFrontOriginAccessControl:
    Type: AWS::CloudFront::OriginAccessControl
    Properties: 
      OriginAccessControlConfig:
        Description: Default Origin Access Control
        Name: DefaultOAC
        OriginAccessControlOriginType: s3
        SigningBehavior: always
        SigningProtocol: sigv4

Outputs:
  URL:
     Value: !Join [ "", [ "https://", !GetAtt AssetsDistribution.DomainName ]]

```
</div></details>

## 移行してみた例
修正後のテンプレートを使用して、既存の CloudFormation スタックを更新してみます。

変更セットの内容が想定通りであることを確認してスタックを更新します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/9f3d8793-0f5e-39f9-653c-a5d3aa5fe507.png)

前述のとおり、OAI 用のバケットポリシーを削除して OAC に移行するとディストリビューションの更新中は AccessDenied が発生しますのでご注意ください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/065e2cc5-b228-9ae1-bca5-72b844425d27.png)

ディストリビューションの更新後、S3 バケットアクセスの設定が OAC に更新されていることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/64358717-0630-d1f0-ffb6-9efadbace595.png)

S3 バケットポリシーも想定通り更新されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/a2bb5ed5-312d-a026-97e4-99db74939ef2.png)

ブラウザからのアクセスも問題無さそうです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/a824c342-b95e-5257-f754-47336db9fcf4.png)




以上です。
参考になれば幸いです。