Route 53 + CloudFront + S3 + WAFの構成をCloudFormationで作る

# やりたいこと
- CloudFront + S3でSPAサイトを公開したい
- そしてRoute53で独自ドメインでアクセスできるようにする
- かつCloudFrontにWAFを設定してアクセス制限したい

CloudFormationで一気に作ろうとしたらWAFを作るところで怒られてしまった...

調べてみるとWebACLのScopeを`CLOUDFRONT`で作成する場合はリージョンを`us-east-1`で作成する必要がありました

参考↓

https://dev.classmethod.jp/articles/cloudformation-webacl-cloudfront-error/

ドキュメントにも書いてありました、なるほど
> For CLOUDFRONT, you must create your WAFv2 resources in the US East (N. Virginia) Region, us-east-1

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-wafv2-webacl.html#:~:text=%3A%20No%20interruption-,Scope,-Specifies%20whether%20this

# 解決
ということで`us-east-1`で作る必要があるものは別で作成してから、
CloudFrontとかS3などは後から別のテンプレートで作成します
（今回は東京リージョンに作りたかったのでそこに）

全体の流れとしては下記のような感じにしました
1. `us-east-1`で作成が必要なものを作る
    1. SSL証明書
    1. Route53 ホストゾーン
    1. WAF
1. `ap-northeast-1`で他のものを作る
    1. S3
    1. CloudFront
    1. Route53のレコード


## us-east-1で作成するもの
まずは`us-east-1`で作成が必要なものから先に作成します
独自ドメインは取得済みである前提です

WAFは特定のIPからだけアクセスできるようなルールにしています
また、WebACLはブロックした時に`404`を返すようにしています
（デフォルトは403ですが、403の場合はCloudFrontでルートパスにリダイレクトしたいので404を返すようにしてます）

CloudFormationテンプレートは下記のような感じです
```yaml
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  DomainName:
    Description: Domain Name
    Type: String

Resources:
  # -----
  # ACM
  # -----
  ACM:
    Type: AWS::CertificateManager::Certificate
    Properties:
      DomainName: !Ref DomainName
      ValidationMethod: DNS
    DeletionPolicy: Delete

  # -----
  # Route53 Hosted Zone
  # -----
  Route53HostedZone:
    Type: AWS::Route53::HostedZone
    Properties:
      Name: !Ref DomainName
    DeletionPolicy: Delete

  # -----
  # WAF
  # -----
  ##### IP Set
  WAFIPSet:
    Type: AWS::WAFv2::IPSet
    Properties:
      NAME: MyIPSet
      Addresses:
        - 2400:2400:2400:2400:2400:2400:2400:2400/128
      IPAddressVersion: IPV6
      Scope: CLOUDFRONT
    DeletionPolicy: Delete

  ##### WAF Web ACL
  WAFWebAcL:
    Type: AWS::WAFv2::WebACL
    Properties:
      DefaultAction:
        Block:
          CustomResponse:
            ResponseCode: 404
      Name: MyWebACL
      Rules:
        - Name: MyRule
          Action:
            Allow: {}
          Priority: 0
          Statement:
            IPSetReferenceStatement:
              Arn: !GetAtt WAFIPSet.Arn
          VisibilityConfig:
            CloudWatchMetricsEnabled: true
            MetricName: MyRuleMetricName
            SampledRequestsEnabled: true
      Scope: CLOUDFRONT
      VisibilityConfig:
        CloudWatchMetricsEnabled: true
        MetricName: MyWebACLMetricName
        SampledRequestsEnabled: true
    DeletionPolicy: Delete
```

これでスタックを作成します（us-east-1で作成することを忘れずに）

ACMのSSL証明書はDNS検証が必要になります
作成したホストゾーンにDNSレコードを追加して検証が完了したらOKです

https://docs.aws.amazon.com/ja_jp/acm/latest/userguide/dns-validation.html

## 残りのもの
あとは東京リージョンで作成するものを作ります
CloudFront + S3の設定はOAIを使用して、CloudFrontを経由せずにS3へ直接アクセスすることを制限します
それと独自ドメインをCloudFrontに向かわせるためのRoute53レコードも作ります

パラメータは下記のように上で作成したリソースを指定するようにします
- CertificateArn
    - 上で作成したSSL証明書のARN
- DomainName
    - ドメイン
- WebACLArn
    - 作成したWebACLのARN
- Route53HostedZone
    - 作成したホストゾーンのID

テンプレートはこんな感じ
```yaml
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  CertificateArn:
    Description: Certificate Arn
    Type: String
  DomainName:
    Description: Domain Name
    Type: String
  WebACLArn:
    Description: Web ACL Arn for CloudFront
    Type: String
  Route53HostedZone:
    Description: Route53 Hosted Zone Id
    Type: String

Resources:
  # -----
  # S3
  # -----
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: shiotomo-vite-app-bucket
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
    DeletionPolicy: Delete

  BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3Bucket
      PolicyDocument:
        Statement:
          - Action: s3:GetObject
            Effect: Allow
            Resource: !Sub arn:aws:s3:::${S3Bucket}/*
            Principal:
              AWS: !Sub arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity}
    DeletionPolicy: Delete

  # -----
  # CloudFront
  # -----
  CloudFront:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Comment: My Distribution
        Enabled: true
        Origins:
          - Id: S3Bucket
            DomainName: !GetAtt S3Bucket.DomainName
            S3OriginConfig:
              OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}
        ViewerCertificate:
          AcmCertificateArn: !Ref CertificateArn
          MinimumProtocolVersion: TLSv1.2_2021
          SslSupportMethod: sni-only
        Aliases:
          - !Ref DomainName
        CustomErrorResponses:
          -
            ErrorCode: 403
            ResponsePagePath: /
            ResponseCode: 200
        DefaultRootObject: index.html
        DefaultCacheBehavior:
          TargetOriginId: S3Bucket
          Compress: true
          CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6
          ViewerProtocolPolicy: redirect-to-https
        DefaultRootObject: index.html
        IPV6Enabled: true
        WebACLId: !Ref WebACLArn
    DeletionPolicy: Delete

  CloudFrontOriginAccessIdentity:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: MyOAI
    DeletionPolicy: Delete

  # -----
  # Route53 Records
  # -----
  Route53ARecord:
    Type: AWS::Route53::RecordSet
    Properties:
      Name: !Ref DomainName
      Type: A
      AliasTarget:
        DNSName: !GetAtt CloudFront.DomainName
        HostedZoneId: Z2FDTNDATAQYW2
      HostedZoneId: !Ref Route53HostedZone
    DeletionPolicy: Delete

  Route53AAAARecord:
    Type: AWS::Route53::RecordSet
    Properties:
      Name: !Ref DomainName
      Type: AAAA
      AliasTarget:
        DNSName: !GetAtt CloudFront.DomainName
        HostedZoneId: Z2FDTNDATAQYW2
      HostedZoneId: !Ref Route53HostedZone
    DeletionPolicy: Delete
```

これでスタック作成して問題なければ
あとはS3にindex.htmlをアップするだけです

独自ドメインにアクセスして表示されれば完了！

# 参考
CloudFrontのOAIについて↓

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


https://dev.classmethod.jp/articles/s3-cloudfront-with-oai-by-cloudformation/