S3を更新したのにサイトが古いまま表示される
AWS S3 + CloudFront でウェブサイトをホスティングしていると、こんな状況に遭遇することがあります。
- S3 にファイルをアップロードしたのに、サイトが古いバージョンのままで表示される
- CloudFront のキャッシュをクリアしたはずなのに、まだ古いファイルが返ってくる
- Invalidation(無効化)をかけたのに変化がない
- 自分の環境では更新されているのに、ユーザーから「古い画面が出る」と言われる
この問題は「どのレイヤーのキャッシュが原因か」を特定しないと解決できません。CloudFront のキャッシュ・ブラウザキャッシュ・S3 の設定ミスがそれぞれ絡み合うため、対処法を知らないと無駄に時間を使います。本記事では原因ごとに対処法を体系的に解説します。
仕組みの前提:CloudFront はどうキャッシュを判断するか
CloudFront はオリジン(S3)からのレスポンスを受け取ったとき、以下の優先順位でキャッシュ TTL を決定します。
- CloudFront キャッシュポリシーの設定(最低・最大 TTL)
- S3 オブジェクトの Cache-Control ヘッダー
- S3 オブジェクトの Expires ヘッダー
- 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
出力に ContentType や CacheControl が含まれます。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 URL で X-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-Cache と Age を見れば CloudFront が原因かブラウザが原因か切り分けられます。上記のチェックリストを順に確認すれば、ほとんどのケースで原因を特定できます。
