はじめに
最近は静的サイトジェネレーター+静的サイトホスティングで安価に高性能なサイトを運営することが多いのではないかと思います。本ブログもHugoで生成したファイルをAWS CloudFront + Amazon S3でホスティングしています(2024年4月現在)。
本記事ではCloudFrontとS3を使ったインフラをAWS CloudFormationで記述します。なるべくシンプルに、かつなるべく現状で推奨されるセキュアな設定での構築を意識しています。
調査
まずは既存の記事を検索していろいろと調べてみます。
検索してまず目に留まるのがクラスメソッドさんによる記事。さすが全体がうまく整理されていて図も多くてわかりやすいですね。
今回私が書く記事はこの中の「2. S3 REST APIを使うパターン」に相当します。やはりアクセス経路がCloudFrontとS3の両方ある、かつHTTP (not SSL) の経路があるというのは気持ち悪さがあります。
以下の記事でもオリジンへのアクセスを禁止する構成です。
ですがどちらのパターンにおいても本記事のものとは少し違いがあり、本記事ではOAIではなくOACという仕組みを使っています。AWSの公式ドキュメントでも以下のように書かれています(2024年1月現在)。
OAC は以下をサポートしているため、OAC の使用をお勧めします。
― Amazon S3 オリジンへのアクセスの制限
- すべての AWS リージョンのすべての Amazon S3 バケット (2022 年 12 月以降に開始されたオプトインリージョンを含む)
- AWS KMS による Amazon S3 サーバー側の暗号化 (SSE-KMS)
- Amazon S3 に対する動的なリクエスト (PUT と DELETE)
続いてもクラスメソッドさんの記事。こちらも非常にわかりやすいです。
こちらはS3オブジェクトの暗号化にKMSを用いています。しかしS3バケットはデフォルトでKMSキーによる暗号化がされるようになったため、自前の鍵を使う場合などを除けばあえて自身でKMSキーのリソースを管理する必要はありません。
Amazon S3 では、Amazon S3 のすべてのバケットに対する基本レベルの暗号化として、Amazon S3 マネージドキーによるサーバー側の暗号化 (SSE-S3) が適用されるようになりました。2023 年 1 月 5 日以降、Amazon S3 にアップロードされるすべての新しいオブジェクトは、追加費用なしで、パフォーマンスに影響を与えずに自動的に暗号化されます。
― AWS KMS キーによるサーバー側の暗号化 (SSE-KMS) の使用
また本記事では個人ブログのデプロイを想定しているため独自ドメインの設定まで行うようにしています。S3からCloudFrontまですべて1つのテンプレートで済むようにも意識しています。
設計
構成図は以下の通りです。
Hugoなどで生成したコンテンツはS3バケットに配置し、それをCloudFront経由で配信します。
以下に今回の構成のポイントを記載します。
- 独自ドメインを使うためにAWS Certificate Manager (ACM) で証明書を発行する
- ドメインはRoute53に限らない
- ただし、サブドメインを使わない場合はトップでCNAMEを指定できるサービスである必要があります(本サイトではCloudflareで登録しています)
- S3バケットはap-northeast-1リージョンに配置する
- CloudFrontの設定は現時点(2023年12月)で推奨とされるものを優先的に利用
- Origin access identity ではなく Origin access control を利用
- Legacy cache setting ではなく Cache policy and origin request policy を利用
- ディレクトリ名でアクセスしたときにindex.htmlの内容を表示(Hugoではこの設定が必要)
AWSを利用する上で気になるのは料金かもしれません。CloudFrontやS3には毎月適用される無料枠があるため、よっぽど大ヒットしない限りは無料で運用できるかと思います。
- データ転送: 1TBまで
- リクエスト件数: 1千万件まで
- CloudFront Functions 呼び出し: 200万件まで
注意
Certificate Manager で発行する証明書はリージョンをus-east-1にする必要がありますが、今回の場合はS3バケットをなるべくエンドユーザーの近くに配置しておきたいため、ap-northeast-1にデプロイしたいです。1つのCloudFormationテンプレートでは1つのリージョンにしかデプロイできないため、今回は証明書を無理にコード管理する必要は無いと考え、事前にコンソールなどで作成しておくものとします。
もし複数リージョンにまたがって一度にデプロイしたい場合は以下の記事が参考になるかもしれません。
コード
以下のパラメータを受け取るテンプレートを作成しました。
DomainName
: サイトのドメイン。本サイトではrkd3.dev
となる。StaticContentBucketName
: 静的ファイルを置くS3バケット名。CertificateArn
: 事前にACMで発行した証明書のARN。
別サイトを同じテンプレートで作成しようとするとリソース名の重複が発生するため、別AWSアカウントを発行するか、名前を変更する必要がありますのでご注意ください。
AWSTemplateFormatVersion: '2010-09-09'
Description: CloudFormation template for serving static content using S3 and CloudFront.
Parameters:
DomainName:
Type: String
Description: The custom domain name for the CloudFront distribution.
StaticContentBucketName:
Type: String
Description: The backet name for put static contents.
CertificateArn:
Type: String
Description: The certificate ARN.
Resources:
StaticContentBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref StaticContentBucketName
S3BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref StaticContentBucket
PolicyDocument:
Statement:
- Sid: AllowCloudFrontServicePrincipal
Action:
- s3:GetObject
- s3:ListBucket
Effect: Allow
Resource:
- !Sub 'arn:aws:s3:::${StaticContentBucket}/*'
- !Sub 'arn:aws:s3:::${StaticContentBucket}'
Principal:
Service: cloudfront.amazonaws.com
Condition:
StringEquals:
AWS:SourceArn: !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution.Id}'
CloudFrontOAC:
Type: AWS::CloudFront::OriginAccessControl
Properties:
OriginAccessControlConfig:
Name: 'OAC'
OriginAccessControlOriginType: s3
SigningBehavior: always
SigningProtocol: sigv4
CachePolicy:
Type: AWS::CloudFront::CachePolicy
Properties:
CachePolicyConfig:
Name: !Sub 'caching-optimized-policy'
Comment: Custom cache policy similar to CachingOptimized
DefaultTTL: 86400
MaxTTL: 31536000
MinTTL: 1
ParametersInCacheKeyAndForwardedToOrigin:
CookiesConfig:
CookieBehavior: none
EnableAcceptEncodingBrotli: true
EnableAcceptEncodingGzip: true
HeadersConfig:
HeaderBehavior: none
QueryStringsConfig:
QueryStringBehavior: none
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Origins:
- DomainName: !GetAtt StaticContentBucket.RegionalDomainName
Id: S3Origin
OriginAccessControlId: !Ref CloudFrontOAC
S3OriginConfig: {}
Aliases:
- !Ref DomainName
Enabled: true
DefaultCacheBehavior:
Compress: true
CachePolicyId: !Ref CachePolicy
TargetOriginId: S3Origin
ViewerProtocolPolicy: redirect-to-https
ForwardedValues:
QueryString: false
Cookies:
Forward: none
FunctionAssociations:
- EventType: viewer-request
FunctionARN: !GetAtt IndexHtmlRedirectFunction.FunctionARN
DefaultRootObject: index.html
ViewerCertificate:
AcmCertificateArn: !Ref CertificateArn
SslSupportMethod: sni-only
MinimumProtocolVersion: TLSv1.2_2018
IndexHtmlRedirectFunction:
Type: AWS::CloudFront::Function
Properties:
Name: !Sub 'redirect-to-index-html'
AutoPublish: true
FunctionCode: |
function handler(event) {
var request = event.request;
var uri = request.uri;
if (uri.endsWith('/')) {
request.uri += 'index.html';
} else if (!uri.includes('.')) {
request.uri += '/index.html';
}
return request;
}
FunctionConfig:
Comment: Redirect to index.html for subdirectories
Runtime: cloudfront-js-2.0
Outputs:
S3BucketName:
Value: !Ref StaticContentBucket
Description: Name of the S3 bucket for static content.
CloudFrontDistributionDomainName:
Value: !GetAtt CloudFrontDistribution.DomainName
Description: Domain name of the CloudFront distribution.
デプロイ後の処理
デプロイ後にはOutputsで指定している CloudFrontDistributionDomainName
を、CNAMEレコードの値として登録する必要があります。
さいごに
これでS3に保存した静的サイトをCloudFront経由で配信することができるようになりました。AWSでのインフラ構築は一見取っつきづらいですが、それに見合うだけの柔軟性を持ったシステムが構築できます。ぜひ本記事をベースとしてより発展的なシステム構築に挑戦してみていただければと思います。