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操作)ができません。
- 存在しないファイル(例:
/nonexistent.html
)をリクエスト - S3は「このファイルが存在するか」を確認する権限(
s3:ListBucket
)が与えられていない - 権限がないため、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