さくらのウェブアクセラレータで細かくキャッシュ設定する方法

年三日坊主のKKです。

 以前、「CDNの基本を初心者向けに整理してみた」「CDNのキャッシュ制御の基本を初心者向けに整理してみた」というCDN一般に関する記事を書きました。今回は、具体的なCDN製品を想定した設定方法についてまとめてみたいと思います。

今回、利用するのは、さくらインターネットの「さくらのウェブアクセラレータ」です。

以前の記事でもCDN一般の解説として、CDNのキャッシュ設定はオリジンサーバの Cache-Control レスポンスヘッダで指定する、と書いていたのですが、AWSのCloudFrontなど最近の多機能なCDNではオリジンサーバ側の Cache-Control レスポンスヘッダの内容を無視してCDN側できめ細かなキャッシュ設定ができるようになっています。
そのため、ウェブアクセラレータでも同様にCDN側で細かなキャッシュ設定が可能と勘違いしている方も少なくありません。

しかしながら、さくらのウェブアクセラレータはシンプルでストイックな Cache-Control レスポンスヘッダの内容に忠実なCDNです。
ウェブアクセラレータ単体では Cache-Control レスポンスヘッダでキャッシュ設定の指定が無い場合のデフォルトのキャッシュ期間しか設定することはできません。

それ以上のキャッシュ設定にはオリジンサーバの Cache-Control レスポンスヘッダの設定が必須です。
「オリジンサーバの設定変更せずにCDN側で良い感じにキャッシュしてよ」って思う人は他社のCDNを使いましょう。

とは言え、オリジンサーバの設定といっても所詮はApacheやNGINXで Cache-Control レスポンスヘッダの値を設定するだけです。
……と書くと、キャッシュ期間を指定するだけで終わりそうですが、既存の設定によってはそう簡単にはいきません。

今回は、「キャッシュ期間を指定したのにキャッシュされない」場合の注意点も合わせてご紹介します。


1. 今回設定したいキャッシュ期間

今回は、次のようにファイルの種類によってキャッシュ期間を変えることにします。

対象 拡張子 キャッシュ期間 秒数
Webフォント .woff2 7日間 604800
JPEG・PNG画像 .jpg
.jpeg 
.png
7日間 604800
WebP画像 .webp 24時間 86400
JavaScript・CSS .js
.css
7日間 604800

ここでいうキャッシュ期間は、まずはCDN側のキャッシュ期間です。ブラウザ側のキャッシュ期間とは分けて考えます。

また、対象は認証不要で、誰がアクセスしても同じ内容を返す公開ファイルを想定します。HTMLやAPI、ログイン後の画面などは、今回の設定対象に含めません。

WebPだけ24時間としているのは今回の要件によるもので、WebPという形式に特別なキャッシュ期間の制約があるわけではありません。

以降は、Apache 2.4系と mod_headers が利用でき、ウェブアクセラレータを経由した配信設定は完了している前提です。CMSやWebアプリケーション固有の設定方法には立ち入らず、Apache側での設定を扱います。

さくらのウェブアクセラレータ自体のセットアップについては「さくらのクラウドでCDNを利用してみる」を参照してください。


2. CDN向けのキャッシュ期間は「s-maxage」で指定する

最初に押さえておきたいのが、さくらのウェブアクセラレータのキャッシュ仕様です。

オリジンサーバのレスポンスヘッダからCDNのキャッシュ期間を指定する場合は、次のように s-maxage を使用します。max-age や Expires だけでは、ウェブアクセラレータのキャッシュ期間を指定できません。

Cache-Control: s-maxage=86400

これで、ウェブアクセラレータに24時間のキャッシュ期間が指定されます。

max-age の設定なら既に入っている、というサーバもあるかと思いますが、それだけでは今回の目的を満たせない点に注意して下さい。
ウェブアクセラレータの「デフォルトのキャッシュ期間」の設定が有効かつ s-maxage が未設定の場合、ウェブアクセラレータはキャッシュ期間が未指定と判断してデフォルトのキャッシュ期間が設定されてしまいます。

参考:さくらのウェブアクセラレータとは (概要・仕様)

ブラウザとCDNで期間を分ける

s-maxage は、CDNなどの共有キャッシュ向けの指定です。max-age と併用することで、ブラウザとCDNで異なるキャッシュ有効期間が指定できます。例えば、次の設定です。

Cache-Control: public, max-age=3600, s-maxage=604800

この場合、ブラウザ側は1時間、CDN側は7日間という使い分けになります。

なお、ウェブアクセラレータの最大キャッシュ保持期間は7日間です。ただし、指定した期間中の保持が保証されるわけではなく、アクセス頻度や配信基盤の状態などにより、期限前にキャッシュが削除される場合もあります。

参考:RFC 9111 HTTP Caching

「デフォルトのキャッシュ期間」は有効にする?

冒頭でも書きましたがウェブアクセラレータには、コントロールパネルから「デフォルトのキャッシュ期間」を設定する機能があります。これは、オリジンの応答に s-maxage の指定がない場合などに適用される設定です。

今回は話を簡単にするためこの機能は使用しませんが、キャッシュしてはいけないコンテンツが含まれずオリジン側で後述のキャッシュ禁止設定がされていないのであれば、手っ取り早くキャッシュ効果を実感できる機能です。

CMSやWebアプリケーション側の動作が分からない状態でサイト全体に期間を設定するよりも、まずは公開画像やCSSなど、対象を絞って設定する方が進めやすいと思いますが、オブジェクトストレージをオリジンにする場合などはこれでキャッシュ設定するしかないです。


3. Apacheで拡張子ごとに設定する

まず、既存のキャッシュ禁止・再検証の指定がなく、s-maxage も未設定の場合の基本形です。

Apacheの FilesMatch で拡張子を判定し、Header append で s-maxage を追記します。さくらの公式マニュアルでも、この組み合わせによる設定方法が紹介されています。

# Webフォント、JPEG、PNG、JavaScript、CSS:CDNで7日間
<FilesMatch "(?i)\.(woff2|jpe?g|png|js|css)$">
        Header append Cache-Control "s-maxage=604800"
</FilesMatch>

# WebP:CDNで24時間
<FilesMatch "(?i)\.webp$">
        Header append Cache-Control "s-maxage=86400"
</FilesMatch>

この例では .jpeg も .jpg と同じ扱いにし、大文字・小文字を区別しない条件にしています。

Header append は、既存の値を消さずに追記する指定です。そのため、既にブラウザ向けの max-age が設定されている場合、その値を残したままCDN向けの期間を追加できます。

ただし、既に s-maxage が設定されている場合は、さらに追記するのではなく、既存の設定値を変更して下さい。

参考:Apache モジュール mod_headers
参考:Webサーバ設定ファイルの記述方法

設定を記載する場所

サーバ設定を編集できる場合は、対象サイトの <VirtualHost> 内に記載します。CDNがオリジンへ接続する際に使用するVirtualHostが対象です。

.htaccess に記載することもできますが、こちらは対象ディレクトリで Header ディレクティブの利用が許可されている必要があります。.htaccess での設定を許可したい場合は、AllowOverride FileInfo などの設定もあわせて確認して下さい。

なお、サーバ全体の設定へ無条件に追加すると、他のサイトまで対象になってしまいます。複数サイトを収容しているサーバでは、適用範囲にも注意が必要です。

参考:セクションの設定



4. 「no-store」が付いていたら、追記だけではキャッシュされない

今回の検討で問題になったのが、オリジンサーバからのWebP画像の応答に次の指定が入っているケースです。

Cache-Control: no-store, no-cache, must-revalidate

ここに先ほどの Header append で s-maxage=86400 を追記すると、例えば次のようになります。

Cache-Control: no-store, no-cache, must-revalidate, s-maxage=86400

24時間という指定は入りました。

でも、これではウェブアクセラレータにキャッシュされません。

no-store が残っているためです。ウェブアクセラレータでは、Cache-Control: no-store、Cache-Control: private、Pragma: no-cache によるキャッシュ禁止指定が、期間の指定より優先されます。

後ろに追記した設定が、前の設定を取り消すわけではありません。

設定を足したつもりでも、元の「保存するな」という指定がそのまま残っているわけですね。

「no-cache」と「no-store」は意味が違う

ついでに、この3つの指定の違いも整理しておきます。以下はHTTP標準にて定義されている意味です。

指定 意味
no-store キャッシュに保存してはいけない
no-cache 保存は可能だが、再利用する前に再検証が必要
must-revalidate 有効期限が切れた後は、再検証に成功するまで再利用してはいけない

no-cache という名前を見ると「キャッシュ禁止」に思えますが、保存自体を禁止する no-store とは異なります。

今回、期間を追記してもキャッシュされない直接の理由は、no-store が残っていることです。

5. 公開WebPだけ、既存の設定を置き換える

では、no-store を消せばよいのか。

技術的にはその方向ですが、何のために付いている指定なのかを確認することが先です。

今回の情報だけでは、Apacheの設定なのか、CMSや画像生成処理が付けているのかまでは分かりません。まずは設定元を確認し、そちらで対象画像だけをキャッシュ可能に変更できるなら、その方法を優先します。

ここでは、対象が公開画像であり、認証状態やCookieによって内容が変わらず、キャッシュして問題ないことを確認できたものとして話を進めます。

今回の設定例

今回、WebPについては「追記」ではなく「置き換え」に変更します。

先ほどの設定と併記するのではなく、次の設定に置き換えて下さい。 7日間の対象は追記のまま、WebPだけ既存値を置き換えています。

# Webフォント、JPEG、PNG、JavaScript、CSS:CDNで7日間 
# 既存のキャッシュ禁止指定やs-maxageがないことを確認して使用する
<FilesMatch "(?i)\.(woff2|jpe?g|png|js|css)$">
        Header append Cache-Control "s-maxage=604800"
</FilesMatch>

# 公開WebP:CDNで24時間
# 200/304応答に限定して既存のCache-Controlを置き換える
<FilesMatch "(?i)\.webp$">
        Header onsuccess unset Cache-Control "expr=%{REQUEST_STATUS} == 200 || %{REQUEST_STATUS} == 304"
        Header always set Cache-Control "public, max-age=0, s-maxage=86400" "expr=%{REQUEST_STATUS} == 200 || %{REQUEST_STATUS} == 304"

        # Pragma: no-cache が付いている場合も除去する
        Header onsuccess unset Pragma "expr=%{REQUEST_STATUS} == 200 || %{REQUEST_STATUS} == 304"
        Header always unset Pragma "expr=%{REQUEST_STATUS} == 200 || %{REQUEST_STATUS} == 304"
</FilesMatch>

少し長くなりましたが、理由があります。

Apacheでは、通常の応答ヘッダと、CGIやFastCGIなどから渡されたヘッダが、内部的に異なる領域で管理されることがあります。単純な Header set だけでは元のヘッダが残り、同じ名前のヘッダが重複する場合があるので確実にヘッダ値を書き換えるため、onsuccess unset と always set を組み合わせています。

また、HTTPステータスが200または304の場合に限定することで、この設定によって404や500などの応答をキャッシュ可能に変更しないようにしています。 Header では、このように応答ステータスによる条件指定が可能です。

この設定でブラウザのキャッシュはどうなる?

WebPについて、設定後に期待する値は次のとおりです。

Cache-Control: public, max-age=0, s-maxage=86400

CDN側には24時間を指定し、ブラウザ側で新鮮な応答として扱う期間は0秒としています。max-age=0 は、保存自体を禁止する no-store とは異なります。

ブラウザにも24時間を指定する場合は、設定例の max-age=0 を max-age=86400 に変更します。

Cache-Control: public, max-age=86400, s-maxage=86400

ただし、ブラウザにも長期間キャッシュさせる場合は、後述する更新時の運用も合わせて検討して下さい。

拡張子だけで判定できない構成には注意

今回の設定は、Apacheの FilesMatch によるファイル名の判定です。レスポンスの Content-Type を見て分類しているわけではありません。

例えば、拡張子のない画像配信URLや、内部的に画像生成用PHPへ転送する構成では、この設定が期待どおり適用されるか別途確認が必要です。

CMSやWebアプリケーションの詳細が分からないからこそ、拡張子だけを見て「静的ファイルのはず」と決め付けず、実際の配信経路も確認しておきたいところです。

6. 設定したら、レスポンスを確認する

Apacheの設定ファイルを変更した場合は、構文を確認してから設定を再読み込みします。

一方、.htaccess はリクエスト時に読み込まれるため、変更のための再読み込みは不要です。
ただし、本体設定の構文確認だけで終わらせず、実際に対象ファイルへアクセスして応答とエラーログを確認して下さい。

参考:apachectl - Apache HTTP Server Control Interface
参考:Apache チュートリアル: .htaccess ファイル

レスポンスヘッダーの確認にはHEADではなくGETを使う

ヘッダを見るだけなら curl -I を使いたくなりますが、ウェブアクセラレータのキャッシュ対象メソッドはGETです。キャッシュ動作の確認には、GETリクエストを使用します。

次の例では、本文を捨ててレスポンスヘッダだけを表示しています。URLは実在する画像に変更して下さい。

URL='https://www.example.com/images/sample.webp' 

for i in 1 2 3; do
        echo "===== Request ${i} ====="
        curl -sS --connect-timeout 5 --max-time 20 \
        -D - -o /dev/null "$URL" |
        grep -iE '^(HTTP/|cache-control:|pragma:|x-cache:|age:|content-type:|set-cookie:)'
done

WebPについて、期待するレスポンスヘッダの例は次のようになります。

HTTP/2 200 
Content-Type: image/webp
Cache-Control: public, max-age=0, s-maxage=86400
X-Cache: HIT

確認したいのは、s-maxage=86400 が含まれ、元の no-store や Pragma: no-cache が残っていないことです。

そのうえで、同じURLに数回アクセスし、X-Cache: HIT となるか確認します。さくらの公式手順でも、複数回アクセスした後に Cache-Control と X-Cache を確認する方法が案内されています。

Webフォント、JPEG、PNG、JavaScript、CSSについても、それぞれの実在するURLで s-maxage=604800 を確認します。

設定ファイルに指定を書いたことと、実際にそのヘッダが返ってくることは別です。最後は、出てきたレスポンスを見て確認しましょう。

参考:curl man page
参考:【TIPS】さくらのウェブアクセラレータをさくらのレンタルサーバで使ってみよう

7. 更新時の運用もセットで考える

キャッシュできるようになったら、次に考えるのは更新時の反映です。

同じURLのファイルをオリジンで上書きしても、CDNに古い内容がキャッシュとして残っていれば、その内容が配信され続けます。すぐに反映したい場合は、ウェブアクセラレータのコントロールパネルやAPIから、対象URLのキャッシュを削除を実行します。

また、ファイル名やクエリ文字列を変更し、別のURLとして取得させる方法もあります。

変更前:/css/style.css?v=1 
変更後:/css/style.css?v=2

ウェブアクセラレータでは、クエリ文字列の違いもキャッシュの識別に使用されます。

ここで注意したいのが、CDNのキャッシュ削除では、利用者のブラウザに保存されたキャッシュまでは消せないことです。ブラウザにも長い有効期間を指定する場合は、ファイル名やクエリ文字列によるバージョン管理が重要になります。

「長くキャッシュさせる設定」と「更新した内容を届ける方法」は、別々ではなくセットで考えておく必要があります。

参考:HTTP caching - MDN Web Docs

おまけ:オリジンへ直接アクセスして切り分ける

CDN経由で確認していると、Apacheの設定が反映されていないのか、CDNに古いキャッシュが残っているのか分かりにくい場合があります。

オリジンへの直接接続が許可されている環境なら、curlの --resolve で接続先IPアドレスを指定すると切り分けに使えます。hostsファイルを書き換える必要はありません。

curl -sS --connect-timeout 5 --max-time 20 \ 
  --resolve www.example.com:443:192.0.2.10 \
  -D - -o /dev/null \
  'https://www.example.com/images/sample.webp'

192.0.2.10 は説明用のアドレスなので、実際のオリジンサーバのIPアドレスに置き換えて下さい。この例は、オリジンが対象ホスト名のHTTPS接続を受け付ける前提です。

この方法については、以前の「hostsを書き換えずに任意のドメインに任意のIPアドレスでWebアクセスする」でも紹介しています。

オリジンへの直接アクセスではApacheの設定を、CDN経由ではキャッシュの動作を、それぞれ確認すると整理しやすいと思います。

まとめ

今回は、さくらのウェブアクセラレータでファイルの種類ごとに細かくキャッシュするためのオリジンサーバ向けApache設定を整理しました。

押さえておきたいのは、次の3点です。

  • CDN向けのキャッシュ期間指定には s-maxage を使う。 ウェブアクセラレータでは、max-age だけではキャッシュ期間を指定できません。
  • no-store の設定が残っていると、キャッシュ期間を追記してもキャッシュされない。キャッシュして問題ないことを確認した上で、必要な範囲で既存設定を置き換えます。
  • 設定後の確認と、更新時の運用まで考える。 実際のレスポンスと X-Cache を確認し、長期間キャッシュさせる場合はURLのクエリを活用したバージョン管理も組み合わせます。

キャッシュは、長く保存させればそれでよいというものでもありません。

「保存してよいものか」「どれくらい保存するか」「更新をどれだけ迅速に反映させるか」。

この3つを揃えて考えるところまでが、キャッシュの設定なのだと思います。

なお、当社では上記を踏まえた具体的なCDNやサーバの設計、構築、運用を承っておりますので、お気軽にご相談ください。