🌐

CloudFront で カスタム404エラーページを表示させるまでにつまずいた話

に公開

はじめに

CloudFrontのオリジンとしてS3とALBを使用する構成で、それぞれのオリジンから404エラーが返される動作を確認していたところ、2つの問題に遭遇しました。

1つ目はS3から403エラーが返される問題、2つ目はALB経由で表示が崩れる問題です。

同じ問題で困っている方の参考になれば幸いです🍀

なお、今回のインフラはTerraformで構築しており、コード例もTerraform形式で記載しています。

前提

構成は以下の通りです。

  • CloudFrontのオリジン構成

    • S3: 静的ファイルを配信
    • ALB: 動的コンテンツを配信
  • S3へのアクセス制御

    • OAC(Origin Access Control)でCloudFrontからのみアクセス可能
  • SSL/TLS証明書

    • ワイルドカード証明書(*.example.com)を使用
  • CloudFrontの基本設定

    • custom_error_responseで404エラー時に/404.htmlを返す設定済み

CloudFrontのカスタムエラー設定

resource "aws_cloudfront_distribution" "this" {
  # ...他の設定...

  # カスタム404エラーページ
  custom_error_response {
    error_code         = 404
    response_code      = 404
    response_page_path = "/404.html"
  }

  # ...他の設定...
}

つまずいたポイント

問題1: S3からの403エラーが返される

S3オリジンからの404エラーの動作確認のために、存在しないページにアクセスした際、期待していた404エラーではなく、403 Access Deniedが表示されてしまいました。

<Error>
  <Code>AccessDenied</Code>
  <Message>Access Denied</Message>
</Error>

原因

S3バケットポリシーで s3:GetObject のみを許可している場合の権限不足が原因です。

なぜ403エラーになるのか

s3:GetObjectだけでは、CloudFrontはファイルの取得はできますが、ファイルの存在確認(List操作)ができません。

  1. 存在しないファイル(例:/nonexistent.html)をリクエスト
  2. S3は「このファイルが存在するか」を確認する権限(s3:ListBucket)が与えられていない
  3. 権限がないため、403 Access Denied を返す

つまり、「ファイルが存在しない(404)」という情報を返す前に、「そもそも確認する権限がない(403)」というエラーになってしまいます。

解決方法

S3バケットポリシーに s3:ListBucket 権限を追加することで、404エラーが返されるようになります。

参考:CloudFrontでファイルがないエラーは404にしてほしいのに403で返ってくる

実装例(Terraform)

resource "aws_s3_bucket_policy" "static_website" {
  bucket = var.bucket_name

  policy = jsonencode({
    Version = "2008-10-17"
    Statement = [
      {
        Sid    = "AllowCloudFrontServicePrincipal"
        Effect = "Allow"
        Principal = {
          Service = "cloudfront.amazonaws.com"
        }
        Action   = "s3:GetObject"
        Resource = "${var.bucket_arn}/*"
        Condition = {
          StringEquals = {
            "AWS:SourceArn" = aws_cloudfront_distribution.this.arn
          }
        }
      },
      {
        Sid    = "AllowCloudFrontListBucket"
        Effect = "Allow"
        Principal = {
          Service = "cloudfront.amazonaws.com"
        }
        Action   = "s3:ListBucket"
        Resource = var.bucket_arn  # バケット自体(/*なし)
        Condition = {
          StringEquals = {
            "AWS:SourceArn" = aws_cloudfront_distribution.this.arn
          }
        }
      }
    ]
  })
}

この2つを正しく設定することで、S3は「ファイルの存在確認」と「ファイルの取得」の両方ができるようになります。

問題2: ALB経由での404エラー時に表示が崩れる

問題1を解決し、S3オリジンから正しく404.htmlが表示されることを確認しました。

次にALBオリジン(例:/products/nonexistent)から404エラーが返される動作を確認したところ、新たな問題が発生しました。

CSSやJavaScriptが読み込まれず、404ページの表示が崩れるという事象です。

CloudFrontの動作フロー

ALBから404エラーが返された場合も、CloudFrontのcustom_error_response設定により、以下の流れで処理されます。

つまり、どのオリジンから404エラーが返されても、最終的にはCloudFrontがS3の/404.htmlを配信します。

原因

404.html内でアセットファイルを相対パスで参照していたため、アクセスURLによってアセットのパスが変わってしまっていました。

相対パスの問題点

相対パス(./assets/error.css)は、現在のURLを基準にしてアセットのパスを解決します。

<!-- 相対パスの場合 -->
<link rel="stylesheet" href="./assets/error.css">

<!-- S3からの404の場合 -->
URL: https://www.example.com/nonexistent.html
→ 基準: https://www.example.com/
→ アセット: https://www.example.com/assets/error.css ✅

<!-- ALB経由の404の場合 -->
URL: https://www.example.com/products/nonexistent
→ 基準: https://www.example.com/products/
→ アセット: https://www.example.com/products/assets/error.css ❌

このように、エラーが発生したURLの階層によって、アセットファイルのパスが変わってしまうため、深い階層(/products/など)でエラーが発生すると、アセットファイルが見つからなくなります。

解決方法

アセットファイルのパスを絶対パス(ルート相対パス)に変更します。

絶対パス(ルート相対パス)の利点

絶対パス(/assets/error.css)は、ドメインのルートを基準にしてアセットのパスを解決します。

<!-- 修正前: 相対パス -->
<link rel="stylesheet" href="./assets/error.css">
<script src="./assets/jquery.min.js"></script>

<!-- 修正後: 絶対パス -->
<link rel="stylesheet" href="/assets/error.css">
<script src="/assets/jquery.min.js"></script>

これにより、どのURLで404エラーが発生しても、常に正しい場所からアセットを読み込むようになります。

最終的な構成

CloudFront
  ├─ Custom Error Response (404) → /404.html
  ├─ Origin: S3 (静的ファイル)
  │   ├─ Bucket Policy: s3:GetObject + s3:ListBucket
  │   ├─ 404.html
  │   └─ assets/(CSS、JS、画像など)
  └─ Origin: ALB (アプリケーション)

最後に

CloudFrontとS3のカスタムエラーページ設定は、一見シンプルに見えますが、実際に構築してみると、オリジンアクセスやパス解決のロジックなど、CDN特有の考慮事項が多くつまずきポイントが多かったです💦
 
この記事がどなたかの参考になれば幸いです。
えみり〜でした|ωΦ)ฅ

参考リンク

Discussion