30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

Cloudflare を使ったキャッシュの基礎

CDN のエッジサーバにコンテンツをキャッシュしておくことで、コンテンツのダウンロードを高速化できる。
エッジサーバは物理的にユーザーに近い場所にあり、レイテンシが小さくなるため。また、エッジサーバは配信に最適化されているため、その点でも高速化が見込める。

しかし、ただ CDN を導入すればそれだけで最適なキャッシュを行ってくれるわけではない。
そもそも、全てのコンテンツをキャッシュしてもらいたいわけではない。ウェブサイトによっては、キャッシュして欲しくないコンテンツ、キャッシュされたら困るコンテンツも存在するはず。
キャッシュする場合も、短期間だけキャッシュさせたいコンテンツもあれば、可能な限り長くキャッシュさせたいコンテンツもある。
そしてそういったことは CDN には判断できないので、開発者が明示的に設定する必要がある。
つまり、CDN を効果的かつ安全に使うためには、CDN の使い方を理解し、エッジサーバへのキャッシュを意図した通りに行えるようになる必要がある。

この記事では、CDN のひとつである Cloudflare の、キャッシュに関するルールや設定方法を見ていく。
なお、全ての設定やその組み合わせを検証したわけではないし、見落としている変数もあるかもしれない。実際に設定を行う際には必ず動作確認を行うべき。
とはいえ、闇雲に試行錯誤するのでは効率が悪すぎる。
実際に設定を行っていく際の足掛かりになるよう基本となる概念や原則を整理したのが、この記事になる。

動作確認は、無料プランの Cloudflare で行った。
ダッシュボードの言語設定は「日本語」にしており、この記事で扱う用語もその表記に準じている。
詳細は後述するが、サイト全体の「キャッシュ レベル」は「標準」で固定してある。
また、リクエストは全て GET メソッドで行い、ステータスコードは全て200になる環境で、検証している。

デフォルトでキャッシュ対象のコンテンツ

Cloudflare においては、デフォルトでキャッシュされるコンテンツと、そうでないコンテンツの 2 つに、コンテンツを分類できる。
どちらのコンテンツであるかによって挙動が変わるので、まずはこの 2 つに分類できるということを、把握しておく必要がある。

あるコンテンツがどちらであるかは、ファイルの拡張子で判断される。どれがキャッシュ対象の拡張子であるかは公式サイトで確認できるので、暗記する必要はない。
https://support.cloudflare.com/hc/ja/articles/200172516#h_a01982d4-d5b6-4744-bb9b-a71da62c160a

txtは対象外だが、robots.txtは例外的にキャッシュ対象となる。

指定した有効期間だけクライアントにキャッシュさせる方法

まずはクライアントにキャッシュさせる方法を見ていく。エッジサーバへのキャッシュは一旦措いておく。

どのようにクライアントにキャッシュするのかを決める要素のひとつに、「ブラウザ キャッシュ TTL」がある。
これは、Cloudflare のダッシュボードで開発者が設定する。
サイト全体の設定と URL 毎の設定があり、URL 毎の設定が優先される。
例えば、サイト全体の設定が「既存のヘッダーを優先」で、https://example.com/foo.jsの設定が「30分」の場合、https://example.com/foo.jsの「ブラウザ キャッシュ TTL」は「30分」に、それ以外の URL は未設定になる。

そしてもうひとつ「キャッシュ レベル」という要素がある。
これも、サイト全体の設定と URL 毎の設定があり、URL 毎の設定が存在すればそれが優先される。
冒頭で説明したように、サイト全体の「キャッシュ レベル」は「標準」になっているという前提で話を進めていく。

クライアントへのキャッシュがどのようになるのかは、「キャッシュ レベル」で大きく分かれる。

「キャッシュ レベル」が「スキップ」の場合

「ブラウザ キャッシュ TTL」は無視される。そのため、オリジンサーバでCache-Controlmax-ageを設定しておけば、その時間だけクライアントでキャッシュされる。

「キャッシュ レベル」が「Cache Everything」の場合

オリジンサーバがmax-ageを指定していなかった場合、「ブラウザ キャッシュ TTL」の時間が採用され、その値をmax-ageとして設定したCache-Controlが、クライアントにレスポンスされる。「ブラウザ キャッシュ TTL」も未設定だった場合、max-ageは何も設定されない。
オリジンサーバがmax-ageを指定していた場合、「ブラウザ キャッシュ TTL」と比較し、長いほうが採用されてクライアントにレスポンスされる。「ブラウザ キャッシュ TTL」が未設定だった場合は、オリジンサーバが指定したmax-ageがそのまま採用される。

「キャッシュ レベル」が「標準」の場合

デフォルトでキャッシュ対象のコンテンツの場合、「Cache Everything」のケースと同様の挙動になる。
そうでない場合、「スキップ」のケースと同様の挙動になる。

エッジサーバにキャッシュさせる方法

次に、Cloudflare のエッジサーバへのキャッシュについて見ていく。
エッジサーバにキャッシュされているかどうかは、レスポンスヘッダのCF-Cache-Statusフィールドを見るとわかる。
各値の意味は公式サイトで確認できる。
https://support.cloudflare.com/hc/ja/articles/200172516#h_bd959d6a-39c0-4786-9bcd-6e6504dcdb97

これも、「キャッシュ レベル」で大きく挙動が分かれる。
そして、先程の「ブラウザ キャッシュ TTL」に相当する概念として「エッジ キャッシュ TTL」がある。これは、サイト全体の設定は存在せず、URL に対してしか設定できない。

「キャッシュ レベル」を「スキップ」にしている場合、CF-Cache-Statusは必ずDYNAMICになる。
つまり、エッジサーバへのキャッシュは行われない。

「キャッシュ レベル」が「スキップ」以外の場合は、「キャッシュ レベル」、「エッジ キャッシュ TTL」、そしてデフォルトでキャッシュ対象のコンテンツであるかどうかで、挙動が変わる。

「エッジ キャッシュ TTL」が設定されておらず、「キャッシュ レベル」が「Cache Everything」の場合

この場合、オリジンサーバがCache-Controlフィールドに設定した値によって挙動が変化する。
no-storeprivateを設定しておくと、CF-Cache-StatusBYPASSになり、エッジサーバへのキャッシュは行われない。
s-maxage=nを設定すると、n秒間だけエッジサーバにキャッシュされる。

Cache-Controlを指定しなかった場合も、エッジサーバへのキャッシュが行われる。その場合キャッシュされる時間は恐らく、ステータスコードによって決まる。
https://support.cloudflare.com/hc/ja/articles/200172516#h_51422705-42d0-450d-8eb1-5321dcadb5bc

「エッジ キャッシュ TTL」が設定されておらず、「キャッシュ レベル」が「標準」の場合

この場合、デフォルトでキャッシュ対象のコンテンツであるかどうかで、挙動が変化する。

対象の場合、オリジンサーバがCache-Controlフィールドに設定した値によって、挙動が決まる。
つまり、「キャッシュ レベル」が「Cache Everything」のときと同じ挙動になる。

対象でない場合、エッジサーバにはキャッシュされない。CF-Cache-Statusは必ずDYNAMICになる。
オリジンサーバがmax-ages-maxageを設定していたとしても、それは変わらない。
つまり、「キャッシュ レベル」を「スキップ」にしたときと同じ挙動になる。

「エッジ キャッシュ TTL」が設定されており、「キャッシュ レベル」が「Cache Everything」の場合

全てのコンテンツが、必ずエッジサーバにキャッシュされるようになる。
オリジンサーバがno-storeprivateを設定していたとしても、キャッシュされてしまう。それでいて、Cache-Controlフィールドの値を書き換えることはない。
そのため、クライアントに届いたレスポンスヘッダのCache-Controlフィールドにはprivateが設定されているのに、エッジサーバにキャッシュされてしまっている、という状況が発生し得る。

「エッジ キャッシュ TTL」が設定されており、「キャッシュ レベル」が「標準」の場合

デフォルトでキャッシュ対象のコンテンツである場合、「キャッシュ レベル」が「Cache Everything」のときと同じ挙動になる。
つまり、必ずエッジサーバにキャッシュされるようになる。

デフォルトでキャッシュ対象のコンテンツでない場合、エッジサーバにはキャッシュされない。CF-Cache-Statusは必ずDYNAMICになる。
オリジンサーバがmax-ages-maxageを設定していたとしても、それは変わらない。

まとめ

ここまでの内容を踏まえて、結局どうすればいいのかを整理する。

エッジサーバにキャッシュさせたい場合

デフォルトでキャッシュ対象であるコンテンツをエッジサーバにキャッシュするには、s-maxageを設定すればよい。
その名の通りデフォルトでもキャッシュされるのだが、s-maxageを設定しておけばコンテンツ毎に時間を指定できる。

デフォルトではキャッシュ対象外のコンテンツをエッジサーバにキャッシュさせるには、「キャッシュ レベル」を「Cache Everything」に設定する必要がある。
そうしておかないと、s-maxagemax-ageを付与しても無視されてしまう。
「Cache Everything」にした上でs-maxage=nとすれば、n秒だけエッジサーバにキャッシュされる。

「エッジ キャッシュ TTL」を設定すると、no-storeprivateを無視してエッジサーバにキャッシュされてしまうので、注意する。
しかもCache-Controlを書き換えないので、レスポンスヘッダだけを見てもなぜエッジサーバにキャッシュされたのかが分からない。
そのため、デバックや動作確認が行いにくくなる可能性がある。

エッジサーバにキャッシュさせたくない場合

既述した通り、「キャッシュ レベル」を「スキップ」にしておけば、エッジサーバへのキャッシュは行われない。オリジンサーバでs-maxageを設定していたとしても、無視される。
Cache-Controlフィールドを書き換えられてしまうわけではないので、max-ageを設定しておけば、エッジサーバにはキャッシュさせずクライアントにはキャッシュさせる、ということが可能になる。

Cache-Controlno-storeprivateを設定しておけば、「エッジ キャッシュ TTL」が設定されていない限り、エッジサーバへのキャッシュは行われない。
no-storeなら、クライアントへのキャッシュも行われない。

エッジサーバにもクライアントにもキャッシュさせたい場合

デフォルトでキャッシュ対象のコンテンツの場合は、s-maxagemax-ageを設定すればよい。

デフォルトでキャッシュ対象ではないコンテンツの場合は、取り敢えず「Cache Everything」を設定しないとエッジサーバにキャッシュできない。
その上でs-maxagemax-ageを設定すればよい。

「ブラウザ キャッシュ TTL」を設定すれば、その値とオリジンサーバが指定したmax-ageを比較し、長い方が採用される。
そして採用された値がCache-Controlmax-ageとしてクライアントにレスポンスされるので、「エッジ キャッシュ TTL」と違ってクライアントから確認しやすい。

参考資料