こちらはdipアドベントカレンダー2025 15日目の記事です。
はじめに
前にCDN(Fastly)の挙動において「今、このリクエストはどの処理を通っているんだ?」と疑問を持ったため、実際のVCLライフサイクルがどう流れているかを知りたいと思ったことがありました。
そこで今回は、Fastly公式のサンドボックス環境である Fastly Fiddle を使い、各サブルーチン通過時にログを残すことでリクエストの経路を可視化するハンズオンをやってみました。
今回のゴール:リクエスト経路の可視化
Fastly VCLは vcl_recv(受付)、vcl_fetch(オリジン取得)、vcl_deliver(応答)など、いくつかの通過ポイント(サブルーチン)があります。

https://www.fastly.com/documentation/guides/full-site-delivery/fastly-vcl/about-fastly-vcl/
今回は、各サブルーチンを通るたびにカスタムヘッダ X-Trace に文字を追記し、最終的なレスポンスで以下のような情報を得ることを目指します。
-
キャッシュミス時:
Recv -> Miss -> Fetch -> Deliver -
キャッシュヒット時:
Recv -> Hit -> Deliver
これにより、ブラックボックスになりがちなCDNの挙動を見える化させます。
検証環境
- Fastly Fiddle (アカウント登録なしでVCLを即時実行できるWebツール)
Step 1: VCLコードの準備
まずは、各ライフサイクルイベントでヘッダを操作するVCLを書きます。
Fiddleを開き、左側のエディタに以下のロジックを組み込みました。
実装のポイント:
-
req.http.X-Trace(リクエストヘッダ)を変数として使い、バケツリレーのように文字列を連結していく。 -
vcl_deliver(最後)で、クライアントに返すレスポンスヘッダに中身を書き出す。
注意点
実際に試される方は、以下の画像のようにサブルーチンのブロックを削除して記載してください。

sub vcl_recv {
# 1. リクエスト受付
set req.http.X-Trace = "Recv";
# 検証用: クエリに "pass=true" があればキャッシュしない
if (req.url.qs ~ "pass=true") {
return(pass);
}
return(lookup);
}
sub vcl_miss {
# 2. キャッシュになかった場合
set req.http.X-Trace = req.http.X-Trace + " -> Miss";
return(fetch);
}
sub vcl_fetch {
# 3. オリジンから取得した直後
# オリジンからのレスポンス(beresp)にトレース情報を引き継いで保存
set beresp.http.X-Trace = req.http.X-Trace + " -> Fetch";
# 検証しやすくするためTTLを短めに設定
set beresp.ttl = 300s;
return(deliver);
}
sub vcl_hit {
# 3'. キャッシュがあった場合
set req.http.X-Trace = req.http.X-Trace + " -> Hit";
return(deliver);
}
そして、最後にクライアントへ返す vcl_deliver です。ここで少し工夫が必要でした。
sub vcl_deliver {
# 4. クライアントへ応答直前
# ★ここが重要ポイント(後述)
if (fastly_info.state ~ "^HIT") {
# HIT時は今回のリクエスト処理情報を出力
set resp.http.X-Debug-Trace = req.http.X-Trace + " -> Deliver(HIT)";
} else {
# MISS/PASS時はオリジンからのレスポンス情報を出力
set resp.http.X-Debug-Trace = resp.http.X-Trace + " -> Deliver(Origin)";
}
return(deliver);
}
Step 2: 初回アクセスの確認(Cache Miss)
右上の「RUN」ボタンを押して実行します。
初回なので当然キャッシュはありません。結果は以下の通りです。
-
X-Cache:
MISS -
X-Debug-Trace:
Recv -> Miss -> Fetch -> Deliver(Origin)
期待通りです。vcl_recv から Miss を経由して Fetch(オリジン)まで到達していることがわかります。
Step 3: キャッシュヒットの確認と「HIT判定」の修正
続いて、同じURLでもう一度「RUN」を実行します。今度はキャッシュから返されるはずなので、Fetch が消えて Hit になるはずです。
発生した事象
X-Cache: HIT と表示されているにもかかわらず、自作したトレースログが更新されず、MISS時のルートが表示されてしまう現象に遭遇しました。
調査したところ、原因は vcl_deliver での判定ロジックにありました。
学び:HITには種類がある
当初、私は以下のように書いていました。
# NGな書き方
if (fastly_info.state == "HIT") { ... }
しかし、Fastlyのデバッグ情報を見ると、実際のステータスは HIT-CLUSTER となっていました。
公式ドキュメントを確認してみると、HIT-CLUSTER は、ステータスの HIT とサブフィックスの CLUSTER が結合した形のステータスだということがわかりました。
Fastlyのような分散環境では、単純な HIT だけでなく、クラスタリング構成に関連した HIT-CLUSTER や、Stale(期限切れ猶予)キャッシュへの HIT-STALE などが存在みたいです。
そのため、完全一致(==)で判定するとこれらが漏れてしまい、else判定(Miss扱い)されてしまっていたのです。
解決策:正規表現でマッチさせる
ここは正規表現を使って「HITから始まるものすべて」を対象にするのが正解でした。
# OKな書き方
if (fastly_info.state ~ "^HIT") { ... }
コードを修正して再実行すると...
-
X-Cache:
HIT -
X-Debug-Trace:
Recv -> Hit -> Deliver(HIT)
無事、意図通り Fetch(オリジン通信)をスキップして、Hit ルートを通ったことが確認できました!
Step 4: キャッシュバイパスの確認(Pass)
最後に、vcl_recv に仕込んでおいた「強制バイパス」の挙動も見てみます。
リクエストのQuery Paramsに ?pass=true を追加して実行します。
-
X-Cache:
MISS(またはPASS) -
X-Debug-Trace:
Recv -> Pass -> Fetch -> Deliver(Origin)
こちらも vcl_recv で pass を返したため、vcl_miss(キャッシュ確認)に行かず、vcl_pass → vcl_fetch という「キャッシュを見ずにオリジンへ直行するルート」を通っていることが可視化できました。
まとめ:見えないものを見る大切さ
今回、手を動かして可視化してみたことで、以下の知見が得られました。
-
ライフサイクルの実感が湧く:
図で見るだけよりも、今ここでヘッダが付与されたと追っていくことで理解度が段違いに深まりました。 -
fastly_info.stateの判定は慎重に:
HIT判定は完全一致ではなく~ "^HIT"(正規表現)を使うべき、というのは実際にやってみて得られた学びでした。 -
Fiddleが便利:
手元でサクッと試せたのでとてもありがたかったです。
「CDNの挙動が怪しいな?」と思ったら、まずはFiddleで小さな再現コードを書いて、ヘッダに足跡を出力させてみることをおすすめします!
参考
https://www.fastly.com/documentation/guides/full-site-delivery/fastly-vcl/about-fastly-vcl/
https://www.fastly.com/documentation/reference/tools/fiddle/
https://www.fastly.com/documentation/reference/vcl/variables/client-request/req-http/
https://www.fastly.com/documentation/reference/vcl/variables/miscellaneous/fastly-info-state/



