これは
YAMAP エンジニア Advent Calendar 2021 の第 12 日目の記事にします。
経緯
今回、API レスポンスを CloudFront にキャッシュしようということになって、CloudFront のコンテンツキャッシュの挙動について確認する機会があったのでメモしておきます。
確認したい内容としては、以下の通りでした。
- オリジンからの
Cache-Control
ヘッダの値によってキャッシュの挙動が変わるか - CloudFront の TTL 設定によって、キャッシュの挙動がどのように変わるか
これらの挙動については、以下の AWS ドキュメントに整理されており、自分で手を動かすまでもないかもしれませんが、自分は、ドキュメントを読んだだけではピンと来なかったので、今回、手を動かして確認してみることにしました。
尚、今回の記事では、「オリジンが Cache-Control: no-cache、no-store、および (または) private を返すパターン」のみを動作確認したいと思います。
検証
とある 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 です。
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-control
に public
が付与されている為、CDN 等にもキャッシュされている。また、max-age=3600
なので、キャッシュされているリソースの生存期間は 1 時間となる
$ curl https://${API_ENDPOINT}/foo/bar?p=123&cache=0 ... cache-control: max-age=0, private, must-revalidate
このリクエストのレスポンスは、Cache-control
に private
が付与されている為、ブラウザキャッシュのみに保存され (他のユーザーと共有されない)、リソースの生存期間は 0
ということで、常にリソース取得の為にサーバーにアクセスが発生される
「とある API」をキャッシュしてみる
本題に戻って
「とある API」を CloudFront でキャッシュしながら、キャッシュの挙動を確認してみたいと思います。
先述の通り、「とある API」は、リクエストパラメータによってオリジンから返却される Cache-Control
ヘッダのディレクティブが異なる為、ディレクティブの設定に応じて、キャッシュの挙動を変える必要があります。
その辺りは、ドキュメントに記載されている通り、Cache-Control
ヘッダのディレクティブを CloudFront が良しなに解釈してキャッシュの挙動を制御してくれますが、以下のパターン (再掲) で動作確認してみます。
- オリジンからの
Cache-Control
ヘッダの値によってキャッシュの挙動が変わるか - CloudFront の TTL 設定によって、キャッシュの挙動がどのように変わるか
検証 (1) Cache-Control ヘッダの値によってキャッシュの挙動が変わるか
CloudFront のキャッシュ設定は、下図のような設定にしています。
「とある API」にアクセスしてみます。レスポンスヘッダの中から、cache-control
と x-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-cache
が Hit from cloudfront
となっている) ことが解ります。
一方で、オリジンレスポンスの Cache-Control
に private
が含まれている場合には、どのような挙動になるでしょうか。
# 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 のキャッシュ設定を下図のように変更してみます。
最小 TTL
を 0
から 60
に変更してみます。最小 TTL
を 0
以上にした場合、下図の通り、挙動が変わってきます。(再掲)
オリジンレスポンスが 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-cache
が Hit from cloudfront
となっている) ことが確認出来ました。
ブラウザだけにキャッシュを想定してオリジンとなるアプリケーションを実装したとしても、CloudFront によって意図しないキャッシュが行われてしまうので、最小 TTL
の設定には気をつけたいと思います。
以上
CloudFront コンテンツキャッシュの極一部の挙動について確認してみました。実際に手を動かすことで、これまでフワッとしていたことが明確になったので本当に良かったです。