はじめに
Fastly は Varnish をベースに作られています。そのため挙動を設定するのに Varnish の設定言語である Varnish Configuration Language(以下 VCL) を利用しています。
Fastly のウェブコントロールパネルで挙動を設定した場合も実は自動的に VCL が生成されており、生成された VCL は設定画面の show VCL をクリックすることで確認することが出来ます。
UI から挙動を設定するのではなく Snippet、Custom VCL といった機能を利用して VCL を自分で書くことも出来ます。VCL を直接書くことで UI では出来ないような柔軟な挙動を設定することが出来ます。
この記事では初めて Fastly の VCL を書く人を対象に VCL の書き方の基本を説明したいと思います。
サブルーチン
まず、最初に理解すべき点として、Varnish の処理のサブルーチンがあります。それぞれのサブルーチンは return で移動します。サブルーチンの種類としては以下のようなものがあり、それぞれ以下のような処理を行います。
vcl_recv : クライアントからのリクエストを受け付け
vcl_hash : キャッシュを確認するための hash key を生成
vcl_hit : hash key が一致するキャッシュオブジェクトが存在
vcl_miss : hash key が一致せず、キャッシュが存在しない
vcl_pass : キャッシュをすべきでないと判断されたリクエスト
vcl_pipe : VCL の処理を通さずにオリジンからコンテンツを取得
(Fastly の VCL では pipe を利用することは出来ません)
vcl_fetch : オリジンからコンテンツを受け取った直後
vcl_deliver : コンテンツをクライアントに配信
vcl_error : エラーを検知した場合の処理
VCL の処理はそれぞれのサブルーチンの配下に書くので、例えばクライアントからのリクエストに情報の追加などの何らかの処理を行いたいのであれば vcl_recv に書く必要があり、クライアントへ配信するレスポンスのヘッダーを書き換えたいのであれば vcl_deliver に記載します。
Fastly でのこれらのサブルーチンを状態遷移図で表すと以下のようになります。
※ Fastly では配信拠点内でキャッシュを効果的に利用するためクライアントからリクエストを受けるサーバー(Edge Node)とオリジンからコンテンツを取得するサーバー(Fetch Node)が異なります。
※ 特殊な挙動として vcl_pass で return(pass) すると fetch に移動します。vcl_deliver で return(deliver) すると log に移動します。
VCL で処理を行う場合どのサブルーチンに処理を記載するかが重要になってきます。処理フローを完全に覚える必要はありませんが、迷ったときはシーケンス図からどこに処理を追加すべきかを考えてみて下さい。
変数の操作
ヘッダーや変数の値を追加したり変更する操作は以下の2つの方法で行います。
set [変数] = [値] : 指定した内容を設定。すでに値を含むヘッダーなどを指定した場合は指定した内容で上書きされる
例: set req.http.X-test="a";
unset [変数] : 指定したヘッダなどを削除
例: unset req.http.X-test;
add [変数] = [値]:新しいヘッダを追加。(setと違い同名のヘッダーを追加。)
一部(varyなど)は内部処理のため既存のヘッダーに指定した値を追記しますが、キャッシュ対象とそうでない場合で挙動が異なるため既存ヘッダーに値を追加する目的での利用はお勧めしません
変数としてはヘッダーや各種の Fastly が保持している情報が利用可能です。詳細については Fastly でよく使う変数とテクニックを参照してみて下さい。
演算子
演算子としては以下のものが利用可能です
演算子
== : 等しい
!= : 等しくない
< : より小さい
<= : 以下
> : より大きい
>= : 以上
代入演算子
= : 値を代入
+= : 加算代入
-= : 減算代入
*= : 乗算代入
/= : 除算代入
論理演算子
&& : AND, 論理積
|| : OR, 論理和
! : 否定
実行条件の追加
VCL では、if
を利用して処理を行う条件を指定することが出来ます。記述方法は以下のようになります。
if(式){
処理
}elseif(式){
処理
}else{
処理
}
※ elseif は elsif でも大丈夫です。
例えば以下のコードを vcl_recv に記載すると、リクエストされたパスが /dri1/
の場合は 1
、dir2
の場合は2
、それ以外はother
を値を含む X-type というリクエストヘッダーを追加します。
if (req.url == "/dir1/") {
set req.http.X-type = "1";
} elseif (req.url == "/dir2/") {
set req.http.X-type = "2";
} else {
set req.http.X-type = "other";
}
※ req.url
にはリクエストされたパスの情報が含まれます。
複数条件の指定
if 条件に複数の条件を指定したい場合は、論理演算子を利用して以下のように記述することが出来ます。
if (client.geo.country_code == "JP" && client.ip !~ white_acl ){
処理
}
日本からのアクセスかつ、クライアントのIPが white_acl
に含まれない場合
if(req.url ~ "^/111/" || req.request == "GET"){
処理
}
リクエストパスが/111/
で始まる、またはリクエストメソッドがGET
の場合
if (req.url !~ "^/favicon.ico" && (resp.status == 404 || resp.status == 403) ){
# 処理
}
リクエストパスが /favicon.ico
で始まらない、かつレスポンスコードが404か403
正規表現を利用した条件指定
VCL では Perl 互換の PCRE 形式の正規表現を利用することが出来ます。VCL で正規表現を記載する際にスラッシュ(/) はエスケープする必要はありません。まずは次のようなものを理解しておくとよいと思います。
~ : 含む
!~ : 含まない
^ : 行頭
$ : 行末
(?i) : 最初に記述することで大文字小文字を区別しない
正規表現の詳しい書き方は VCL 正規表現早見表をご参照下さい。
例えば以下のコードは、大文字小文字を問わずに拡張子が html か php の場合、リクエストに X-type ヘッダーを追加します。
if (req.url.ext ~ "(?i)^(html|php)$") {
set req.http.X-type = "htmlorphp";
}
いくつかよく使うような条件のサンプルをあげてみます。
特定の拡張子
req.url.ext ~ "(?i)^(html|php)$"
: リクエストの拡張子が html または php
特定のパスで始まる
req.url ~ "^/pass"
: リクエストのパスが /pass で始まる
req.url ~ "^/(aaa|bbb|ccc)"
: リクエストのパスが /aaa か /bbb か /ccc で始まる
特定の Content-Type
beresp.http.content-type ~ "text/html"
: コンテンツ種別が text/html(という文字列を含む)
その他のよく使う条件
特定のステータスコードにマッチ
http_status_match ではレスポンスのステータスコードを拾うことが出来ます
if (http_status_matches(beresp.status, "200,206")) {
# 200または206 のときに実行したい処理
}
if (!http_status_matches(beresp.status, "200,206")) {
# 200または206 以外のときに実行したい処理
}
特定のヘッダーが存在する
特定のヘッダーなどが存在する場合に処理を行いたい場合は以下のように記述することが出来ます。
if(<header_name>){
処理
}
特定のヘッダーが存在しない
逆に特定のヘッダーなどが存在しない場合は以下の条件で取得することが出来ます。
if(!<header_name>){
処理
}
絶対にヒットしない条件
何らかの理由で UI で生成されたコードを特定の処理を実施しないようにしたい場合、以下の条件の中に処理を入れることで、VCL 上にはコードを残したまま、処理をスキップさせることが可能です。
if (false){
スキップしたい処理
}
他の書き方として、req.url
はリクエストには必ず含まれているので、!req.url
という条件も常にFalseになり同様に処理をスキップすることが出来ます。
※ if(false)
が導入される以前はこちらを推奨していました。
ヘッダーの操作
続いてヘッダーなどに含まれる情報を操作する方法や値を取り出す方法をご紹介します。
値の追記
単に set
にヘッダーを指定すると指定した値で既存の値を上書きするためヘッダーに元々含まれていた値は失われます。上書きでなく追記したい場合は、以下のようなコードで実現することが出来ます。
if (!beresp.http.vary) {
set beresp.http.vary = "value2";
} else{
set beresp.http.vary = beresp.http.vary "," "value2";
}
最初の if 条件でオリジンからのレスポンスに vary ヘッダがない場合は vary ヘッダにvalue2
をsetします。
もしオリジンからのレスポンスにvaryヘッダが存在した場合、処理は else に入りますので、元々Varyに含まれていた値の後ろに ,
と value2
を追記します。
つまり、オリジンからのレスポンスに元々 vary:value1
が含まれていた場合、この処理を行うとvaryヘッダーの中身は vary:value1,value2
となります。
この処理でもよいのですが、ただ値を追記するだけでコードが少し長いですね。上記のコードは以下の1行のコードに置き換えることが出来ます。
set beresp.http.Vary = if(beresp.http.Vary, beresp.http.Vary + ",", "") + "value2";
if(beresp.http.Vary, beresp.http.Vary + ",", "")
の部分でオリジンからのレスポンスに vary が存在した場合は元々の vary の値と,
を、存在しない場合は""(null)を設定し、その後に value2
を付与しています。
※ +
は文字列を結合するのに利用しますが、なくても動作します。
Subfield への値の追加
更に別の書き方として以下のようなコードでも同じように既存ヘッダーに値を追加することが出来ます。
set beresp.http.Cache-Control:max-age = "3600";
set beresp.http.Vary:value2 = "";
1行目はレスポンスの Cache-Control ヘッダーに max-age=3600
を追記しています。(max-ageがレスポンスに元々含まれる場合は数字が上書きされます)
2行目は追加パラメータの値として ""(null) が指定されているので value2
のみが Vary ヘッダに追加されます。
このように :
を使用して subfield に値を追加することが可能です。たとえば以下のようなコードが実行されると x-value ヘッダには key1=value-1,key2=value-2,key3=value-3
が設定されます。
# Add information in subfield
set req.http.x-value:key1 = "value-1";
set req.http.x-value:key2 = "value-2";
set req.http.x-value:key3 = "value-3";
subfield 機能の詳細はこちらをご参照下さい。
正規表現による値の取得
re.group.[0-9]
オブジェクトと正規表現を利用して特定の文字列をマッチさせたり、値を取得することが出来ます。re.group.0
はマッチした全体の文字列を含みます。
Varnishの正規表現では大文字小文字が区別されます。正規表現記述の最初に(?i)
を含めることで大文字小文字を区別しないように指定することが可能です。また、Varnishの正規表現では/
はエスケープする必要はありません。
re.group
の使い方についてはVCL regular expression cheat sheet - Capturing matchesをご参照下さい。
正規表現を使わずにヘッダーから値を取得
ヘッダーに含まれる値が
value1=123; testValue=asdf_true; loggedInTest=true;
や
max-age=0, surrogate-control=3600
といったフォーマットの場合、以下の形式でHeader-Nameで指定したヘッダーからkey-nameで指定した値を取り出すことが出来ます。
req.http.Header-Name:key-name
例えばオリジンからのSet-Cookieレスポンスに
value1=123; testValue=asdf_true; loggedInTest=true;
といった値が含まれる場合、loggedInTest
クッキーの値は以下のように取得することが出来ます。
beresp.http.Set-Cookie:loggedInTest
同様に以下のようなコードでヘッダーに含まれる特定の要素を指定して削除することもできます。
unset beresp.http.Cache-Control:private;
本機能の詳細についてはIsolating header values without regular expressionsをご参照下さい。
クエリストリング関連の操作
クエリストリングから指定した key の値を取得
querystring.get(req.url, "key")
クエリストリングに値を追加
set req.url = querystring.add(req.url, "foo", "bar");
querystring.add の場合は追加で querystring.set の場合は上書きになります。
その他クエリストリングの操作については以下のページをご参照下さい。
https://developer.fastly.com/reference/vcl/functions/query-string/
値の置き換え
regsub(str, regex, sub)
regsuball(str, regex, sub)
regsub()
と regsuball()
を利用して値の置き換えや削除をすることが出来ます。一つ目の引数の文字列を入力値として受け取り、二つ目の引数の正規表現(regex)にマッチした内容を、3つ目の引数で指定した値に置き換えます。
regsub
は最初にマッチしたものだけを置き換えるのに対し、regsuball
は条件にマッチするすべての文字列を置き換えます。
regsub
、regsuball
の使い方についてはVCL regular expression cheat sheet - Replacing contentをご参照下さい。
その他の機能と Function
その他 Fastly で使える知っていると便利な機能と Function をご紹介します。
Function については https://developer.fastly.com/reference/vcl/functions/ にも多く紹介されていますのでこちらもご参照下さい。
Edge Dictionary
VCL では Edge Dictionary というキーと値のペアの情報のテーブルを持ち、各サブルーチンから情報にアクセスすることが出来ます。
Edge Dictionary にはサービスのバージョンを変更せずに情報を追加したり、値を更新することが出来るという特徴があるので、頻繁に更新される情報を含めておくと便利です。
Edge Dictionary は API 経由、もしくは Fastly のコントロールパネルの Configure - 該当の Service - Data - Dictionary から追加したり既存の Dictionary の値を変更したりすることが出来ます。VCL としてはどのサブルーチンにも入らずに以下のようなものになります。
table geoip_lang {
"US": "en-US",
"FR": "fr-FR",
"NL": "nl-NL",
}
Edge Dictionary の値には以下のように table.lookup でアクセスします。
table.lookup(ID id, STRING key[, STRING default])
id
はテーブルの名前です、Key はテーブルの中の key, default はオプションで、マッチする key がなかった場合に返却する文字列を指定します。
例えば以下のコードでは、リクエストに Accept-Language
ヘッダーがない場合に、クライアントのアクセス元の国にあわせて上述のテーブルから Accept-Language
に値を設定します。
アクセス元の国が geoip_lang テーブルに存在しない場合はデフォルト値として en-US
を設定します。
if (!req.http.Accept-Language) {
set req.http.Accept-Language = table.lookup(geoip_lang, geoip.country_code, "en-US");
}
単に table の中に指定した Key があるかないかを if 条件で拾いたい場合は table.contains で以下のように記述することが出来ます。
if (table.contains(table_name, key_name)){
処理内容;
}
ローカル変数
複数のサブルーチンで処理を行う必要がある場合は req.http.*
ヘッダーに値を付与する必要がありますが、同一サブルーチン内での処理のために一時的に情報を保持する必要がある場合は、ローカル変数を利用することが出来ます。
ローカル変数は、以下のように宣言してから利用します。
declare local var.<name> <type>;
type は以下のものが指定可能です。
- BOOL
- FLOAT
- INTEGER
- IP
- RTIME (relative time)
- STRING
- TIME (absolute time)
宣言した変数を VCL で呼び出すときは var.<name>
と指定します。以下はローカル変数を宣言、値を収納、呼びだしてヘッダーに設定するサンプルコードです。
declare local var.x STRING;
set var.x = "string";
set req.http.x-variable = var.x;
ローカル変数の詳細については Local variables をご参照下さい。
ランダムな整数値の生成
randomint()
たとえば randomint(1, 10);
では1から10の整数をランダムに生成します。例えば以下のようにSurrogate-Keyにランダムな数字を入れておいて、Purge Allする際に徐々に実施するなどといった使い方も可能です。
set beresp.http.Surrogate-Key = randomint(1,10);
UUIDの生成
UUID を生成することができます。例えば以下のように利用することが出来ます。
set req.http.uuidv4 = uuid.version4();
詳細は UUID をご参照下さい。
JSONのエスケープ
json.escape()
JSONを安全にエスケープすることが出来ます。
ちょっと便利な VCL コードのサンプル
使用された Service のバージョンを確認
動作を検証しながら Service のバージョンをどんどん更新していると、リクエストが Service のどのバージョンで処理されたかを確認したいことがあります。
そんな場合は Snippet を利用して以下のコードを VCL に追加すると、クライアントへのレスポンスに、リクエストを処理するのに使用された Service のバージョン情報を含むヘッダが追加されます。
set resp.http.vcl = req.vcl.version;
レスポンスにデバッグに便利な情報を追加する
Service のバージョン以外にも挙動に関する各種情報がレスポンスヘッダに含まれていると curl などを利用した際にログを見なくてもレスポンスからある程度挙動に関する情報を確認することが出来て便利です。
デバッグ時や新しいサービスの作成時に確認できると便利な情報をレスポンスヘッダに含めるコードのサンプルをご紹介します。
Snippet
sub vcl_fetch {
if(req.backend.is_origin){
set beresp.http.fastly-backend-path = server.datacenter " -> " if(beresp.backend.name ~ ".*--(.*)", re.group.1, "");
} else {
set beresp.http.fastly-backend-path = server.datacenter " -> " beresp.http.fastly-backend-info;
}
}
sub vcl_deliver {
if (req.http.fastly-debug){
set resp.http.vcl = req.vcl.version;
set resp.http.fastly-restart = req.restarts;
set resp.http.fastly-info.state = fastly_info.state;
} else {
unset resp.http.fastly-backend-path;
}
}
上記のコードを Snippet としてサービスに追加すると、リクエストに適用された VCL のバージョン、利用されたオリジン、キャッシュヒットステータスの情報等がレスポンスヘッダーに追加されます。リクエストに fastly-debug ヘッダが存在しない場合はこれらの情報は追加されませんのでご注意下さい。
Custom VCL
Custom VCL を利用する場合は以下のコードを Custom VCL として追加するとデバッグ情報がレスポンスに追加されます。こちらのサンプルではリクエストに fastly-debug
ヘッダーが付与されている場合のみデバッグ情報をレスポンスヘッダーに追加して返却するようになっています。
sub vcl_recv {
#FASTLY recv
# Normally, you should consider requests other than GET and HEAD to be uncacheable
# (to this we add the special FASTLYPURGE method)
if (req.method != "HEAD" && req.method != "GET" && req.method != "FASTLYPURGE") {
return(pass);
}
# If you are using image optimization, insert the code to enable it here
# See https://www.fastly.com/documentation/reference/io/ for more information.
return(lookup);
}
sub vcl_hash {
set req.hash += req.url;
set req.hash += req.http.host;
#FASTLY hash
return(hash);
}
sub vcl_hit {
#FASTLY hit
return(deliver);
}
sub vcl_miss {
#FASTLY miss
return(fetch);
}
sub vcl_pass {
#FASTLY pass
return(pass);
}
sub vcl_fetch {
# For debug
if(req.backend.is_origin){
set beresp.http.fastly-backend-path = server.datacenter " -> " if(beresp.backend.name ~ ".*--(.*)", re.group.1, "");
} else {
set beresp.http.fastly-backend-path = server.datacenter " -> " beresp.http.fastly-backend-path;
}
#FASTLY fetch
# Unset headers that reduce cacheability for images processed using the Fastly image optimizer
if (req.http.X-Fastly-Imageopto-Api) {
unset beresp.http.Set-Cookie;
unset beresp.http.Vary;
}
# Log the number of restarts for debugging purposes
if (req.restarts > 0) {
set beresp.http.Fastly-Restarts = req.restarts;
}
# If the response is setting a cookie, make sure it is not cached
if (beresp.http.Set-Cookie) {
return(pass);
}
# By default we set a TTL based on the `Cache-Control` header but we don't parse additional directives
# like `private` and `no-store`. Private in particular should be respected at the edge:
if (beresp.http.Cache-Control ~ "(?:private|no-store)") {
return(pass);
}
# If no TTL has been provided in the response headers, set a default
if (!beresp.http.Expires && !beresp.http.Surrogate-Control ~ "max-age" && !beresp.http.Cache-Control ~ "(?:s-maxage|max-age)") {
set beresp.ttl = 3600s;
# Apply a longer default TTL for images processed using Image Optimizer
if (req.http.X-Fastly-Imageopto-Api) {
set beresp.ttl = 2592000s; # 30 days
set beresp.http.Cache-Control = "max-age=2592000, public";
}
}
# Override ttl for specified extensions
if(req.url.ext ~ "(?i)^(pdf|css|js)$") {
set beresp.ttl = 86400s;
}
# For debug
set beresp.http.ttl = beresp.ttl;
return(deliver);
}
sub vcl_error {
#FASTLY error
return(deliver);
}
sub vcl_deliver {
#FASTLY deliver
if (req.http.fastly-debug){
set resp.http.vcl = req.vcl.version;
set resp.http.fastly-restart = req.restarts;
set resp.http.fastly-info.state = fastly_info.state;
} else {
unset resp.http.ttl;
unset resp.http.fastly-backend-path;
}
return(deliver);
}
sub vcl_log {
#FASTLY log
}
※ レスポンスヘッダーに含まれる ttl については vcl_fetch で set beresp.http.ttl = beresp.ttl;
の処理を通らないと正しいTTLは設定されません。
Fastly Fiddle を利用したテスト
実際に記述した VCL コードをテストする方法としては以下の3種類あります。
Snippet と Custom VCL は実際の Fastly の配信設定に VCL のコードを追加し、その設定を Activate する必要がありますが、3つめの Fiddle は VCL のテストツールで、サービスを実際に Activate することなく挙動を簡単にテストすることが出来ます。以下が実際の Fiddle ツールの画面になります。
Fiddle に記載したコードは特にアクセス制限などがかかるわけではないので、誰からでもアクセスされる可能性があります。パスワードや機密情報など、外部に漏れて困る内容はコードに含めないように注意して下さい。
ツールの基本的な使い方は以下の通りです。
- Origin Servers にコンテンツを取得するオリジンサーバーを指定(デフォルトのままでも大丈夫です)
- Your VCL Code と記載されている箇所の各サブルーチンの下のテキストフィールドに VCL のコードを記入
- 右側の Configure requests の下のフィールドにテストしたいリクエストのパスを記入
- 右側のRunボタンをクリック
しばらくすると、リクエストが処理され以下のような通信に含まれるヘッダー情報が右側に表示されます。
Request to Fastly
Request to Origin
Response from Origin
Response from Fastly
ここで VCL で追加したヘッダーなどは黄色で色掛けされて強調表示されます。
一部制約はありますが、このツールを利用することで、VCL のバージョンを変更したり Activate することなく手軽にちょっとしたコードの動作をテストすることが出来ます。
Tip 1
Run ボタンをクリックする際に Shift キーを押したままクリックすると、リクエストを送信する前に Purge 処理が行われます。キャッシュがない状態の挙動を確認することができます。
Tip 2
Configure Requests のテキストボックスの下の各リンクからリクエストの各種パラメータなどを変更することが出来ます。よく使用されるものとして、Headers に情報を入力することでリクエストに指定した任意のヘッダーを付与してリクエストを送信することが出来ます。
ではここまでの内容を踏まえて、リクエストヘッダーに値を追加する処理を書いてみたいと思います。
Fiddle の RECV のフィールドに以下のコードを記述して Run をクリックして下さい。
set req.http.X-test = "a";
Request to Origin に x-test: a
が付与されていればコードは正しく処理されています。
ただ、この内容ではあまり意味がないのでもう少し役に立ちそうなサンプルを考えてみます。
RECV に次の内容を追記してみて下さい。
set req.http.X-country = client.geo.country_code;
client.geo.country_code
という変数には国情報が入っています。上記のコードを1行追加することでリクエストにクライアントの国情報が追加されますので、オリジンサーバーはこのヘッダーを参照することでクライアントがどの国からアクセスしてきたかを判断することが出来るようになります。
Fiddle では個別の URL が生成されるので、記入したコードを保存して他の人と共有することも可能です。例えば、以下のリンクをクリックすると上記のヘッダー情報を追加するサンプルにアクセスすることが出来ます。
Fiddle に記載したコードは特にアクセス制限などがかかるわけではないので、誰からでもアクセスされる可能性があります。パスワードや機密情報など、外部に漏れて困る内容はコードに含めないように注意して下さい。
まとめと参考リンク
この記事では Fastly で VCL を記載するにあたって最初に理解しておくべき内容と手軽なテスト方法について紹介しました。どんな複雑な処理もこのページで紹介した内容の応用なので、まずはこのページの内容を少し変更したりしながら色々と試してみると理解が早いと思います。
また、この記事を読んでいただいた方には以下の記事も参考になると思います。
Fastly VCL 入門2 - Fiddleツールを使って実際に設定を作成する手順を紹介しています。
Fastly VCL ベストプラクティス - 公式の VCL Best Practice の翻訳です
その他ここでは紹介していない Fastly 独自の便利な関数などもあるので、何か気になることがあれば Fastly の公式ドキュメント も参照してみて下さい。