概要
S3 バケットをオリジンにした CloudFront で静的サイトを配信していると、index.html を新しいものに差し替えたのに、ブラウザには古いページが表示され続けることがあります。S3 のオブジェクトは確かに新しくなっていて、バケットポリシーも OAC も間違っていません。
原因は CloudFront のキャッシュです。CloudFront はエッジロケーションにオブジェクトをキャッシュし、有効期限(TTL)が切れるまでは S3 に取りに行かずにキャッシュを返します。オリジンが Cache-Control などのヘッダーを返さない場合、マネージドキャッシュポリシー CachingOptimized のデフォルト TTL は 24 時間です。つまり、何も設定していなければ、S3 を更新しても最大 24 時間は古い内容が返ることがあります。
この記事では、古い内容が返っているかをレスポンスヘッダー(X-Cache・Age)で見分ける方法と、新しい内容に切り替える 3 つの方法を整理します。
- キャッシュを無効化(コンソールの表示は「キャッシュ削除」)して、次のリクエストで S3 から取り直させる
- ファイル名やクエリ文字列にバージョンを入れて、新しい URL として配信する
Cache-Control: max-ageで、オブジェクトごとにキャッシュしてよい時間を決めておく
実践では、OAC 経由で S3 を配信している CloudFront で、更新した index.html が反映されない状態を実際に起こし、無効化で直します。続けて、max-age を付けたオブジェクトが無効化なしで切り替わること、新しいファイル名なら即座に新しい内容が返ることを確かめます。

クラウドおさるバナナ倉庫の中身を入れ替えたのに、麓の売店の棚には昨日のバナナが並んだままでござる。今日は、売店の棚をいつ入れ替えるかを決める仕組みを確かめるでござるよ。
この記事のメリット
- CloudFront が古い内容を返しているかを、
X-CacheとAgeのレスポンスヘッダーで確かめられるようになる - オリジンの
Cache-Controlとキャッシュポリシーの最小 TTL・最大 TTL・デフォルト TTL から、キャッシュされる時間を説明できるようになる - 無効化の指定方法(パス・ワイルドカード)と、無料枠・課金の数え方が分かる
- 無効化とファイル名のバージョニングを、更新の頻度やコストで使い分けられるようになる
CachingOptimizedではクエリ文字列がキャッシュキーに入らないため、?v=2を付けても切り替わらないことが分かる
執筆者とキャラクター紹介
執筆者:土肥(とひ)
株式会社ジェニュインの代表であり元エンジニア。これまでオンプレミスと AWS 環境のインフラ業務を担当。AWS 認定資格の学習で得た知識を、ハンズオンとともに解説するのが得意。直近では新人の教育も行っており、これから技術を学ぶ方が納得できる解説を心がけています。基礎編・Cloud Practitioner の記事では、解説役としても登場します。


クラウドおさる



はじめましてでござる。弊猿はクラウドおさる。IT インフラが異常に発達した猿山から、雲に乗って地上の AWS を学びに降りてきたのでござる。



基礎編や Cloud Practitioner の記事では、とひさんに素朴な疑問をぶつける役でござるな。Security - Specialty などの記事では、要所でワンポイントや注意点をひとこと添えるでござる。



猿山にもデータセンターや監視当番があって、地上で起きることはだいたい猿山にも同じ仕組みがあるのでござる。例え話がつい猿山に寄るのは、弊猿の癖でござる。
技術解説
CloudFront が古い内容を返す理由
CloudFront のエッジロケーションは、リクエストを受けるとまず自分のキャッシュを見ます。キャッシュにオブジェクトがあり、有効期限が切れていなければ、オリジンの S3 には問い合わせずにそのまま返します。S3 側でオブジェクトを上書きしても、CloudFront には何も通知されません。
有効期限が切れたオブジェクトへのリクエストが来ると、CloudFront は S3 に問い合わせます。このとき、キャッシュしているオブジェクトの ETag や Last-Modified を付けた条件付きリクエストを送ります。
| S3 の応答 | 意味 | CloudFront の動き |
|---|---|---|
304 Not Modified | キャッシュが最新 | 本文は受け取らず、キャッシュを使い続けて新しい有効期限を設定する |
200 OK | オブジェクトが更新されている | 新しいオブジェクトを受け取り、キャッシュを置き換える |
つまり「S3 を更新したのに反映されない」のは、まだ有効期限が切れていないからです。
キャッシュから返したかどうかは、レスポンスヘッダーで確かめられます。
| ヘッダー | 見方 |
|---|---|
X-Cache | Hit from cloudfront ならキャッシュから返している。Miss from cloudfront ならオリジンから取ってきた |
Age | キャッシュに入ってからの経過秒数。値が大きいほど、古い時点で取ってきた内容 |
CloudFront の標準ログでも、結果の種類として Hit(キャッシュから返した)・RefreshHit(期限切れだったのでオリジンに最新か確かめた)・Miss(オリジンに転送した)が記録されます。



X-Cache が Hit で Age が大きければ、売店の棚に昔から置いてあるバナナでござる。倉庫(S3)を疑う前に、まずこの 2 つのヘッダーを見るのでござるな。
キャッシュされる時間(TTL)の決まり方
キャッシュされる時間は、キャッシュポリシーの 3 つの TTL と、オリジンが返すヘッダーの組み合わせで決まります。S3 オリジンでは、オブジェクトのメタデータに Cache-Control を設定しておくと、S3 がそのヘッダーを返します。
マネージドキャッシュポリシーの CachingOptimized(キャッシュキーに含める値を最小限にして、キャッシュ効率を高めるポリシー)の設定は次のとおりです。
| 項目 | CachingOptimized の値 |
|---|---|
| 最小 TTL | 1 秒 |
| 最大 TTL | 31,536,000 秒(365 日) |
| デフォルト TTL | 86,400 秒(24 時間) |
| キャッシュキーに含まれるクエリ文字列 | なし |
| キャッシュキーに含まれる Cookie | なし |
| キャッシュキーに含まれるヘッダー | 正規化された Accept-Encoding のみ(Gzip と Brotli を別々にキャッシュするため) |
オリジンのヘッダーと TTL の関係は次のとおりです(最小 TTL が 0 より大きいとき)。
| オリジンが返すヘッダー | CloudFront がキャッシュする時間 |
|---|---|
| なし | 最小 TTL とデフォルト TTL の大きい方(CachingOptimized なら 24 時間) |
Cache-Control: max-age | max-age の値。ただし最小 TTL より短ければ最小 TTL、最大 TTL より長ければ最大 TTL に収められる |
Cache-Control: s-maxage(max-age と両方) | s-maxage の値を、最小 TTL と最大 TTL の範囲に収めたもの |
Cache-Control: no-cache / no-store / private | 最小 TTL の時間(CachingOptimized なら 1 秒はキャッシュされる) |
max-age はブラウザのキャッシュにも効きます。CloudFront とブラウザで時間を分けたいときは、CloudFront 向けに s-maxage、ブラウザ向けに max-age を指定します。





Cache-Control を付けずに置いたオブジェクトは、何も言わなくても 24 時間棚に並ぶのでござる。「設定していないからキャッシュされない」ではなく、「設定していないからデフォルト TTL」でござるぞ。
新しい内容に切り替える 3 つの方法
| 方法 | やること | 向いている場面 | 注意点 |
|---|---|---|---|
| 無効化(キャッシュ削除) | 無効化するパスを指定し、エッジのキャッシュを無効にする | たまにしか更新しない、URL を変えられない、誤った内容をすぐ消したい | 1 か月 1,000 パスを超えると課金。ブラウザや企業のプロキシのキャッシュは消えない |
| ファイル名のバージョニング | style.v2.css のように新しい名前で置き、参照するリンクを書き換える | 頻繁に更新する CSS・JS・画像 | 古いファイルが S3 に残る(その分の保存料金)。参照元(HTML)は短い TTL にしておく |
Cache-Control: max-age | オブジェクトごとにキャッシュしてよい時間を決める | 入口の index.html のように URL を変えられないファイル | 時間が切れるまでは古い内容が返る。短くするほどオリジンへの問い合わせが増える |
公式ドキュメントでは、ファイルを頻繁に更新する場合は、まずバージョン付きのファイル名を使うことが推奨されています。無効化の料金がかからず、どのバージョンを返すかを確実に制御でき、ロールバックもリンクを戻すだけで済むためです。
クエリ文字列でバージョンを切り替える方法(style.css?v=2)もありますが、キャッシュポリシーでクエリ文字列をキャッシュキーに含めている場合に限ります。デフォルトのキャッシュキーはドメイン名と URL パスだけです。CachingOptimized もクエリ文字列と Cookie は含めません(含めるのは、圧縮形式ごとにキャッシュを分けるための、正規化された Accept-Encoding ヘッダーだけです)。この場合、?v=1 と ?v=2 は同じキャッシュとして扱われます。
なお、ここでいうバージョン付きのファイル名は、S3 のバージョニング機能とは関係ありません。S3 のバージョニングを有効にしても、CloudFront のキャッシュは切り替わりません。
無効化のパスの指定と料金
無効化では、ディストリビューションのドメインからの相対パスを 1 行に 1 つ指定します。
| 指定 | 対象 |
|---|---|
/index.html | そのファイルだけ |
/images/* | images/ の下のすべてのファイルとサブディレクトリ |
/style.css* | style.css と、クエリ文字列付きのすべてのバージョン |
/* | ディストリビューションのすべてのファイル |
- パスは大文字と小文字を区別します
- ワイルドカード
*はパスの末尾にだけ置けます。/images/*.jpgのように途中に置いた*はワイルドカードになりません - コンソールでは先頭の
/を省略できます。API や AWS CLI では先頭に/が必要で、*を含むパスは"/*"のように引用符で囲みます - 無効化は開始後にキャンセルできません
料金は、無効化したファイルの数ではなくパスの数で決まります。1 か月に送信した無効化パスのうち最初の 1,000 件は無料で、超えた分はパス 1 件ごとに課金されます。/images/* や /* のようなワイルドカードは、対象のファイルが何千件あっても 1 パスと数えます。無料枠の 1,000 件は、アカウント内のすべてのディストリビューションの合計に対して適用されます。
実践
S3 を OAC 経由で配信している CloudFront で、更新した index.html が反映されない状態を起こし、無効化で直します。続けて、Cache-Control: max-age を付けたオブジェクトが無効化なしで切り替わること、クエリ文字列では切り替わらずファイル名なら切り替わることを確かめます。確認には AWS CloudShell の curl を使い、レスポンスヘッダーを見ます。
前提条件
- リージョン:
ap-northeast-1(東京) - S3・CloudFront・CloudShell の操作権限を持つ IAM ユーザーまたはロールでコンソールにログイン済みであること
以下のリソースが作成済みであることを前提とします。バケットはパブリックアクセスをすべてブロックしています。
まだ用意していない場合は、「作り方の参考」の記事の手順で作れます。リソース名は、この記事の名前に読み替えてください。
| リソース種別 | リソース名 | 内容 | 作り方の参考 |
|---|---|---|---|
| S3 バケット | cf-cache-lab-<アカウントID> | index.html(中身 <p>version 1</p>)と style.css(中身の末尾に /* v1 */)を配置。どちらも Cache-Control なし | Amazon S3 でバケットを作成しオブジェクトを管理する |
| CloudFront ディストリビューション | (説明 cf-cache-lab) | オリジンは上記バケット。オリジンアクセスコントロール cf-cache-lab-oac 経由。キャッシュポリシーはマネージドの CachingOptimized | Amazon CloudFront でコンテンツをキャッシュ配信する |
このハンズオンで作成・変更するものは以下のとおりです。
| 種別 | 名前 | 用途 |
|---|---|---|
| S3 オブジェクト(上書き) | index.html / style.css | 更新した内容が CloudFront に反映されるかを確かめる |
| S3 オブジェクト(追加) | style.v2.css | ファイル名のバージョニングを確かめる |
| キャッシュ削除(無効化) | /index.html | エッジのキャッシュを無効にする |
無効化パスは 1 か月 1,000 件まで無料で、超えた分は課金されます。この記事で使うのは数件ですが、/* を繰り返し試すような使い方はしないでください。CloudFront のデータ転送やリクエストにも料金がかかります(このハンズオンの量ではわずかです)。
手順 1: キャッシュポリシーの TTL を確かめる
AWS マネジメントコンソールにログインし、上部の検索バーに CloudFront と入力して、「CloudFront」を選択します。ディストリビューションの一覧で、説明が cf-cache-lab のディストリビューションを開き、「ディストリビューションドメイン名」を控えておきます。
左側ナビゲーションの「ポリシー」を選択し、「キャッシュ」タブの「Amazon マネージドポリシー」の一覧から CachingOptimized を開きます(詳細画面の見出しは Managed-CachingOptimized です)。最小 TTL が 1、最大 TTL が 31536000、デフォルト TTL が 86400 秒であること、キャッシュキーにクエリ文字列が含まれないことを確認します。


手順 2: CloudFront がキャッシュから返していることを確かめる
コンソール下部の「CloudShell」(または上部の CloudShell アイコン)をクリックして CloudShell を開き、次のコマンドを実行します。CF にはディストリビューションドメイン名、BUCKET にはバケット名が入ります。check は、指定したパスを CloudFront から取得して、本文と X-Cache・Age・Cache-Control のヘッダーを表示する関数です。
CF=$(aws cloudfront list-distributions --query "DistributionList.Items[?Comment=='cf-cache-lab'].DomainName" --output text)
BUCKET=cf-cache-lab-$(aws sts get-caller-identity --query Account --output text)
echo $CF $BUCKET
check() { curl -s -D /tmp/h -o /tmp/b "https://$CF/$1"; cat /tmp/b; grep -iE '^(x-cache|age|cache-control):' /tmp/h; }index.html を 3 回取得します。
check index.html; sleep 5; check index.html; sleep 5; check index.html1 回目は x-cache: Miss from cloudfront で、S3 から取ってきます。2 回目以降は x-cache: Hit from cloudfront になり、age ヘッダーにキャッシュに入ってからの秒数が表示されます。同じエッジロケーションでも、リクエストを受けたサーバーにキャッシュが無いうちは Miss になることがあるので、Hit になるまで繰り返します。cache-control ヘッダーは表示されません(オブジェクトに設定していないため)。


手順 3: S3 を更新しても古い内容が返ることを確かめる
index.html を version 2 に書き換えて S3 に上書きします。今回は Cache-Control: max-age=60(60 秒)を付けてアップロードします。
printf '<p>version 2</p>\n' > index.html
aws s3 cp index.html s3://$BUCKET/index.html --cache-control max-age=60
aws s3 cp s3://$BUCKET/index.html -
aws s3api head-object --bucket $BUCKET --key index.html --query CacheControlS3 のオブジェクトが version 2 になり、CacheControl が "max-age=60" になったことを確認します。続けて CloudFront から取得します。
check index.htmlS3 は version 2 なのに、CloudFront は version 1 を Hit from cloudfront で返します。age は手順 2 から増え続けています。キャッシュされている version 1 には Cache-Control が無かったため、デフォルト TTL の 24 時間が切れるまでこの状態が続きます。新しいオブジェクトに max-age=60 を付けても、すでにキャッシュされている古いオブジェクトの有効期限は変わりません。





倉庫のバナナに「1 分で下げてよし」と札を付けても、売店の棚に並んでいる昨日のバナナには札が付いていないのでござる。棚の古いものは、期限切れを待つか、下げる指示を出すしかないのでござるぞ。
手順 4: キャッシュを無効化する
CloudFront コンソールに戻り、cf-cache-lab のディストリビューションを開きます。「キャッシュ削除」タブを選択し、「キャッシュ削除を作成」をクリックします。
- 「選択方法」は既定の「パス別」のまま
- 「無効にするオブジェクトパス」:
/index.html


「キャッシュ削除を作成」をクリックします。ステータスが「進行中」から「完了済み」に変わるまで待ちます(今回は 20 秒足らずで完了しました。通常は 10〜100 秒ほどです)。


CloudShell に戻り、もう一度取得します。
check index.html; sleep 5; check index.html1 回目は Miss from cloudfront で version 2 が返り、cache-control: max-age=60 ヘッダーが付いています。2 回目は version 2 が Hit from cloudfront で返ります。無効化によって、次のリクエストで S3 から取り直したことが分かります。


手順 5: max-age を付けたオブジェクトは無効化なしで切り替わる
今度は無効化をせずに、index.html を version 3 に上書きします。
printf '<p>version 3</p>\n' > index.html
aws s3 cp index.html s3://$BUCKET/index.html --cache-control max-age=60
check index.html直後は、まだ 60 秒が経っていなければ version 2 が Hit from cloudfront で返ります。age は 60 より小さい値です。60 秒以上待ってから、もう一度取得します。
sleep 60; check index.htmlversion 3 が返ります。max-age=60 が切れたので、CloudFront が S3 に問い合わせ、更新されていた新しいオブジェクトを受け取ったためです。x-cache は Miss from cloudfront になり、age ヘッダーは付きません。TTL が切れたあとにオリジンへ問い合わせて、内容が変わっていたため新しいオブジェクトを取得し直したことを示します(内容が変わっていなければ RefreshHit from cloudfront になります)。


入口の index.html のように URL を変えられないファイルは、このように短い max-age を付けておくと、無効化しなくても決まった時間で新しい内容に切り替わります。
手順 6: クエリ文字列とファイル名のバージョニングを比べる
style.css をキャッシュに載せてから、中身を v2 に上書きします。
check style.css; sleep 5; check style.css
printf 'p { color: red; } /* v2 */\n' > style.css
aws s3 cp style.css s3://$BUCKET/style.css
check style.cssstyle.css は Cache-Control 無しでキャッシュされているので、上書きしても /* v1 */ が Hit from cloudfront で返ります。ここで、URL にクエリ文字列を付けて取得します。
check 'style.css?v=2'?v=2 を付けても、/* v1 */ が Hit from cloudfront で返ります。CachingOptimized はクエリ文字列をキャッシュキーに含めないため、style.css?v=2 は style.css と同じキャッシュとして扱われるからです。
次に、新しいファイル名 style.v2.css で置いて取得します。
aws s3 cp style.css s3://$BUCKET/style.v2.css
check style.v2.cssstyle.v2.css はまだ誰もリクエストしていない URL なので、Miss from cloudfront で S3 から取ってきて、/* v2 */ が返ります。無効化をしなくても、新しい URL ならすぐに新しい内容になります。


実際のサイトでは、index.html の <link> を style.v2.css に書き換えます。index.html を手順 5 のように短い max-age にしておけば、index.html が切り替わった時点で読者のブラウザは style.v2.css を読みに行きます。CSS や JS は長い TTL のままキャッシュさせ、入口の HTML だけを短くするのが、無効化に頼らない構成の基本です。



「S3 を更新したのに古い」と言われたら、まず TTL の間キャッシュが返っていないかを疑うのでござる。すぐ消すなら無効化、頻繁に変えるならファイル名のバージョニング、決まった時間で入れ替えたいなら Cache-Control でござる。S3 のバージョニングや、クエリ文字列をキャッシュキーに入れていないポリシーでの ?v=2 と取り違えないことでござるな。
まとめ
| 困りごと・要件 | 選ぶもの |
|---|---|
| 誤った内容をいますぐ差し替えたい・URL を変えられない | 無効化(キャッシュ削除)。ワイルドカードは 1 パスと数える |
| CSS・JS・画像を頻繁に更新する | ファイル名(またはディレクトリ名)のバージョニング |
index.html のように URL を変えられないファイルを、決まった時間で切り替えたい | Cache-Control: max-age(CloudFront だけなら s-maxage) |
| クエリ文字列でバージョンを切り替えたい | クエリ文字列をキャッシュキーに含めるキャッシュポリシー(CachingOptimized では切り替わらない) |
- CloudFront は TTL が切れるまでエッジのキャッシュを返す。S3 を更新しても CloudFront には通知されない
X-Cache: Hit from cloudfrontとAgeヘッダーで、キャッシュから返しているか・どれだけ古いかが分かる- オリジンが
Cache-Controlを返さなければデフォルト TTL(CachingOptimizedは 24 時間)でキャッシュされる。max-ageは最小 TTL と最大 TTL の範囲に収められる - 新しいオブジェクトに
max-ageを付けても、すでにキャッシュされている古いオブジェクトの有効期限は変わらない - 無効化は 1 か月 1,000 パスまで無料で、超えた分はパス単位で課金。開始後にキャンセルできず、ブラウザのキャッシュは消えない
- バージョン付きのファイル名は S3 のバージョニングとは別物。頻繁に更新するなら、無効化よりファイル名のバージョニングが推奨される
関連記事として、S3 オリジンを CloudFront 経由だけで読ませる設定は「CloudFront Origin Access Control(OAC)による S3 オリジン保護:OAI からの移行と推奨構成」で扱っています。
参照先
- CloudFront から、Amazon S3 の過去コンテンツが配信される原因を教えてください。 – AWS re:Post
- コンテンツをキャッシュに保持する期間 (有効期限) を管理する – Amazon CloudFront
- マネージドキャッシュポリシーを使用する – Amazon CloudFront
- キャッシュキーを理解する – Amazon CloudFront
- ファイルを無効化してコンテンツを削除する – Amazon CloudFront
- パスを無効化するときに知っておくべきこと – Amazon CloudFront
- ファイルを無効化する – Amazon CloudFront
- ファイルの無効化に対する支払い – Amazon CloudFront
- ファイルバージョニングを使用して CloudFront ディストリビューションでコンテンツを更新または削除する – Amazon CloudFront
- Amazon S3 オリジンに対するリクエストとレスポンスの動作 – Amazon CloudFront
- 標準ログ記録リファレンス – Amazon CloudFront













