CloudFront で S3 の更新が反映されないときの確認ポイント ── キャッシュの無効化と TTL

目次

概要

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 を付けたオブジェクトが無効化なしで切り替わること、新しいファイル名なら即座に新しい内容が返ることを確かめます。

S3 を更新しても、エッジは TTL が切れるまで古い内容を返す
S3 を更新しても、エッジは TTL が切れるまで古い内容を返す
クラウドおさる

バナナ倉庫の中身を入れ替えたのに、麓の売店の棚には昨日のバナナが並んだままでござる。今日は、売店の棚をいつ入れ替えるかを決める仕組みを確かめるでござるよ。

この記事のメリット

  • 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-CacheHit 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 の値
最小 TTL1 秒
最大 TTL31,536,000 秒(365 日)
デフォルト TTL86,400 秒(24 時間)
キャッシュキーに含まれるクエリ文字列なし
キャッシュキーに含まれる Cookieなし
キャッシュキーに含まれるヘッダー正規化された Accept-Encoding のみ(Gzip と Brotli を別々にキャッシュするため)

オリジンのヘッダーと TTL の関係は次のとおりです(最小 TTL が 0 より大きいとき)。

オリジンが返すヘッダーCloudFront がキャッシュする時間
なし最小 TTL とデフォルト TTL の大きい方(CachingOptimized なら 24 時間)
Cache-Control: max-agemax-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 とキャッシュポリシーの TTL で決まる
キャッシュされる時間は、オリジンの Cache-Control とキャッシュポリシーの TTL で決まる
クラウドおさる

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 件は、アカウント内のすべてのディストリビューションの合計に対して適用されます。

新卒・未経験の方へ
クラウドおさる

AWS を仕事にしたい方へ ── 新卒・未経験から

この記事を書いているのは、AWS の設計・構築を仕事にしている現役エンジニアです。運営元の株式会社ジェニュインでは、新卒・未経験の方も募集しています。異業種や文系からインフラエンジニアになり活躍しているメンバーもいます。

  • ✓システム開発・WEB エンジニア / インフラ構築などの職種で、未経験から応募できます
  • ✓未経験から入社した社員の声を含む社員アンケートを掲載
  • ✓応募フォームから 1 分で応募できます

実践

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 経由。キャッシュポリシーはマネージドの CachingOptimizedAmazon 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 秒であること、キャッシュキーにクエリ文字列が含まれないことを確認します。

Managed-CachingOptimized の詳細画面(最小・最大・デフォルト TTL とキャッシュキーの設定)
Managed-CachingOptimized の詳細画面(最小・最大・デフォルト TTL とキャッシュキーの設定)

手順 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.html

1 回目は x-cache: Miss from cloudfront で、S3 から取ってきます。2 回目以降は x-cache: Hit from cloudfront になり、age ヘッダーにキャッシュに入ってからの秒数が表示されます。同じエッジロケーションでも、リクエストを受けたサーバーにキャッシュが無いうちは Miss になることがあるので、Hit になるまで繰り返します。cache-control ヘッダーは表示されません(オブジェクトに設定していないため)。

CloudShell で check index.html を繰り返した結果(version 1、Hit from cloudfront、age が表示されている)
CloudShell で check index.html を繰り返した結果(version 1、Hit from cloudfront、age が表示されている)

手順 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 CacheControl

S3 のオブジェクトが version 2 になり、CacheControl が "max-age=60" になったことを確認します。続けて CloudFront から取得します。

check index.html

S3 は version 2 なのに、CloudFront は version 1 を Hit from cloudfront で返します。age は手順 2 から増え続けています。キャッシュされている version 1 には Cache-Control が無かったため、デフォルト TTL の 24 時間が切れるまでこの状態が続きます。新しいオブジェクトに max-age=60 を付けても、すでにキャッシュされている古いオブジェクトの有効期限は変わりません。

S3 は version 2 なのに、CloudFront は version 1 を Hit from cloudfront で返している CloudShell の画面
S3 は version 2 なのに、CloudFront は version 1 を Hit from cloudfront で返している CloudShell の画面
クラウドおさる

倉庫のバナナに「1 分で下げてよし」と札を付けても、売店の棚に並んでいる昨日のバナナには札が付いていないのでござる。棚の古いものは、期限切れを待つか、下げる指示を出すしかないのでござるぞ。

手順 4: キャッシュを無効化する

CloudFront コンソールに戻り、cf-cache-lab のディストリビューションを開きます。「キャッシュ削除」タブを選択し、「キャッシュ削除を作成」をクリックします。

  • 「選択方法」は既定の「パス別」のまま
  • 「無効にするオブジェクトパス」: /index.html
キャッシュ削除の作成画面(パスに /index.html を入力した状態)
キャッシュ削除の作成画面(パスに /index.html を入力した状態)

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

キャッシュ削除の詳細画面(ステータスが完了済み、パスが /index.html)
キャッシュ削除の詳細画面(ステータスが完了済み、パスが /index.html)

CloudShell に戻り、もう一度取得します。

check index.html; sleep 5; check index.html

1 回目は Miss from cloudfront で version 2 が返り、cache-control: max-age=60 ヘッダーが付いています。2 回目は version 2 が Hit from cloudfront で返ります。無効化によって、次のリクエストで S3 から取り直したことが分かります。

無効化のあと version 2 が Miss → Hit で返り、cache-control: max-age=60 が付いている CloudShell の画面
無効化のあと version 2 が Miss → Hit で返り、cache-control: max-age=60 が付いている CloudShell の画面

手順 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.html

version 3 が返ります。max-age=60 が切れたので、CloudFront が S3 に問い合わせ、更新されていた新しいオブジェクトを受け取ったためです。x-cache は Miss from cloudfront になり、age ヘッダーは付きません。TTL が切れたあとにオリジンへ問い合わせて、内容が変わっていたため新しいオブジェクトを取得し直したことを示します(内容が変わっていなければ RefreshHit from cloudfront になります)。

60 秒待ったあと、無効化なしで version 3 が返っている CloudShell の画面
60 秒待ったあと、無効化なしで version 3 が返っている CloudShell の画面

入口の 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.css

style.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.css

style.v2.css はまだ誰もリクエストしていない URL なので、Miss from cloudfront で S3 から取ってきて、/* v2 */ が返ります。無効化をしなくても、新しい URL ならすぐに新しい内容になります。

style.css?v=2 は v1 が Hit で返り、style.v2.css は v2 が Miss で返っている CloudShell の画面
style.css?v=2 は v1 が Hit で返り、style.v2.css は v2 が Miss で返っている CloudShell の画面

実際のサイトでは、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 からの移行と推奨構成」で扱っています。

参照先

キャリア相談
クラウドおさる

独学の先に、AWS の仕事がある ── ジュニアおさるのキャリア相談

CLF には受かったけれど、次に何をすればいいのか分からない。独学で AWS を学ぶジュニアおさるの相談に、株式会社ジェニュイン代表の土肥が答えています。

  • ✓資格はゴールか、CLF の次に何から手を付けるか
  • ✓EC2 の次に作ってみる構成と、IaC・コスト管理
  • ✓実務経験ゼロでも、面接で話せる「これまで」の作り方
新卒・未経験の方へ
クラウドおさる

AWS を仕事にしたい方へ ── 新卒・未経験から

この記事を書いているのは、AWS の設計・構築を仕事にしている現役エンジニアです。運営元の株式会社ジェニュインでは、新卒・未経験の方も募集しています。異業種や文系からインフラエンジニアになり活躍しているメンバーもいます。

  • ✓システム開発・WEB エンジニア / インフラ構築などの職種で、未経験から応募できます
  • ✓未経験から入社した社員の声を含む社員アンケートを掲載
  • ✓応募フォームから 1 分で応募できます

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

株式会社ジェニュイン(https://genuine-pt.jp)の代表取締役.

2018年〜インフラエンジニアとしてキャリアをスタートし、オンプレミスのネットワーク・サーバ環境で3年半、クラウド環境で4年半の8年間エンジニアとして従事。
2021年に佐藤氏の創業した株式会社Luxyを引き継ぎ、代表に就任。
その後アガルートグループ内の株式会社ジェニュインと合併。
合併後同社代表に就任。

目次