← 記事一覧に戻る

AWS S3 + CloudFront でキャッシュが更新されない・古いファイルが表示される時の原因と対処法【Cache-Control / Invalidation】

S3を更新したのにサイトが古いまま表示される

AWS S3 + CloudFront でウェブサイトをホスティングしていると、こんな状況に遭遇することがあります。

  • S3 にファイルをアップロードしたのに、サイトが古いバージョンのままで表示される
  • CloudFront のキャッシュをクリアしたはずなのに、まだ古いファイルが返ってくる
  • Invalidation(無効化)をかけたのに変化がない
  • 自分の環境では更新されているのに、ユーザーから「古い画面が出る」と言われる

この問題は「どのレイヤーのキャッシュが原因か」を特定しないと解決できません。CloudFront のキャッシュ・ブラウザキャッシュ・S3 の設定ミスがそれぞれ絡み合うため、対処法を知らないと無駄に時間を使います。本記事では原因ごとに対処法を体系的に解説します。

仕組みの前提:CloudFront はどうキャッシュを判断するか

CloudFront はオリジン(S3)からのレスポンスを受け取ったとき、以下の優先順位でキャッシュ TTL を決定します。

  1. CloudFront キャッシュポリシーの設定(最低・最大 TTL)
  2. S3 オブジェクトの Cache-Control ヘッダー
  3. S3 オブジェクトの Expires ヘッダー
  4. CloudFront のデフォルト TTL(未設定の場合は 24 時間)

これを知らずに「S3 を更新したから即座に反映されるはず」と思っていると、毎回ハマります。

原因1:CloudFront のデフォルト TTL が 24 時間のまま

もっともよくある原因です。CloudFront はデフォルトで 24 時間(86400 秒) オリジンのレスポンスをキャッシュします。Cache-Control ヘッダーを S3 に設定していない場合、このデフォルト値が使われます。

確認方法

# レスポンスヘッダーでキャッシュ状態を確認
curl -I https://d1234abcdef.cloudfront.net/index.html

レスポンスの X-Cache ヘッダーで状態がわかります。

X-Cache: Hit from cloudfront   ← CloudFront キャッシュから返却
X-Cache: Miss from cloudfront  ← S3(オリジン)から取得

Age ヘッダーはキャッシュされてからの経過秒数です。

対処法:キャッシュポリシーの TTL を調整する

CloudFront コンソール → ディストリビューション → ビヘイビア → 編集 → キャッシュポリシー で設定します。

開発環境では TTL を短くし、本番でも静的アセット以外は短めにするのが基本です。

# AWS CLI でキャッシュポリシーを確認
aws cloudfront list-cache-policies --type custom

原因2:S3 オブジェクトに Cache-Control ヘッダーが設定されていない

S3 にアップロードしたファイルのメタデータに Cache-Control が設定されていないと、CloudFront は独自の判断(デフォルト TTL)でキャッシュします。

確認方法

# S3 オブジェクトのメタデータを確認
aws s3api head-object --bucket your-bucket-name --key index.html

出力に ContentTypeCacheControl が含まれます。CacheControl が表示されていない場合は未設定です。

対処法:ファイルの種類に応じた Cache-Control を設定する

ファイルの役割によって最適な Cache-Control は異なります。

SPA の index.html(エントリーポイント)はキャッシュしない:

aws s3 cp dist/index.html s3://your-bucket/index.html \
  --cache-control "no-cache, no-store, must-revalidate" \
  --content-type "text/html"

ハッシュ付き静的アセット(main.abc123.js など)は長期キャッシュ OK:

aws s3 sync dist/assets/ s3://your-bucket/assets/ \
  --cache-control "public, max-age=31536000, immutable" \
  --exclude "*.html"

Vite や webpack はビルド時にファイル名にハッシュを付与するため、内容が変わればファイル名も変わります。そのため古いキャッシュが読まれることはなく、長期キャッシュが安全です。

GitHub Actions での自動デプロイ設定例:

- name: Deploy to S3
  run: |
    # index.html はキャッシュしない
    aws s3 cp dist/index.html s3://${{ secrets.S3_BUCKET }}/index.html \
      --cache-control "no-cache, no-store, must-revalidate" \
      --content-type "text/html"

    # ハッシュ付きアセットは長期キャッシュ
    aws s3 sync dist/assets/ s3://${{ secrets.S3_BUCKET }}/assets/ \
      --cache-control "public, max-age=31536000, immutable" \
      --delete

AWS CDK での設定例:

import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment'

// index.html:キャッシュなし
new s3deploy.BucketDeployment(this, 'DeployHtml', {
  sources: [s3deploy.Source.asset('./dist', { exclude: ['assets/**'] })],
  destinationBucket: bucket,
  distribution,
  distributionPaths: ['/*'],
  cacheControl: [
    s3deploy.CacheControl.fromString('no-cache, no-store, must-revalidate'),
  ],
})

// ハッシュ付きアセット:長期キャッシュ
new s3deploy.BucketDeployment(this, 'DeployAssets', {
  sources: [s3deploy.Source.asset('./dist/assets')],
  destinationBucket: bucket,
  destinationKeyPrefix: 'assets',
  cacheControl: [
    s3deploy.CacheControl.fromString('public, max-age=31536000, immutable'),
  ],
})

原因3:CloudFront Invalidation の使い方が間違っている

キャッシュを手動でクリアするには Invalidation(キャッシュ無効化)を使いますが、落とし穴があります。

よくある間違い①:パスの先頭スラッシュを忘れる

# ❌ 間違い:スラッシュなし → 対象にヒットしない
aws cloudfront create-invalidation \
  --distribution-id EXXXXXXXXXX \
  --paths "index.html"

# ✅ 正しい:先頭にスラッシュが必須
aws cloudfront create-invalidation \
  --distribution-id EXXXXXXXXXX \
  --paths "/index.html"

# ✅ 全キャッシュをクリアする場合
aws cloudfront create-invalidation \
  --distribution-id EXXXXXXXXXX \
  --paths "/*"

よくある間違い②:完了を待たずにアクセス確認する

Invalidation は 完了まで 1〜5 分 かかります。コマンド実行直後にアクセスしても、まだ古いキャッシュが返ってくることがあります。

# Invalidation の状態を確認
aws cloudfront get-invalidation \
  --distribution-id EXXXXXXXXXX \
  --id IXXXXXXXXXXXXXXXXXXXXXXXXXX

# "Status": "Completed" になるまで待つ
# "Status": "InProgress" はまだ処理中

CI/CD での自動 Invalidation

デプロイのたびに Invalidation を自動実行するのがベストプラクティスです。

- name: Invalidate CloudFront cache
  run: |
    INVALIDATION_ID=$(aws cloudfront create-invalidation \
      --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
      --paths "/*" \
      --query 'Invalidation.Id' \
      --output text)

    echo "Waiting for invalidation $INVALIDATION_ID to complete..."
    aws cloudfront wait invalidation-completed \
      --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
      --id "$INVALIDATION_ID"

    echo "Invalidation completed."

コスト注意: Invalidation は月 1,000 パス分まで無料、それ以降は有料です。/* は1パスとしてカウントされるため、毎回全キャッシュをクリアしても費用はほぼかかりません。

原因4:ブラウザキャッシュと CloudFront キャッシュを混同している

「CloudFront のキャッシュはクリアした」と思っていても、実はブラウザキャッシュが効いているケースがあります。

確認方法

Chrome DevTools(F12)→ Network タブ → 「キャッシュを無効化」にチェックしてリロード。

または、シークレットモード(Ctrl+Shift+N)でアクセスしてみてください。シークレットモードではブラウザキャッシュが使われません。

レスポンスヘッダーでも判断できます。

# CloudFront がキャッシュを返している(HIT)
X-Cache: Hit from cloudfront
Age: 3600

# S3 から直接取得(MISS)
X-Cache: Miss from cloudfront
Age: 0

ブラウザキャッシュが原因の場合は、CloudFront の設定ではなく Cache-Control: no-cache ヘッダーで制御します。

原因5:S3 バケットポリシーと OAC の設定ミス

まれに、S3 のアクセス制御の問題で CloudFront が最新ファイルを取得できず、古いキャッシュを返し続けることがあります。

CloudFront の OAC(Origin Access Control)を使用している場合、S3 バケットポリシーが正しくないと CloudFront が 403 エラーを受け取ります。このとき CloudFront はエラーをキャッシュしてしまうことがあります。

S3 バケットポリシーの正しい設定(OAC 使用時)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCloudFrontServicePrincipal",
      "Effect": "Allow",
      "Principal": {
        "Service": "cloudfront.amazonaws.com"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::your-bucket-name/*",
      "Condition": {
        "StringEquals": {
          "AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/EXXXXXXXXXX"
        }
      }
    }
  ]
}

AWS:SourceArn にはディストリビューションの ARN を正確に指定してください(アカウント ID とディストリビューション ID は環境に合わせて変更)。

確認方法

# バケットポリシーを確認
aws s3api get-bucket-policy --bucket your-bucket-name

# CloudFront がオリジンから正常に取得できているか確認
# エラーページが返っていないかチェック
curl -v https://d1234abcdef.cloudfront.net/index.html 2>&1 | grep "< HTTP"

原因6:エラーレスポンスのキャッシュ(Error Caching TTL)

CloudFront は 4xx・5xx のエラーレスポンスもキャッシュします。デフォルトのエラーキャッシュ TTL は 5 分 です。

S3 の設定ミスで一度 403 や 404 が返ったあと、設定を直しても 5 分間は古いエラーレスポンスが返り続けます。

確認・対処法

CloudFront コンソール → エラーページ → カスタムエラーレスポンスを設定し、エラーキャッシュ TTL を短くするか 0 にします。

# AWS CLI でエラーキャッシュの設定を確認(distribution config を取得)
aws cloudfront get-distribution-config --id EXXXXXXXXXX \
  --query 'DistributionConfig.CustomErrorResponses'

まとめ:キャッシュが更新されない時のチェックリスト

確認項目 確認方法
CloudFront キャッシュ状態 curl -I URLX-Cache を確認
S3 の Cache-Control ヘッダー aws s3api head-object
Invalidation の完了 コンソールまたは aws cloudfront get-invalidation
ブラウザキャッシュ DevTools で「キャッシュを無効化」してリロード
S3 バケットポリシー aws s3api get-bucket-policy
エラーレスポンスのキャッシュ CloudFront コンソールのエラーページ設定

推奨 Cache-Control 設定まとめ

ファイル種別 Cache-Control 理由
index.html no-cache, no-store, must-revalidate 常に最新を取得させる
ハッシュ付き JS/CSS public, max-age=31536000, immutable 内容変化でファイル名も変わるため安全
ハッシュなし JS/CSS no-cache または短い TTL 変更が反映されないリスクを避ける
画像・フォント public, max-age=86400 更新頻度に合わせて調整

CloudFront のキャッシュ問題は「どのレイヤーのキャッシュか」を特定することが解決への近道です。curl -I でヘッダーを確認し、X-CacheAge を見れば CloudFront が原因かブラウザが原因か切り分けられます。上記のチェックリストを順に確認すれば、ほとんどのケースで原因を特定できます。