CloudFormationで静的サイトをホスティングしてみた

2022-08-12 Fri 00:00

Emacsでブログを作ったので公開する際にCloudFormationを使ってAWSにホスティングすることにしました。設定する際に一体何をしたのか理解するために、やったことをブレイクダウンしました。ちなみにこの記事のコードは僕のGitHub Repositoryにあります。

ウェブサイトのインフラ構成

ただの静的サイトにお金をかけたくなかったので、今回作ったサイトはお金があまりかからないようにデザインしました。もしお金が大量にかかるのであればNetlifyみたいに無料でホスティングできるサービスとかを使います。じゃなんでCloudFormationなんだって思いました?それはIaC (Infrastructure as Code)をやってみて学びたかったからです。あと僕のちょっとだけあるIaCの知識によればメンテナンスしやすくなるはずなのでそれも理由の一つです。

サイトに使ったAWSのサービス

インフラ構造は頭で想像はできていましたが、はっきりさせるためネットワーク図を作ってみることにしました。

2022-07-30_09-30-30_screenshot.png

下記のAWSサービスを使いました。

AWS Serviceだけ見るととてもシンプルな作りになってます。中身がどうなってるのかもうちょっととどんな処理がされているのかを表してみました。

2022-08-12_08-08-52_screenshot.png

この構成でだとお金があまりかからないので必要としていたデザインになりました。払うのはRoute 53にかかる月$0.50ぐらいです。もっとサイトに来るユーザーが増えてもCloudFrontがあるので月に1TBのデータ送信と10,000,000回のHTTPかHTTPS通信が無料で使える!安いですねー。

静的ウェブサイトを作る!

やっとCloudFormationでインフラ構築するときが来ました。まっさらな状態からは難しそうなのでググって見つけた似てるインフラ構成のCloudFormationがあるGitHub Repositoryを参考にしました。このテンプレートファイルをそのまま使ったら使いたいインフラ構築の作成ができそうですが、複数のCloudFormation stackを使ってたりして複雑でした。なのでインフラのメンテナンスしないといけない未来の自分のために不要なものはすべてなくして、できるだけシンプルな一つのCloudFormation stackにしました。

このセクションではS3にあるHTMLファイルをCloudFrontを通してアクセスできるようなウェブサイトを作ります。このセクションが終わったらCloudFrontが提供してくれたドメインからサイトにアクセスすることができました。

S3 bucketを作る!

最初にサイトのHTMLファイルとかをおいておく場所を作成します。下記の設定はS3 Bucketを作成してくれます。

Resources:
  WebsiteRootS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub '${AWS::StackName}-root-bucket'

BucketName のプロパティーはユニークである必要があったため、 AWS::StackName を名前の最初につけることにしました。 AWS::StackNameCloudFormation Pseudo ParameterというADSが用意してくれるパラメーターの一つです。結構使えそうですね。覚えておこう。

AWS Management ConsoleでCloudFormation stackを作成するときにこんな感じでStack nameを聞かれます。

2022-08-09_09-02-51_screenshot.png

僕は gene-website って指定したので !Sub '${AWS::StackName}-root-bucket'gene-website-root-bucket に変換されます。便利ですねー。

CloudFront Distributionを作る!

S3 bucketができたところで今度はCloudFrontを通してアクセスできるようにします。個人的に設定がたくさん入り組んでいて、ここは想像以上の難しさでした。わかりやすくするためにどんなAWS Resourceが必要なのかとそいつらがどういう関係なのかを表した図を作ってみました。図を作ったら自分でも結構理解が深まりました。作ってよかったー。

2022-08-12_08-53-31_screenshot.png

一気に設定みても難しいので3つのパートに分けました。

Part 1: Distributionを作る!

CloudFrontの中心的存在のCloudFront Distributionを作ります。それぞれのプロパティーの詳細はAWSのドキュメントにあるので見てみてください。

Resources:
  ...

  WebsiteCloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Enabled: true
        HttpVersion: 'http2'
        IPV6Enabled: true
        PriceClass: 'PriceClass_All'

Part 2: S3のoriginを作る!

最初に作成したS3をCloudFrontのoriginに設定するのには複数のリソースを正しく設定する必要があります。どんな設定が必要なのかをはっきりさせるためにこのパートで設定するものだけを図から切り離しました。

2022-08-12_08-54-52_screenshot.png

CloudFront Origin Access Identityが必要です。これがCloudFrontがS3 bucketをアクセスするときに使うユーザーになります。

Resources:
  ...

  WebsiteCloudFrontOAI:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: 'CloudFront OAI for website'

次はS3 Bucket Policyでorigin access identityがS3 bucketの中身を取得する許可をする設定をします。

Resources:
  ...

  WebsiteS3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      # 最初に作ったS3 bucket
      Bucket: !Ref WebsiteRootS3Bucket 
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Action:
              - s3:GetObject
            Effect: Allow
            Resource: !Join ['', [!GetAtt WebsiteRootS3Bucket.Arn,  '/*']]
            Principal:
              # さっき作ったOriginAccessIdentity
              CanonicalUser: !GetAtt WebsiteCloudFrontOAI.S3CanonicalUserId 
          - Action:
              - s3:ListBucket
            Effect: Allow
            Resource: !GetAtt WebsiteRootS3Bucket.Arn
            Principal:
              # さっき作ったOriginAccessIdentity
              CanonicalUser: !GetAtt WebsiteCloudFrontOAI.S3CanonicalUserId

これでorigin access identityでS3 bucketの内容を取得できるようになります。今度はdistributionにS3 bucketをoriginとして設定してそのoriginにアクセスするときはorigin access identityを使うように設定します。

Resources:
  ...

  WebsiteCloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        ...
        Origins:
          - Id: 'website-root-s3'
            # 最初に作ったS3 bucket
            DomainName: !GetAtt WebsiteRootS3Bucket.DomainName
            S3OriginConfig:
              OriginAccessIdentity:
                # さっき作ったOriginAccessIdentity
                !Join ['', ['origin-access-identity/cloudfront/', !Ref WebsiteCloudFrontOAI]]

これでCloudFront distributionはS3 bucketの内容を取得することができるようになりました!

Part 3: デフォルトの設定する

もう設定完了したと思いましたか?まだなんです。CloudFrontにデフォルトの設定してあげないと動かないんです。CloudFront Cache PolicyでCacheの方針を決めます。

Resources:
  ...

  WebsiteCloudFrontCachePolicy:
    Type: AWS::CloudFront::CachePolicy
    Properties: 
      CachePolicyConfig:
        Name: Sub '${AWS::StackName}-cache-policy'
        Comment: 'CachePolicy for website'
        DefaultTTL: 86400 # in seconds (one day)
        MaxTTL: 31536000 # in seconds (one year)
        MinTTL: 1 # Must be at least 1
        ParametersInCacheKeyAndForwardedToOrigin: 
          CookiesConfig:
            CookieBehavior: 'none'
          EnableAcceptEncodingBrotli: true
          EnableAcceptEncodingGzip: true
          HeadersConfig: 
            HeaderBehavior: 'none'
          QueryStringsConfig:
            QueryStringBehavior: 'none'

Name のプロパティーがユニークである必要があったため、これも名前の最初にstack nameをつけることにしました。この設定はAWSが提供しているmanaged cache policyの一つと同じです。ドキュメントに詳細があります。特別な設定は必要なかったので CachingOptimized と同じ設定にしました。 CachingOptimized のmanaged cache policyのID(ドキュメントにあります)を指定してそれを使えば新規に作成する必要はなかったですが、コード上にどうやってcacheされているのかを記載したかったので新規に作ることにしました。

下記の設定で新規のcache policyがdistributionのデフォルトとなります。

Resources:
  ...

  WebsiteCloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        ...
        DefaultCacheBehavior:
          # さっき作ったcache policy
          CachePolicyId: !Ref WebsiteCloudFrontCachePolicy
          Compress: true
          # さっき作ったoriginのid
          TargetOriginId: 'website-root-s3'
          ViewerProtocolPolicy: 'redirect-to-https'
        DefaultRootObject: 'index.html'
        CustomErrorResponses:
          - ErrorCachingMinTTL: 60
            ErrorCode: 404
            ResponseCode: 404
            ResponsePagePath: '/404.html'

この設定でCloudFront distributionはデフォルトでS3 bucketを見に行くようになります。ルートパス(/)と404エラーのときにどのファイルを返すかの設定もされています。

これで静的サイトにアクセスできる! AWS Management ConsoleでCloudFront distributionに行けば Distribution domain name があるのでそこからアクセスできますよ。

自分のドメインからCloudFrontにアクセスできるようにする

CloudFrontの設定が終わったので今度は僕のドメインである genenakagaki.com からアクセスできるようにします。これをするにはRoute 53とAWS Certificate Managerの設定をする必要があります。このセクションの終わりには自分のドメインからCloudFrontにアクセスすることができました。

Route 53を設定する!

自分のドメインからアクセスするにはRoute 53に genenakagaki.com をCloudFront distributionに指すように設定します。

Resources:
  ...

  WebsiteRoute53RecordSetGroup:
    Type: AWS::Route53::RecordSetGroup
    Properties:
      HostedZoneName: 'genenakagaki.com.'
      RecordSets:
      - Name: 'genenakagaki.com'
        # This is for IPv4
        Type: 'A'
        AliasTarget:
          DNSName: !GetAtt WebsiteCloudFrontDistribution.DomainName
          EvaluateTargetHealth: false
          # The  following HosteZoneId is always used for alias records pointing to CF.
          HostedZoneId: 'Z2FDTNDATAQYW2'
      - Name: 'genenakagaki.com'
        # Required for IPv6
        Type: 'AAAA'
        AliasTarget:
          DNSName: !GetAtt WebsiteCloudFrontDistribution.DomainName
          EvaluateTargetHealth: false
          # The  following HosteZoneId is always used for alias records pointing to CF.
          HostedZoneId: 'Z2FDTNDATAQYW2'

気をつけないといけないのが HostedZoneName です。最後のドット(.)は必要なんです。これがないとHosted Zoneが見つからないってエラーになります。Route 53 Record Set の設定の詳細はAWSのドキュメントに書いてます。僕の設定は genenakagaki.com へのIPv4IPv6のリクエストをCloudFront distributionにさすことです。

Route 53の設定は完了です。今度はHTTPSでリクエストできるようにします。

CloudFrontにHTTPSでリクエストできるようにする

最初にSSL証明書が必要です。

Parameters:
  HostedZoneId:
    Description: HostedZoneId for the domain e.g. Z23ABC4XYZL05B
    Type: String

Resources:
  WebsiteCertificate:
    Type: AWS::CertificateManager::Certificate
    Properties:
      DomainName: 'genenakagaki.com'
      DomainValidationOptions:
        - DomainName: 'genenakagaki.com'
          HostedZoneId: !Ref HostedZoneId
      ValidationMethod: DNS

Hosted Zoneを指定する必要がありますが、僕は同じドメインを複数のプロジェクトで使う予定なのでHosted ZoneはCloudFormationのテンプレートに入れないことにしました。

上で指定してるParameterはAWS Management ConsoleでCloudFormation stackを作るときに新しい入力ボックスを用意してくれます。こんな感じに。

2022-08-09_08-53-08_screenshot.png

これでSSL証明書が用意できたので今度はこれをCloudFrontに使うように設定します。

Resources:
  ...

  WebsiteCloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Aliases:
          - 'genenakagaki.com'
        ...
        ViewerCertificate:
          # さっき作ったSSL証明書
          AcmCertificateArn: !Ref WebsiteCertificate
          MinimumProtocolVersion: 'TLSv1.1_2016'
          SslSupportMethod: 'sni-only'

AliasesViewerCertificate を設定したら終わりです。これでhttps://genenakagaki.comにアクセスできます!やったね!