CloudFormationを使ってCloudFront+S3の静的サイトを構築

## 目標
- 静的サイトをS3に用意し、CloudFrontから公開する
- S3はCloudFrontからのアクセスのみ許可する
- アクセスログをS3バケットに格納する
- 独自ドメインは使用しない
- CloudFormationで作成する

![static-site.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123946/0d17e7ef-32ab-8b8b-b446-fa3e46d385e3.png)

### 静的ウェブサイトの設定について
そもそも静的ウェブサイトの設定方法は2020年10月6日時点では、以下の設定があるようです。

- アクセスがオリジンアクセスアイデンティティ(OAI) で制限されたオリジンとして、REST API エンドポイントを使用する
- 匿名 (パブリック) アクセスを許可して、ウェブサイトのエンドポイントをオリジンとして使用する
- アクセスが Referer ヘッダーで制限されたオリジンとして、ウェブサイトのエンドポイントを使用する
- AWS CloudFormation を使用して REST API エンドポイントをオリジンとしてデプロイし、OAI と CloudFront を指すカスタムドメインによってアクセスを制限する

今回はCloudFormationを使用してOAIで制限されたREST APIエンドポイント使用する感じになります。


## 実装

```static-site.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: "aws static site sample"
Resources:
  S3BucketLogs:
    Type: AWS::S3::Bucket
    Properties:
      AccessControl: LogDeliveryWrite
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
  S3BucketWebStatic:
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      LoggingConfiguration:
        DestinationBucketName: !Ref S3BucketLogs
        LogFilePrefix: 'origin/'
  BucketPolicyWebStatic:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3BucketWebStatic
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Action:
              - s3:GetObject
            Effect: Allow
            Resource: !Sub '${S3BucketWebStatic.Arn}/*'
            Principal:
              CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId
  CloudFrontOriginAccessIdentity:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: "CloudFront OAI for Static Web Site"
  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        DefaultCacheBehavior:
          Compress: true
          DefaultTTL: 86400
          ForwardedValues:
            QueryString: true
          MaxTTL: 31536000
          TargetOriginId: 'WebStatic-S3'
          # httpの通信はhttpsにリダイレクトする
          ViewerProtocolPolicy: 'redirect-to-https'
        Enabled: true
        HttpVersion: 'http2'
        DefaultRootObject: 'index.html'
        IPV6Enabled: true
        Logging:
          Bucket: !GetAtt S3BucketLogs.DomainName
          IncludeCookies: false
          Prefix: 'cdn/'
        Origins:
          # 作成直後にStatusCode 307とならないように、Region指定の方式
          # https://aws.amazon.com/jp/premiumsupport/knowledge-center/s3-http-307-response/
          - DomainName: !Join ["", [!Ref S3BucketWebStatic, !Sub ".s3-${AWS::Region}.amazonaws.com"]]
            Id: 'WebStatic-S3'
            S3OriginConfig:
              OriginAccessIdentity:
                !Join ['', ['origin-access-identity/cloudfront/', !Ref CloudFrontOriginAccessIdentity]]
        PriceClass: 'PriceClass_All'
        ViewerCertificate:
          CloudFrontDefaultCertificate: true
Outputs:
  S3BucketLogsName:
    Description: "S3 Bucket for keeping access logs."
    Value: !Ref S3BucketLogs
  S3BucketWebStaticName:
    Description: "S3 Bucket for deploying static site."
    Value: !Ref S3BucketWebStatic
  CloudFrontDomainName:
    Description: "CloudFront DomainName."
    Value: !GetAtt  CloudFrontDistribution.DomainName
```

aws cliからスタックを作成します。

```shell-session
$ aws cloudformation create-stack --stack-name static-site-sample --template-body file://static-site.yaml
```

作成完了後にOutputで作成したS3のバケット名とCloudFrontのドメイン名を確認します。

```shell-session
$ aws cloudformation describe-stacks --stack-name static-site-sample                   {
    "Stacks": [
        {
            "StackId": "arn:aws:cloudformation:ap-northeast-1:xxxx:stack/static-site-sample/xxxxx",
            "StackName": "static-site-sample",
            "Description": "aws static site sample",   
...
            "Outputs": [
                {
                    "OutputKey": "S3BucketLogsName",
                    "OutputValue": "static-site-sample-s3bucketlogs-xxxxx",
                    "Description": "S3 Bucket for keeping access logs."
                },
                {
                    "OutputKey": "S3BucketWebStaticName",
                    "OutputValue": "static-site-sample-s3bucketwebstatic-xxxx",
                    "Description": "S3 Bucket for deploying static site."
                },
                {
                    "OutputKey": "CloudFrontDomainName",
                    "OutputValue": "xxxxxxx.cloudfront.net",
                    "Description": "CloudFront DomainName."
                }
            ],
...                           
```

作成したバケットに静的ファイルをアップロードします。仮にビルドしたファイルのアップロードを想定します。

```shell-session
$ aws s3 cp --recursive ./build s3://static-site-sample-s3bucketwebstatic-xxxx
```

以上で作成が完了しているはずです。cloudfrontからのアクセスは確認ができるが、S3のオブジェクトURLは403でエラーとなるはずです。

ソースコードはこちらです。
https://github.com/shikazuki/aws-static-site-sample

## 参考
- https://aws.amazon.com/jp/premiumsupport/knowledge-center/cloudfront-serve-static-website/
- https://github.com/aws-samples/amazon-cloudfront-secure-static-site
- https://aws.amazon.com/jp/premiumsupport/knowledge-center/s3-http-307-response/
- https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/AWS_CloudFront.html
- https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/AWS_S3.html