CloudFront+S3とCodePipelineをCFnでデプロイする – その1

2020.12.01

はじめに

静的サイトの公開でよく利用されるCloudFront+S3、Githubにソースコードがある構成でCodepipelineでデプロイできるCFnを書いてみました。

構成図

 

AWSまたは他のドメインレジストラで取得したドメインのパブリックホステッドゾーンがあることが前提となります。

また、設計方針としてCloudFrontからのみAmazon S3 コンテンツへのアクセスを制限します。OAIを設定する為、CloudFrontとS3でテンプレートを分割すると面倒な分割(OAI作成→S3バケット作成→CloudFront作成)になってしまう為、CloudFront+S3テンプレートは一つのテンプレートで作成します。CloudFrontののSSL Certificateに適用するACMテンプレート、CloudFrontのエイリアスレコードを作成するRoute53テンプレート、GitHubからソースコードをデプロイするCodePipelineテンプレートを作成します。

ACMテンプレートの作成と実行

ACMのDNS検証は、2020年6月にアップデートで対応したのでリンクを参考にACMテンプレートを作成します。


AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  SystemName:
    Type: String
    Default: example
  BucketName:
    Type: String
    Default: www.example.xxx
  HostedZone:
    Type: String
    Default: ZXXXXXXXXXXXXXXXXXXXX

Resources:
  ACM:
    Type: AWS::CertificateManager::Certificate
    Properties:
      DomainName: !Ref BucketName
      DomainValidationOptions:
        - DomainName: !Ref BucketName
          HostedZoneId: !Ref HostedZone
      ValidationMethod: DNS
      Tags:
      -
        Key: Name
        Value: !Sub ${SystemName}-${BucketName}-acm

Outputs:
  ACMCertificateARN:
    Value: !Ref ACM
    Export:
      Name: !Sub ${SystemName}-CertificateARN

また、証明書リソースはCloudFrontがデプロイされるus-east-1リージョンに作成する必要があります。NestedStack且つOutputsセクションは同一リージョン以外で作成、参照ができない為、ACMテンプレートをus-east-1でデプロイします。

デプロイ完了後、Outputされた証明書リソースのARNをメモします。

CloudFront+S3テンプレートの作成(子スタック)

次にCloudFront+S3テンプレートを作成します。DependsOn属性でOAI作成後にバケットポリシーとCloudFrontを作成するように設定します。また、Route53でエイリアスレコードを追加する為、AliasesプロパティでCNAMEを定義します。Parametersは、親Stackで設定するのでデータ型のTypeプロパティだけ設定します。


AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  SystemName:
    Type: String
  BucketName:
    Type: String
  HostedZone:
    Type: String
  MinimumProtocolVersion:
    Type: String
  SslSupportMethod:
    Type: String
  CertificateARN:
    Type: String

Resources:
  S3bucketForOrigin:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${SystemName}-${BucketName}-${AWS::AccountId}
      CorsConfiguration:
        CorsRules:
          - AllowedHeaders:
              - Authorization
              - Content-Length
            AllowedMethods:
              - GET
            AllowedOrigins:
              - '*'
            MaxAge: 3000
  S3BucketPolicy:
    Type: 'AWS::S3::BucketPolicy'
    DependsOn: OAI
    Properties:
      PolicyDocument:
        Statement:
          - Sid: APIReadForGetBucketObjects
            Effect: Allow
            Principal:
              CanonicalUser: !GetAtt 
                - OAI
                - S3CanonicalUserId
            Action: 's3:GetObject'
            Resource: !Join 
              - ''
              - - 'arn:aws:s3:::'
                - !Ref S3bucketForOrigin
                - /*
      Bucket: !Ref S3bucketForOrigin
  OAI:
    Type: 'AWS::CloudFront::CloudFrontOriginAccessIdentity'
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: OAIConfig
  CloudFrontDistribution:
    Type: 'AWS::CloudFront::Distribution'
    DependsOn:
      - S3bucketForOrigin
      - OAI
    Properties:
      DistributionConfig:
        Aliases: 
          - !Sub ${BucketName}
        HttpVersion: http2
        Origins:
          - DomainName: !GetAtt 
              - S3bucketForOrigin
              - DomainName
            Id: !Sub S3bucketForOrigin
            S3OriginConfig:
              OriginAccessIdentity: !Join 
                - ''
                - - origin-access-identity/cloudfront/
                  - !Ref OAI
        Enabled: 'true'
        DefaultCacheBehavior:
          AllowedMethods:
            - DELETE
            - GET
            - HEAD
            - OPTIONS
            - PATCH
            - POST
            - PUT
          TargetOriginId: S3bucketForOrigin
          ForwardedValues:
            QueryString: 'false'
          ViewerProtocolPolicy: redirect-to-https
          DefaultTTL: 86400
          MaxTTL: 31536000
          MinTTL: 60
          Compress: true
        DefaultRootObject: index.html
        CustomErrorResponses:
          - ErrorCachingMinTTL: 300
            ErrorCode: 400
            ResponseCode: 200
            ResponsePagePath: /
          - ErrorCachingMinTTL: 300
            ErrorCode: 403
            ResponseCode: 200
            ResponsePagePath: /
          - ErrorCachingMinTTL: 300
            ErrorCode: 404
            ResponseCode: 200
            ResponsePagePath: /
        ViewerCertificate:
          AcmCertificateArn: !Sub ${CertificateARN}
          MinimumProtocolVersion: !Sub ${MinimumProtocolVersion}
          SslSupportMethod: !Sub ${SslSupportMethod}

Outputs:
  S3bucketForOrigin:
    Value: !Ref S3bucketForOrigin
  S3BucketSecureURL:
    Value: !Join 
      - ''
      - - 'https://'
        - !GetAtt 
          - S3bucketForOrigin
          - DomainName
  CloudFrontDistributionID:
    Value: !Ref CloudFrontDistribution
    Export:
      Name: !Sub ${SystemName}-CloudFrontDistributionID
  CloudFrontDomainName:
    Value: !GetAtt 
      - CloudFrontDistribution
      - DomainName
    Export:
      Name: !Sub ${SystemName}-CloudFrontDomainName
  CloudFrontSecureURL:
    Value: !Join 
      - ''
      - - 'https://'
        - !GetAtt 
          - CloudFrontDistribution
          - DomainName
  OAI:
    Value: !Ref OAI

Route53テンプレートの作成(子スタック)

ユーザガイドを参考にRoute53テンプレートを作成します。先ほどのテンプレートと同じくParametersは、親Stackで設定するのでデータ型のTypeプロパティだけ設定します。


AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  SystemName:
    Type: String
  BucketName:
    Type: String
  HostedZone:
    Type: String

Resources:
  CloudFrontDNSRecord:
    Type: 'AWS::Route53::RecordSetGroup'
    Properties:
      HostedZoneId: !Ref HostedZone
      RecordSets:
        - Name: !Ref BucketName
          Type: A
          AliasTarget:
            HostedZoneId: Z2FDTNDATAQYW2
            DNSName: {'Fn::ImportValue': !Sub '${SystemName}-CloudFrontDomainName'}

Outputs:
  URL:
    Value: !Sub 'https://${BucketName}'



実はユーザガイドにたどり着くまで、AliasTargetのHostedZoneIdプロパティの値を誤って自身の管理するHostedZoneのIDを指定したことでハマってしまいました。。以下の通り、CloudFront.netのホストゾーンIDを指定する必要があります。

注記

エイリアスリソースレコードセットを作成するときは、以下の例に示すように Z2FDTNDATAQYW2 プロパティに HostedZoneId を指定する必要があります。CloudFront 用のエイリアスリソースレコードセットは、プライベートホストゾーンでは作成できません。

Pipeline以外のテンプレートが揃いました。静的サイト構成として動作に問題ないか確認する為、ネストした親テンプレートを作成します。

NestedStackテンプレートの作成(親スタック)

子スタックのテンプレートは、S3に格納する必要があるので任意のS3バケットに格納してTemplateURLをオブジェクトURLに置換してください。ついでに親スタックのテンプレートもS3にアップロードします。


AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  SystemName:
    Type: String
    Default: example
  BucketName:
    Type: String
    Default: www.example.xxx
  HostedZone:
    Type: String
    Default: Z10293303OK6IN7V0JZVB
  MinimumProtocolVersion:
    Type: String
    Default: TLSv1.2_2019
    AllowedValues:
      - SSLv3
      - TLSv1
      - TLSv1.1_2016
      - TLSv1.2_2018
      - TLSv1.2_2019
      - TLSv1_2016
  SslSupportMethod:
    Type: String
    Default: sni-only
    AllowedValues:
      - sni-only
      - static-ip
      - vip
  CertificateARNatVirginia:
    Type: String
    Default: ''

Resources: 
  HOSTING: 
    Type: AWS::CloudFormation::Stack
    Properties: 
      TemplateURL: 'https://cfn-template-XXXXXXXXXXXX.s3-ap-northeast-1.amazonaws.com/hosting/hosting.yml'
      Parameters: 
        SystemName: !Sub ${SystemName}
        BucketName: !Sub ${BucketName}
        HostedZone: !Sub ${HostedZone}
        MinimumProtocolVersion: !Sub ${MinimumProtocolVersion}
        SslSupportMethod: !Sub ${SslSupportMethod}
        CertificateARN: !Sub ${CertificateARN}
  DNS: 
    Type: AWS::CloudFormation::Stack
    DependsOn: HOSTING
    Properties: 
      TemplateURL: 'https://cfn-template-XXXXXXXXXXXX.s3-ap-northeast-1.amazonaws.com/hosting/dnsrecord.yml'
      Parameters: 
        SystemName: !Sub ${SystemName}
        BucketName: !Sub ${BucketName}
        HostedZone: !Sub ${HostedZone}

NestedStackの実行

それでは実行してきます。Amazon S3 URLに親スタックテンプレートのオブジェクトURLを入力します。

スタックの名前、CertificateARNに証明書リソースのARNを入力します。以降、デフォルトで進んでStackを実行します。

ステータスがCREATE_COMPLETEされたことを確認します。 OriginのS3バケットは、hosting-HOSTING-xxxxxxxxの出力タブ、URLはhosting-DNS-xxxxxxxxxの出力タブで確認きます。

S3バケットに任意のhtmlファイルを配置して、URLにアクセスできるようになります。

もし、Access Deniedや307がでた場合、以下の理由でエラーになっている可能性があります。

Amazon S3 から HTTP 307 Temporary Redirect レスポンスが返るのはなぜですか?

回避する為には、CloudFrontのOrigin Domain Nameにリージョンエンドポイントを追記することでエラーを回避できます。

Origin Domain Name変更前:バケット名.s3.amazonaws.com

Origin Domain Name変更後:バケット名.s3-ap-northeast-1.amazonaws.com

 

ここまでで静的サイトの動作確認ができました。 次回、CodePipelineテンプレートで静的サイトを更新する記事を書きたいと思います。