メインコンテンツへスキップ

CloudFront + OACで個別ページが404になる問題の解決

·
インフラ AWS トラブルシューティング 実践
目次

今日学んだこと
#

GitHub PagesからAWS(S3 + CloudFront)へブログを移行した際、トップページは表示されるが個別記事ページで404エラーが発生しました。原因はOAC(Origin Access Control)使用時にS3の静的ウェブサイトホスティング機能が使えず、/posts/article//posts/article/index.html の自動補完が効かないことでした。CloudFront Functionでviewer-request時にURLリライトを実装して解決しました。

学習内容
#

問題の発生状況
#

GitHub Pagesで正常に動作していたHugoブログをAWSに移行したところ、以下の症状が発生しました。

ページURL結果
トップhttps://example.com/✅ 正常表示
記事一覧https://example.com/posts/❌ 404エラー
個別記事https://example.com/posts/article-name/❌ 404エラー

原因の特定
#

S3へのアクセス方式の違いが原因でした。

方式index.html自動補完セキュリティ
静的ウェブサイトホスティング✅ あり❌ パブリック公開が必要
OAC経由(今回の構成)❌ なし✅ S3は非公開のまま

OACを使用する場合、S3はREST APIエンドポイントとしてアクセスされます。この場合、/posts/ というリクエストは「posts/というオブジェクト」を探しに行くため、posts/index.html は見つかりません。

解決策の比較
#

解決策実行場所レイテンシコスト採用
CloudFront Functionエッジ極小無料枠大
Lambda@Edgeリージョン従量課金-
S3静的ホスティング---❌ OAC不可

CloudFront Functionを採用した理由は、単純なURLリライトには十分な機能があり、無料枠が大きく(月200万リクエスト)、エッジで実行されるためレイテンシへの影響が最小限だからです。

解決策:CloudFront FunctionによるURLリライト
#

viewer-requestイベントでURLを書き換え、末尾に index.html を付与します。

function handler(event) {
  var request = event.request;
  var uri = request.uri;
  
  // 末尾が `/` の場合 → `/index.html` を付与
  if (uri.endsWith('/')) {
    request.uri += 'index.html';
  }
  // 拡張子がない場合 → `/index.html` を付与
  else if (!uri.includes('.')) {
    request.uri += '/index.html';
  }
  
  return request;
}

処理フローの図解
#

リクエスト: /posts/article/
    ↓
CloudFront Function(viewer-request)
    ↓
URI書き換え: /posts/article/index.html
    ↓
S3からオブジェクト取得
    ↓
レスポンス: 200 OK

Terraformでの実装
#

resource "aws_cloudfront_function" "rewrite" {
  name    = "url-rewrite"
  runtime = "cloudfront-js-2.0"
  publish = true
  code    = <<-EOF
    function handler(event) {
      var request = event.request;
      var uri = request.uri;
      
      if (uri.endsWith('/')) {
        request.uri += 'index.html';
      }
      else if (!uri.includes('.')) {
        request.uri += '/index.html';
      }
      
      return request;
    }
  EOF
}

CloudFront Distributionの default_cache_behavior 内で、このFunctionを viewer-request イベントに関連付けます。

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

  default_cache_behavior {
    # ... 他の設定 ...

    function_association {
      event_type   = "viewer-request"
      function_arn = aws_cloudfront_function.rewrite.arn
    }
  }
}

デプロイと動作確認
#

1. Terraformでデプロイ
#

terraform plan   # 変更内容を確認
terraform apply  # 適用

2. CloudFrontキャッシュの無効化
#

設定変更後、キャッシュが残っていると古い挙動が続く場合があります。

aws cloudfront create-invalidation \
  --distribution-id <DISTRIBUTION_ID> \
  --paths "/*"

3. 動作確認
#

# 末尾スラッシュありのURL
curl -I https://example.com/posts/

# 末尾スラッシュなしのURL
curl -I https://example.com/posts/article-name

いずれも HTTP/2 200 が返れば成功です。

CloudFront Function vs Lambda@Edge
#

項目CloudFront FunctionLambda@Edge
実行場所全エッジロケーションリージョンエッジキャッシュ
最大実行時間1ms5秒(viewer)/ 30秒(origin)
メモリ2MB128MB〜10GB
ネットワークアクセス❌ 不可✅ 可能
対応イベントviewer-request/response4種類すべて
料金月200万リクエスト無料リクエスト+実行時間課金

単純なURLリライトであればCloudFront Functionで十分です。外部APIへのアクセスや複雑な処理が必要な場合はLambda@Edgeを検討してください。

まとめ
#

  • OAC使用時はS3の「index.html自動補完」が効かない
  • CloudFront Functionでviewer-request時にURLリライトを実装して解決
  • 単純なURL書き換えにはCloudFront Functionが最適(低コスト・低レイテンシ)
  • 設定変更後はキャッシュ無効化を忘れずに実行

参考
#