Emacsでブログを作ったので公開する際にCloudFormationを使ってAWSにホスティングすることにしました。設定する際に一体何をしたのか理解するために、やったことをブレイクダウンしました。ちなみにこの記事のコードは僕のGitHub Repositoryにあります。
ウェブサイトのインフラ構成
ただの静的サイトにお金をかけたくなかったので、今回作ったサイトはお金があまりかからないようにデザインしました。もしお金が大量にかかるのであればNetlifyみたいに無料でホスティングできるサービスとかを使います。じゃなんでCloudFormationなんだって思いました?それはIaC (Infrastructure as Code)をやってみて学びたかったからです。あと僕のちょっとだけあるIaCの知識によればメンテナンスしやすくなるはずなのでそれも理由の一つです。
サイトに使ったAWSのサービス
インフラ構造は頭で想像はできていましたが、はっきりさせるためネットワーク図を作ってみることにしました。

下記のAWSサービスを使いました。
- Route 53 (DNS)
- CloudFront (CDN)
- S3 (HTMLファイルとかの保存場所)
- AWS Certificate Manager (図には載ってないけどSSL証明書)
AWS Serviceだけ見るととてもシンプルな作りになってます。中身がどうなってるのかもうちょっととどんな処理がされているのかを表してみました。

この構成でだとお金があまりかからないので必要としていたデザインになりました。払うのは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::StackName
はCloudFormation Pseudo ParameterというADSが用意してくれるパラメーターの一つです。結構使えそうですね。覚えておこう。
AWS Management ConsoleでCloudFormation stackを作成するときにこんな感じでStack nameを聞かれます。

僕は gene-website
って指定したので !Sub '${AWS::StackName}-root-bucket'
は gene-website-root-bucket
に変換されます。便利ですねー。
CloudFront Distributionを作る!
S3 bucketができたところで今度はCloudFrontを通してアクセスできるようにします。個人的に設定がたくさん入り組んでいて、ここは想像以上の難しさでした。わかりやすくするためにどんなAWS Resourceが必要なのかとそいつらがどういう関係なのかを表した図を作ってみました。図を作ったら自分でも結構理解が深まりました。作ってよかったー。

一気に設定みても難しいので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に設定するのには複数のリソースを正しく設定する必要があります。どんな設定が必要なのかをはっきりさせるためにこのパートで設定するものだけを図から切り離しました。

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
へのIPv4とIPv6のリクエストを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を作るときに新しい入力ボックスを用意してくれます。こんな感じに。

これで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'
Aliases
と ViewerCertificate
を設定したら終わりです。これでhttps://genenakagaki.comにアクセスできます!やったね!