LoginSignup
69
58

More than 1 year has passed since last update.

Fastly VCL 入門

Last updated at Posted at 2018-08-31

#はじめに
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-request-flow.png

※ Fastly では配信拠点内でキャッシュを効果的に利用するためクライアントからリクエストを受けるサーバー(Edge Node)とオリジンからコンテンツを取得するサーバー(Cluster Node)が異なります。

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/ の場合は 1dir2 の場合は2、それ以外はother を値を含む X-type というリクエストヘッダーを追加します。

vcl_recv
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 ヘッダーを追加します。

vcl_recv
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 ではレスポンスのステータスコードを拾うことが出来ます

vcl_fetch
  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 にヘッダーを指定すると指定した値で既存の値を上書きするためヘッダーに元々含まれていた値は失われます。上書きでなく追記したい場合は、以下のようなコードで実現することが出来ます。

vcl_fetch
  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行のコードに置き換えることが出来ます。

vcl_fetch
set beresp.http.Vary = if(beresp.http.Vary, beresp.http.Vary + ",", "") + "value2";

if(beresp.http.Vary, beresp.http.Vary + ",", "") の部分でオリジンからのレスポンスに vary が存在した場合は元々の vary の値と,を、存在しない場合は""(null)を設定し、その後に value2 を付与しています。
+ は文字列を結合するのに利用しますが、なくても動作します。

更に別の書き方として以下のようなコードでも同じように既存ヘッダーに値を追加することが出来ます。

vcl_fetch
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 ヘッダに追加されます。

正規表現による値の取得

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は条件にマッチするすべての文字列を置き換えます。

regsubregsuballの使い方については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 に追加すると、クライアントへのレスポンスにバージョン情報を含むヘッダが追加されます。

vcl_init
sub vcl_recv {
  set req.http.vcl = req.vcl;
}

sub vcl_deliver {
  set resp.http.vcl = req.http.vcl;
}

レスポンスに含まれるヘッダは以下のようなフォーマットで、サービス ID の後ろにリクエストで利用されたサービスのバージョンが含まれます。

vcl: <service_id>.XXX_YYYYYYYYYYYYYY

レスポンスにデバッグに便利な情報を追加する

Service のバージョン以外にも挙動に関する各種情報がレスポンスヘッダに含まれていると curl などを利用した際にログを見なくてもレスポンスからある程度挙動に関する情報を確認することが出来て便利です。

デバッグ時や新しいサービスの作成時に確認できると便利な情報をレスポンスヘッダに含めるコードのサンプルをご紹介します。

vcl_init
sub vcl_recv {
  set req.http.vcl = if(req.vcl ~ ".*\.(.*)_.*", re.group.1, "");  
}

sub vcl_fetch {
  if(req.backend.is_origin){
    set beresp.http.fastly-backend-info = server.datacenter " -> " if(beresp.backend.name ~ ".*--(.*)", re.group.1, "");
  } else {
    set beresp.http.fastly-backend-info = server.datacenter " -> " beresp.http.fastly-backend-info;    
  }
}

sub vcl_deliver {
  set resp.http.fastly-vcl = req.http.vcl;
  set resp.http.fastly-req.restart = req.restarts;
  set resp.http.fastly-info.state = fastly_info.state;
}

上記のコードをサービスに追加すると、リクエストに適用された VCL のバージョン、利用されたオリジン、キャッシュヒットステータスの情報がレスポンスヘッダーに含まれるようになります。

開発するサービスの挙動によって必要な情報は異なってくると思いますので、追加したい情報は自由に追加して使うことが出来ます。

Fastly Fiddle を利用したテスト

実際に記述した VCL コードをテストする方法としては以下の3種類あります。

Snippet と Custom VCLは実際の Fastly の配信設定に VCL のコードを追加し、その設定を Activate する必要がありますが、3つめのFiddleというFastly のエンジニアがベータ提供しているツールを使うことで、VCL の挙動を簡単にテストすることが出来ます。以下が実際の Fiddle ツールの画面になります。

Fiddle_-_Fastly.jpg

Fiddle に記載したコードは特にアクセス制限などがかかるわけではないので、誰からでもアクセスされる可能性があります。パスワードや機密情報など、外部に漏れて困る内容はコードに含めないように注意して下さい。

ツールの基本的な使い方は以下の通りです。

  1. Origin Servers の下にコンテンツを取得するオリジンサーバーを指定(デフォルトのままでも大丈夫です)
  2. Custom VCL と記載されている箇所の各サブルーチンの下のテキストフィールドにVCLのコードを記入
  3. 右側のSend a request の下のフィールドにテストしたいリクエストのパスを記入
  4. 右側のRunボタンをクリック

しばらくすると、リクエストが処理され以下のような通信に含まれるヘッダー情報が右側に表示されます。

Request to Fastly
Request to Origin
Response from Origin
Response from Fastly

ここで VCL で追加したヘッダーなどは黄色で色掛けされて強調表示されます。
一部制約はありますが、このツールを利用することで、VCL のバージョンを変更したり Activate することなく手軽にちょっとしたコードの動作をテストすることが出来ます。

ではここまでの内容を踏まえて、リクエストヘッダーに値を追加する処理を書いてみたいと思います。

Fiddle の RECV のフィールドに以下のコードを記述して Run をクリックして下さい。

vcl_recv
set req.http.X-test = "a";

Request to Origin に x-test: a が付与されていればコードは正しく処理されています。

ただ、この内容ではあまり意味がないのでもう少し役に立ちそうなサンプルを考えてみます。
RECV に次の内容を追記してみて下さい。

vcl_recv
set req.http.X-country = client.geo.country_code;

client.geo.country_code という変数には国情報が入っています。上記のコードを1行追加することでリクエストにクライアントの国情報が追加されますので、オリジンサーバーはこのヘッダーを参照することでクライアントがどの国からアクセスしてきたかを判断することが出来るようになります。

実は Fiddle では記入したコードを保存して、他の人と共有する機能もあります。以下のリンクをクリックすると上記のヘッダー情報を追加するサンプルにアクセスすることが出来ます。

Fiddle でヘッダーを追加するサンプル

Fiddle に記載したコードは特にアクセス制限などがかかるわけではないので、誰からでもアクセスされる可能性があります。パスワードや機密情報など、外部に漏れて困る内容はコードに含めないように注意して下さい。


この記事では Fastly で VCL を記載するにあたって最初に理解しておくべき内容と手軽なテスト方法について紹介しました。どんな複雑な処理もこのページで紹介した内容の応用なので、まずはこのページの内容を少し変更したりしながら色々と試してみると理解が早いと思います。

また、 Fastly VCL 入門2 にて Fiddleツールを使って実際に設定を作成する手順を紹介していますので、こちらも参考にして頂けると幸いです。

その他ここでは紹介していない Fastly 独自の便利な関数などもあるので、何か気になることがあれば Fastly の公式ドキュメント も参照してみて下さい。

69
58
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
69
58