こんにちは、広野です。
記事の概要はタイトル通りです。もはやこなれたソリューションだとは思うのですが、AWS CloudFormation とセットで扱っている記事は少ないようだったので上げておこうと思います。先日、本件が入用になって作成したテンプレートを簡略化しました。
やりたいこと
- Amazon S3 バケットをオリジンとした AWS CloudFront の WEB サイトに、404 などの HTTP エラーステータスが出てしまったときに表示する「カスタムエラーページ」を設定します。
- この対応の意義の詳細説明は割愛しますが、ユーザーの混乱を避けたり、悪意のあるユーザーに不必要に情報を与えない効果があります。
本記事でのコンテンツ、カスタムエラーページはかなり簡略化しています。ブログ用のサンプルですので。
- コンテンツは index.html のみ
- カスタムエラーページは error.html のみ
※エラーコードは複数あり、本来はそれごとにもしくは種類ごとにまとめた複数のエラーページを用意します。
アーキテクチャ
- Amazon CloudFront には Amazon S3 のオリジンを 2つ設定します。1つはコンテンツ配信用、もう1つはカスタムエラーページ用です。アクセス制御設定が特に無ければコンテンツ配信用バケットとカスタムエラーページ用バケットを統合することはできるのですが、コンテンツ配信用バケットに異常があったときのことを考慮すると、別バケットに分けておくことがベストプラクティスだそうです。
- Amazon CloudFront にユーザーがアクセスしたとき、デフォルトではコンテンツ配信用バケットに誘導されます。
- サイトへのアクセスでエラーステータスを返したときには、カスタムエラーページ用バケット内、error.html に誘導します。Amazon CloudFront にそのようなルールを書きます。
- Amazon S3 バケットは、OAC により Amazon CloudFront からのアクセスしか受け付けないようにしています。
AWS CloudFormation テンプレート
少々長いですが、最後に少し説明を入れています。本記事と関係ない部分の設定は適当に作成しております。
AWSTemplateFormatVersion: 2010-09-09
Description: The CloudFormation template that creates a S3 bucket, a CloudFront distribution with a custom error page.
# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
SystemName:
Type: String
Description: System name
Default: example999
MaxLength: 10
MinLength: 1
Resources:
# ------------------------------------------------------------#
# S3
# ------------------------------------------------------------#
S3BucketContents:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${SystemName}-contents
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
Tags:
- Key: Cost
Value: !Ref SystemName
S3BucketContentsPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref S3BucketContents
PolicyDocument:
Version: "2012-10-17"
Statement:
- Action:
- "s3:GetObject"
Effect: Allow
Resource: !Sub "arn:aws:s3:::${S3BucketContents}/*"
Principal:
Service: cloudfront.amazonaws.com
Condition:
StringEquals:
AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}
DependsOn:
- S3BucketContents
- CloudFrontDistribution
S3BucketErrorPage:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${SystemName}-errorpage
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
Tags:
- Key: Cost
Value: !Ref SystemName
S3BucketPolicyErrorPage:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref S3BucketErrorPage
PolicyDocument:
Version: "2012-10-17"
Statement:
- Action:
- "s3:GetObject"
Effect: Allow
Resource: !Sub "arn:aws:s3:::${S3BucketErrorPage}/*"
Principal:
Service: cloudfront.amazonaws.com
Condition:
StringEquals:
AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}
DependsOn:
- S3BucketErrorPage
- CloudFrontDistribution
S3BucketLogs:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${SystemName}-logs
OwnershipControls:
Rules:
- ObjectOwnership: BucketOwnerPreferred
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
Tags:
- Key: Cost
Value: !Ref SystemName
# ------------------------------------------------------------#
# CloudFront
# ------------------------------------------------------------#
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Enabled: true
Comment: !Sub CloudFront distribution for ${SystemName}
HttpVersion: http2
IPV6Enabled: true
PriceClass: PriceClass_200
Logging:
Bucket: !GetAtt S3BucketLogs.DomainName
IncludeCookies: false
Prefix: cloudfrontAccesslog/
DefaultCacheBehavior:
TargetOriginId: !Sub S3Origin-${S3BucketContents}
ViewerProtocolPolicy: redirect-to-https
AllowedMethods:
- GET
- HEAD
CachedMethods:
- GET
- HEAD
CachePolicyId: !Ref CloudFrontCachePolicy
OriginRequestPolicyId: !Ref CloudFrontOriginRequestPolicy
Compress: true
SmoothStreaming: false
CacheBehaviors:
- TargetOriginId: !Sub S3Origin-${S3BucketErrorPage}
ViewerProtocolPolicy: redirect-to-https
AllowedMethods:
- GET
- HEAD
CachedMethods:
- GET
- HEAD
CachePolicyId: !Ref CloudFrontCachePolicy
OriginRequestPolicyId: !Ref CloudFrontOriginRequestPolicy
Compress: true
PathPattern: error.html
SmoothStreaming: false
Origins:
- Id: !Sub S3Origin-${S3BucketContents}
DomainName: !Sub ${S3BucketContents}.s3.${AWS::Region}.amazonaws.com
S3OriginConfig:
OriginAccessIdentity: ""
OriginAccessControlId: !GetAtt CloudFrontOriginAccessControl.Id
ConnectionAttempts: 3
ConnectionTimeout: 10
- Id: !Sub S3Origin-${S3BucketErrorPage}
DomainName: !Sub ${S3BucketErrorPage}.s3.${AWS::Region}.amazonaws.com
S3OriginConfig:
OriginAccessIdentity: ""
OriginAccessControlId: !GetAtt CloudFrontOriginAccessControl.Id
ConnectionAttempts: 3
ConnectionTimeout: 10
DefaultRootObject: index.html
ViewerCertificate:
CloudFrontDefaultCertificate: true
CustomErrorResponses:
- ErrorCachingMinTTL: 10
ErrorCode: 400
ResponseCode: 400
ResponsePagePath: /error.html
- ErrorCachingMinTTL: 10
ErrorCode: 403
ResponseCode: 403
ResponsePagePath: /error.html
- ErrorCachingMinTTL: 10
ErrorCode: 404
ResponseCode: 404
ResponsePagePath: /error.html
- ErrorCachingMinTTL: 10
ErrorCode: 405
ResponseCode: 405
ResponsePagePath: /error.html
- ErrorCachingMinTTL: 10
ErrorCode: 414
ResponseCode: 414
ResponsePagePath: /error.html
- ErrorCachingMinTTL: 10
ErrorCode: 416
ResponseCode: 416
ResponsePagePath: /error.html
- ErrorCachingMinTTL: 10
ErrorCode: 500
ResponseCode: 500
ResponsePagePath: /error.html
- ErrorCachingMinTTL: 10
ErrorCode: 501
ResponseCode: 501
ResponsePagePath: /error.html
- ErrorCachingMinTTL: 10
ErrorCode: 502
ResponseCode: 502
ResponsePagePath: /error.html
- ErrorCachingMinTTL: 10
ErrorCode: 503
ResponseCode: 503
ResponsePagePath: /error.html
- ErrorCachingMinTTL: 10
ErrorCode: 504
ResponseCode: 504
ResponsePagePath: /error.html
Tags:
- Key: Cost
Value: !Ref SystemName
DependsOn:
- CloudFrontCachePolicy
- CloudFrontOriginRequestPolicy
- CloudFrontOriginAccessControl
- S3BucketContents
- S3BucketErrorPage
CloudFrontOriginAccessControl:
Type: AWS::CloudFront::OriginAccessControl
Properties:
OriginAccessControlConfig:
Description: !Sub CloudFront OAC for ${SystemName}
Name: !Sub OriginAccessControl-${SystemName}
OriginAccessControlOriginType: s3
SigningBehavior: always
SigningProtocol: sigv4
CloudFrontCachePolicy:
Type: AWS::CloudFront::CachePolicy
Properties:
CachePolicyConfig:
Name: !Sub CachePolicy-${SystemName}
Comment: !Sub CloudFront Cache Policy for ${SystemName}
DefaultTTL: 3600
MaxTTL: 86400
MinTTL: 60
ParametersInCacheKeyAndForwardedToOrigin:
CookiesConfig:
CookieBehavior: none
EnableAcceptEncodingBrotli: true
EnableAcceptEncodingGzip: true
HeadersConfig:
HeaderBehavior: whitelist
Headers:
- Access-Control-Request-Headers
- Access-Control-Request-Method
- Origin
- referer
- user-agent
QueryStringsConfig:
QueryStringBehavior: none
CloudFrontOriginRequestPolicy:
Type: AWS::CloudFront::OriginRequestPolicy
Properties:
OriginRequestPolicyConfig:
Name: !Sub OriginRequestPolicy-${SystemName}
Comment: !Sub CloudFront Origin Request Policy for ${SystemName}
CookiesConfig:
CookieBehavior: none
HeadersConfig:
HeaderBehavior: whitelist
Headers:
- Access-Control-Request-Headers
- Access-Control-Request-Method
- Origin
- referer
- user-agent
QueryStringsConfig:
QueryStringBehavior: none
# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#
Outputs:
#CloudFront
CloudFrontOriginalDomain:
Value: !GetAtt CloudFrontDistribution.DomainName
- Amazon CloudFront に 2つ以上のオリジンを設定するときには、メイン以外のオリジンに対するふるまい (behavior) を CacheBehaviors のところで設定します。ここに、PathPattern: error.html と書いていますが error.html への通信であればこのオリジンに誘導しなさいよ、という意味のルールになります。ルールにはワイルドカードも使えます。AWS 公式ドキュメントによると先頭にスラッシュを入れても入れなくても動くそうです。本記事の構成ではバケット直下に error.html を保存していますのでディレクトリ構造は書いていませんが、必要に応じて入れましょう。
- CustomeErrorResponses: の部分に、エラーステータスコード単位でどのカスタムエラーページを使用するかを定義できます。当然このエラーページ用 html がカスタムエラーページ用バケットに存在し、上述 PathPattern のルールにマッチする必要があります。この記述では、先頭のスラッシュは必須です。
- カスタムエラーページを表示させる際に、エラーコードをオーバーライドすることができますが、本記事ではしていません。オリジナルのコードをそのままユーザーに返しています。必要に応じて書き換えましょう。
サンプル HTML
サンプルコンテンツ、サンプルカスタムエラーページの HTML も用意しておきます。(ChatGPT 様に作ってもらったものですw)
これらは、Amazon S3 のコンテンツ配信用バケット、カスタムエラーページ用バケットのそれぞれ直下に配置します。
- index.html
<!DOCTYPE html>
<html>
<head>
<title>Welcome to My Website</title>
</head>
<body>
<h1>Welcome!</h1>
<p>This is a simple test page for your website.</p>
</body>
</html>
- error.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Error</title>
<style>
body {
text-align: center;
font-family: 'Arial', sans-serif;
margin-top: 50px;
}
h1 {
font-size: 48px;
color: #333;
}
p {
font-size: 22px;
color: #666;
}
</style>
</head>
<body>
<h1>Custom Error Page</h1>
</body>
</html>
実際の動作
実際にこのテンプレートでデプロイしたものにアクセスしてみます。テンプレートを流すと、最後「出力」欄に Amazon CloudFront の URL が表示されますので、そこにアクセスします。
通常のコンテンツ (index.html) が表示されましたね。
存在しないページ (ドメイン名 + /xxx/xxx.html) にアクセスしてみます。404 (Not Found) のエラーステータスコードが返ってくるので、カスタムエラーページが表示されましたね。本記事の設定をしていないと、Amazon CloudFront のデフォルトのエラー表示がされることになります。
AWS マネジメントコンソールで Amazon CloudFront のカスタムエラーページ設定もされていることが確認できます。
本記事ではカスタムエラーページをテキストのみの簡易な画面にしましたが、通常は WEB サイトのデザインに統一することが多いと思います。カスタムエラーページ内で画像などにリンクする場合は相対パスが使用できないので注意が必要です。(弊社川原のブログ参照)
まとめ
いかがでしたでしょうか。
簡単な構成でしたが、AWS CloudFormation で Amazon CloudFront のカスタムエラーページを設定できたと思います。
よく使われる構成だと思いますので、本記事が皆様のお役に立てれば幸いです。