ようへいの日々精進XP

よかろうもん

急いで CloudFront コンテンツキャッシュの挙動について確認する 〜 最小 TTL の設定には気をつけたい 〜

これは

YAMAP エンジニア Advent Calendar 2021 の第 12 日目の記事にします。

qiita.com

経緯

今回、API レスポンスを CloudFront にキャッシュしようということになって、CloudFront のコンテンツキャッシュの挙動について確認する機会があったのでメモしておきます。

確認したい内容としては、以下の通りでした。

  • オリジンからの Cache-Control ヘッダの値によってキャッシュの挙動が変わるか
  • CloudFront の TTL 設定によって、キャッシュの挙動がどのように変わるか

これらの挙動については、以下の AWS ドキュメントに整理されており、自分で手を動かすまでもないかもしれませんが、自分は、ドキュメントを読んだだけではピンと来なかったので、今回、手を動かして確認してみることにしました。

docs.aws.amazon.com

尚、今回の記事では、「オリジンが Cache-Control: no-cache、no-store、および (または) private を返すパターン」のみを動作確認したいと思います。

f:id:inokara:20211215001519p:plain f:id:inokara:20211215001531p:plain

検証

とある API

YAMAP API のとある API エンドポイント (以後、/foo/bar) では、YAMAP 上のあるデータを返してくれます。パラメータを 2 つ指定することが可能で、1 つは、p= そして、もう一つは、キャッシュを制御する cache= です。

以下はサンプルリクエストです。

$ curl https://${API_ENDPOINT}/foo/bar?p=123
$ curl https://${API_ENDPOINT}/foo/bar?p=123&cache=1

cache= パラメータの有無で、以下のような違いがあります。

  • cache=0 を付与しない場合、レスポンスに cache-control: max-age=3600, public が付与されます
  • cache=0 を付与した場合、レスポンスに cache-control: max-age=0, private, must-revalidate が付与されます

ここで Cache-Control ヘッダについて

自分自身、Cache-Control ヘッダについて、名前しか聞いたことが無かったので、インターネット上のドキュメントを読みながら整理してみたいと思います。

参考にするドキュメントは、HTTP 技術資料の殿堂 (だと勝手に思っている) とも言える、MDN Web Docs です。

developer.mozilla.org

HTTP リクエストとレスポンスの両方でキャッシュを制御する為の設定 (文書内では「ディレクティブ」と書かれているので、以後はディレクティブと書く) が入っている HTTP ヘッダーが Cache-Control ヘッダーで、構文的には、以下のような決まりがあります。

  • 大文字小文字の区別は無いが、小文字が推奨される
  • 複数のディレクティブを定義することが出来るが、それらは、カンマで区切る
  • ディレクティブには、オプションの引数があり、トークンまたは quoted-string のどちらかで指定する

ディレクティブは、大別すると 3 つ (キャッシュ可能性、有効期限、再検証と再読み込み) にカテゴリ分けされています。全てのディレクティブについての説明を載せるのは、この記事の範疇を超えてしまうので、今回の記事に関係しそうなディレクティブのみを抜粋して転載します。

ディレクティブ 説明 (文書より引用)
public レスポンスが通常はキャッシュ可能でなくても、レスポンスをどのキャッシュにも格納することができます。
private レスポンスが通常はキャッシュ可能でなくても、ブラウザーのキャッシュにのみ格納することができます。レスポンスがどのキャッシュにも保存されないようにするには、代わりに no-store を使用してください。このディレクティブにはレスポンスがキャッシュに保存されないようにする効果はありません。
max-age= リソースが新しいとみなされる最長の時間です。 Expires とは異なり、このディレクティブはリクエスト時刻からの相対時間です。
must-revalidate 一度リソースが古くなると、キャッシュは元のサーバーでの検証が成功しない限り、古くなったコピーを使用してはならないことを示します。

冒頭に書いた、YAMAP の「とある API」のレスポンスを、上記の説明を踏まえて解釈すると、以下のように読み取れます。

$ curl https://${API_ENDPOINT}/foo/bar?p=123
...
cache-control: max-age=3600, public

このリクエストのレスポンスは、Cache-controlpublic が付与されている為、CDN 等にもキャッシュされている。また、max-age=3600 なので、キャッシュされているリソースの生存期間は 1 時間となる

$ curl https://${API_ENDPOINT}/foo/bar?p=123&cache=0
...
cache-control: max-age=0, private, must-revalidate

このリクエストのレスポンスは、Cache-controlprivate が付与されている為、ブラウザキャッシュのみに保存され (他のユーザーと共有されない)、リソースの生存期間は 0 ということで、常にリソース取得の為にサーバーにアクセスが発生される

「とある API」をキャッシュしてみる

本題に戻って

「とある API」を CloudFront でキャッシュしながら、キャッシュの挙動を確認してみたいと思います。

先述の通り、「とある API」は、リクエストパラメータによってオリジンから返却される Cache-Control ヘッダのディレクティブが異なる為、ディレクティブの設定に応じて、キャッシュの挙動を変える必要があります。

その辺りは、ドキュメントに記載されている通り、Cache-Control ヘッダのディレクティブを CloudFront が良しなに解釈してキャッシュの挙動を制御してくれますが、以下のパターン (再掲) で動作確認してみます。

  • オリジンからの Cache-Control ヘッダの値によってキャッシュの挙動が変わるか
  • CloudFront の TTL 設定によって、キャッシュの挙動がどのように変わるか

検証 (1) Cache-Control ヘッダの値によってキャッシュの挙動が変わるか

CloudFront のキャッシュ設定は、下図のような設定にしています。

f:id:inokara:20211215001617p:plain

「とある API」にアクセスしてみます。レスポンスヘッダの中から、cache-controlx-cache の値をチェックしてみます。

# 1 回目
$ curl https://${API_ENDPOINT}/foo/bar?p=123 -v 2>&1 | egrep 'cache-control|x-cache'
< cache-control: max-age=3600, public
< x-cache: Miss from cloudfront

# 2 回目
$ curl https://${API_ENDPOINT}/foo/bar?p=123 -v 2>&1 | egrep 'cache-control|x-cache'
< cache-control: max-age=3600, public
< x-cache: Hit from cloudfront

リクエストは、cache=1 が付与されていないので、オリジンのレスポンスには cache-control: max-age=3600, public が含まれている為、CloudFront でキャッシュされている (x-cacheHit from cloudfront となっている) ことが解ります。

一方で、オリジンレスポンスの Cache-Controlprivate が含まれている場合には、どのような挙動になるでしょうか。

# 1 回目
$ curl https://${API_ENDPOINT}/foo/bar?p=123&cache=1 -v 2>&1 | egrep 'cache-control|x-cache'
< cache-control: max-age=0, private, must-revalidate
< x-cache: Miss from cloudfront

# 2 回目
$ curl https://${API_ENDPOINT}/foo/bar?p=123&cache=1 -v 2>&1 | egrep 'cache-control|x-cache'
< cache-control: max-age=0, private, must-revalidate
< x-cache: Miss from cloudfront

リクエストには、cache=1 が付与されている為、オリジンレスポンスは cache-control: max-age=0, private, must-revalidate となり、これが CloudFront で解釈され、CloudFront ではキャッシュされていない (x-cache: Miss from cloudfront となっている)ことが解ります。

検証 (2) CloudFront の TTL 設定によって、キャッシュの挙動がどのように変わるか

試しに CloudFront のキャッシュ設定を下図のように変更してみます。

f:id:inokara:20211215001635p:plain

最小 TTL0 から 60 に変更してみます。最小 TTL0 以上にした場合、下図の通り、挙動が変わってきます。(再掲)

f:id:inokara:20211215001519p:plain f:id:inokara:20211215001531p:plain

オリジンレスポンスが cache-control: max-age=0, private, must-revalidate となるように、リクエストしてみて挙動を確認します。

# 1 回目
$ curl https://${API_ENDPOINT}/foo/bar?p=123&cache=1 -v 2>&1 | egrep 'cache-control|x-cache'
< cache-control: max-age=0, private, must-revalidate
< x-cache: Miss from cloudfront

# 2 回目
$ curl https://${API_ENDPOINT}/foo/bar?p=123&cache=1 -v 2>&1 | egrep 'cache-control|x-cache'
< cache-control: max-age=0, private, must-revalidate
< x-cache: Hit from cloudfront

おっと、CloudFront にキャッシュされている (x-cacheHit from cloudfront となっている) ことが確認出来ました。

ブラウザだけにキャッシュを想定してオリジンとなるアプリケーションを実装したとしても、CloudFront によって意図しないキャッシュが行われてしまうので、最小 TTL の設定には気をつけたいと思います。

以上

CloudFront コンテンツキャッシュの極一部の挙動について確認してみました。実際に手を動かすことで、これまでフワッとしていたことが明確になったので本当に良かったです。