こんにちは、広野です。
記事の概要はタイトル通りです。もはやこなれたソリューションだとは思うのですが、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 のカスタムエラーページを設定できたと思います。
よく使われる構成だと思いますので、本記事が皆様のお役に立てれば幸いです。