ブログ一覧

Cloudflare の Purge Everything で Workers Cache が消えない理由

更新: Web開発者向け

Cloudflare 上で動かしている Astro 製の CMS「EmDash」 のサイトで、ヘッダーの表示を変えたいと思いました。エッジキャッシュに古い HTML が載っているはずなので、ダッシュボードの Caching → Configuration → Purge Everything を押しました。

消えませんでした。

押す前と押した後で、age が増えているだけだった

cf-cache-statusage を見れば、キャッシュが消えたかどうかは分かります。消えていれば次のアクセスは MISS になり、age はリセットされます。

押す前。

$ curl -s -D- -o /dev/null https://example.com/ | grep -iE 'cf-cache-status|^age:'
cf-cache-status: HIT
age: 6

Purge Everything を実行し、数秒待ってから同じコマンドを叩きました。

cf-cache-status: HIT
age: 146

さらに 30 秒ほど置いて、もう一度。

cf-cache-status: HIT
age: 180

age が 6 → 146 → 180 と増え続けています。これは「146 秒前にキャッシュされたものをそのまま返している」という意味で、purge されていれば起こりません。別のパスでも同じでした。ダッシュボードの操作自体は成功していて、エラーも出ていません。

Workers Cache には、ゾーンの purge が届かない

原因は、消そうとしていたキャッシュが「ゾーンのキャッシュ」ではなかったことでした。

Astro の Cloudflare アダプタでルートキャッシュを有効にすると、ビルド時に wrangler の設定へ cache が注入されます。

$ node -e "console.log(JSON.stringify(require('./dist/server/wrangler.json').cache))"
{"enabled":true}

これは Workers Caching という機能で、Worker の前段に置かれる専用のキャッシュになります。ゾーンの CDN キャッシュとは別の層で、Cloudflare のドキュメントにも次のように書かれています。

A Worker cannot reach into another Worker's cache, an entrypoint cannot reach into another entrypoint's cache, and no zone-level purge (via the dashboard, API, or Terraform) affects Workers Caching content.

ダッシュボードからでも、API からでも、Terraform からでも、ゾーンレベルの purge は Workers Caching の中身に影響しない、と明記されています。実測で age が増え続けたのは、仕様どおりの挙動でした。

トークンに Cache Purge の権限を足せば API から消せるのでは、とも考えましたが、これは的外れでした。権限の問題ではなく、purge の宛先が違います。

消せるのは Worker の中からだけ

Workers Caching を失効させる手段は、Worker のコードの中から呼ぶものしかありません。

import { cache } from "cloudflare:workers";

await cache.purge({ tags: ["posts"] });        // タグ指定
await cache.purge({ purgeEverything: true });  // 全部

ctx.cache.purge() でも同じことができます。CMS がコンテンツの保存や公開でキャッシュを自動失効させている場合、内部で呼んでいるのはこれにあたります。

つまり CI やローカルの端末など、Worker の外にいる立場からは、この層に手を出せません。デプロイの後処理でキャッシュを流す、という手順を組もうとして詰まったのがここでした。

そもそも、デプロイ時の purge は要らなかった

詰まったまま調べていて、前提が間違っていたことに気づきました。

Workers Caching は Worker のバージョンをキャッシュキーに含んでいます。設定の cross_version_cache が既定の false のとき、こうなります。

the Worker version is part of the cache key. Each deployed version has its own isolated cache, so a new deployment starts from an empty cache and never serves responses written by a previous version.

新しくデプロイしたバージョンは空のキャッシュから始まり、前のバージョンが書いたレスポンスを配信することは決してない、ということです。ドキュメントには「version-specific purging is unnecessary」とも書かれています。

デプロイ直後に確かめると、そのとおりでした。8 サイトすべてで 1 回目が MISS、2 回目以降が HIT に変わります。

1 回目
example-a.com    MISS
example-b.com    MISS
example-c.com    MISS

2 回目
example-a.com    cf-cache-status: HIT age: 16
example-b.com    cf-cache-status: HIT age: 14
example-c.com    cf-cache-status: HIT age: 11

コードを変更してデプロイすれば、古い HTML は配信されません。デプロイ後に purge を流す仕組みは、作る必要がありませんでした。書きかけていたスクリプトと CI のステップは捨てました。

直しても残るもの

これで「デプロイしたのに反映されない」は起きないと分かりましたが、コンテンツ以外の変更には別の穴が残っていました。

CMS がキャッシュを自動失効させる範囲は、コンテンツの保存・公開・削除に限られていることが多いようです。サイト設定やメニュー、記事へのタグ付けまでは面倒を見ていないことがあります。そしてこれらはデプロイを伴わないので、バージョン分割による自動失効も効きません。エッジキャッシュの保持時間を長く取っているほど、この差が表に出ます。

この穴の詳細と、CMS 本体に手を入れずに塞ぐ方法は EmDash で記事にタグを付けても本番に反映されない原因 に分けました。

この構成で気をつけること

  • ゾーンの Purge Everything は Workers Caching に効かない。ダッシュボードでも API でも Terraform でも同じ
  • 消えたかどうかは cf-cache-statusage で判定する。age が単調増加していれば消えていない
  • Workers Caching を消せるのは Worker のコードの中からだけ。cache.purge({ tags }) または cache.purge({ purgeEverything: true })
  • デプロイすればキャッシュは空から始まる(cross_version_cache が既定の false のとき)。デプロイ後の purge 処理は不要
  • 確かめるときは HEAD(curl -sI)を使わない。キャッシュから返るので Worker に届かず、age を進めるだけになる。curl -s -D- -o /dev/null のように本文も取る GET で見る

キャッシュが消えないとき、まず疑ったのは権限と反映の遅延でした。どちらも外れで、実際には消す対象を取り違えていました。Workers Caching はゾーンのキャッシュと名前も見た目も近いのに、失効の経路だけが完全に分かれています。

関連

この記事をシェア