Amazon CloudFront と Amazon S3 の WEB サイトにカスタムエラーページを追加する [AWS CloudFormation]

こんにちは、広野です。

記事の概要はタイトル通りです。もはやこなれたソリューションだとは思うのですが、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 のカスタムエラーページを設定できたと思います。

よく使われる構成だと思いますので、本記事が皆様のお役に立てれば幸いです。

著者について
広野 祐司

AWS サーバーレスアーキテクチャを駆使して社内クラウド人材育成アプリとコンテンツづくりに勤しんでいます。React で SPA を書き始めたら快適すぎて、他の言語には戻れなくなりました。サーバーレス & React 仲間を増やしたいです。AWSは好きですが、それよりもAWSすげー!って気持ちの方が強いです。
取得資格:AWS 認定は12資格、ITサービスマネージャ、ITIL v3 Expert 等
2020 - 2023 Japan AWS Top Engineer 受賞
2022 - 2023 Japan AWS Ambassador 受賞
2023 当社初代フルスタックエンジニア認定
好きなAWSサービス:AWS Amplify / AWS AppSync / Amazon Cognito / AWS Step Functions / AWS CloudFormation

広野 祐司をフォローする
クラウドに強いによるエンジニアブログです。
SCSKは専門性と豊富な実績を活かしたクラウドサービス USiZE(ユーサイズ)を提供しています。
USiZEサービスサイトでは、お客様のDX推進をワンストップで支援するサービスの詳細や導入事例を紹介しています。
AWSクラウドクラウドセキュリティソリューション
シェアする
タイトルとURLをコピーしました