🦁

【Fastly】VCL 記述方法と使い分けのベストプラクティス

2024/10/05に公開

はじめに

Fastly は、Vernish Configuration Language (VCL) と呼ばれる設定記述言語によってエッジサーバーの挙動を細かく制御できることが特徴の CDN サービスです。
VCL の記述方法は

  • Service configuration: GUI / API を通じて、Fastly が提供する設定を操作する方法
  • VCL snippets: VCL のコードブロックを埋め込む方法
  • Custom VCL: VCL の各サブルーチンの挙動を丸ごと入れ替える方法

の3種類があり、この3種類を混合して柔軟な設定を組むことができますが、逆に言えばどれを使うべきか迷うことがあるかと思います。

本記事では、Service configuration・VCL snippets・Custom VCL のそれぞれの特徴と、どのように使い分けるのが良いかを説明します。

VCL 記述方法の概要と特徴

この章では、まず Fastly において VCL がどのような仕様になっているかを説明した上で、Service configuration・VCL snippets・Custom VCL の各 VCL 記述方法について説明していきます。

Fastly における VCL の取り扱い

Fastly の VCL では、エッジサーバーの「状態」ごとに対応するサブルーチンが用意されており、各状態における挙動はそのサブルーチンで記述します。たとえばリクエストを受け付けた直後の RECV 状態での挙動は vcl_recv、バックエンドにコンテンツを取りにいった直後の FETCH 状態での挙動は vcl_fetch とというサブルーチンで処理されます。

Fastly VCL の状態遷移図。 Fastly Documentation: Using VCL より引用

Fastly において使用できる状態とサブルーチンの種類は以下のとおりです。

状態名 対応するサブルーチン 呼び出されるタイミング
RECV vcl_recv リクエストを受信したとき。すべてのリクエストは最初にこの状態になる
HASH vcl_hash キャッシュキーを計算するとき
HIT vcl_hit HASH にて計算したキーでキャッシュがヒットしたとき
MISS vcl_miss HASH にて計算したキーでキャッシュがヒットしなかったとき。バックエンドとの通信を準備する
PASS vcl_pass キャッシュを無視する命令が呼ばれたとき。バックエンドとの通信を準備する
FETCH vcl_fetch バックエンドからレスポンスを受信したとき
ERROR vcl_error エラー命令が呼び出されたとき。ほぼすべての状態から遷移する可能性がある
DELIVER vcl_deliver クライアントにレスポンスを返す準備を始める時
LOG vcl_log クライアントにレスポンスを返し終わったとき

これらのサブルーチンは、後述する Service configuration・VCL snippets・Custom VCL のいずれの方法でも挙動を記述できますが、最終的には1つの VCL ファイルに記述がまとめられ、エッジサーバーにデプロイされます。
どのような VCL ファイルが生成されているかは、CDN サービス設定画面の「Show VCL」ボタンから参照することができます。

Fastly 管理画面の Service configuration にて、「Show VCL」を押した時の画面。設定内容が VCL に変換され、Fastly の決めた順番に従って1つのファイルに合成される

Service configuration


Service configuration の「Domain」を選んだ際の画面。GUI で細かく設定を加えることが可能

Service configuration は VCL 記述方法の1つで、Fastly によって用意された設定項目を設定することで自動で VCL を記述してくれます。GUI で直観的な操作ができる他、API や CLI 経由でも設定を操作することができます。

Service configuration には以下の設定項目があります。

項目名 説明
Domains CDN にアクセスするドメインを登録する。ドメインはあらかじめ他の DNS サービスで発行しておき、CNAME で Fastly 宛に向けさせる必要がある(設定方法の詳細はこちら
Origins CDN のバックエンドを登録する。HTTP(S) 通信でコンテンツを取りにいければどのような形態でも良い
Settings CDN の挙動を制御する。IPアドレスをブロックする・TLS通信を強制する・キャッシュ戦略をコントロールするなどが可能
Content ヘッダーやレスポンスに情報を追加 / 置換する
Logging ログエクスポートの設定を行う。いくつか PaaS の選択肢が用意されている他、FTP・SFTP・HTTPSといったプロトコルを介してのエクスポートにも対応
VCL snippets / Custom VCL 独自の設定を VCL を用いて記述する。後述
Conditions 設定を適用する条件付けを行う。各種設定にアタッチする形で使用
Data アクセス権制御を行ったり(Access control lists)key-value 形式のデータを保持したり(Dictionaries)する

Conditions の存在により、Service configuration だけでもかなり柔軟な設定が可能です。たとえば、以下のように Origins の設定に Conditions をアタッチすることで、「リクエスト URL の先頭が /var の場合は storageA を、その他の場合は storageB を使用」といったことが可能になります。

URL パスによるストレージ選択の例。storageA に「Path is VAR」条件をアタッチすることで、/var/*の時のみ storageA を使用するようにできる

VCL snippets

VCL snippets では、各サブルーチン内に VCL で記述された独自の処理を加えることができます。
下画像のように挿入する先のサブルーチンと挿入する VCL の内容を記述することで、対象のサブルーチン内に 記述した VCL を埋め込むことができます。


VCL snippet 作成サンプル。vcl_fetch にて、ファイルの拡張子ごとにキャッシュの TTL を制御している

各 VCL snippets は優先度の概念があり、同サブルーチン内の VCL snippets や一部の Service configuration に対して順番を制御することが可能です。ただし、Service configuration やデフォルト設定の中には VCL snippets と処理の順番を入れ替えられないものも存在するため、「Show VCL」で期待する順番通りに記述されているかどうか確認する必要があります。

Custom VCL とマクロ

Custom VCL では、VCL ファイルをアップロードすることで、 VCL ファイル内に記述されている各サブルーチンの処理を丸ごと独自の処理に置き換えることができます。
何もしなければ Service configuration や VCL snippets で記述したサブルーチンの処理は Custom VCL によって上書きされるのですが、各サブルーチン内で #FASTLY (状態名) というマクロを記述すると、Custom VCL 内に Service configuration や VCL snippets の設定を展開することができます。

たとえば、VCL snippets の項の画像にあるvcl_fetch の VCL snippet を作成している状態で、以下のような Custom VCL をアップロードします。

Custom VCL
sub vcl_fetch {
    ...
    #FASTLY FETCH
    ...
}

すると、 #FASTLY FETCH の部分が展開され、合成された VCL 上では以下のように表示されます。

合成される VCL
sub vcl_fetch {
    ...

    #--FASTLY FETCH BEGIN
    # record which cache ran vcl_fetch for this object and when
      set beresp.http.Fastly-Debug-Path = "(F " server.identity " " now.sec ") " if(beresp.http.Fastly-Debug-Path, beresp.http.Fastly-Debug-Path, "");
    # generic mechanism to vary on something
      if (req.http.Fastly-Vary-String) {
        if (beresp.http.Vary) {
          set beresp.http.Vary = "Fastly-Vary-String, "  beresp.http.Vary;
        } else {
          set beresp.http.Vary = "Fastly-Vary-String, ";
        }
      }

    # Snippet Sample VCL snippet : 100
    if (req.url.ext ~ "m3u8") {
      set beresp.ttl = 0s;
      set beresp.grace = 0s;
      return(deliver);
    } else if (req.url.ext ~ "m4s") {
      set beresp.ttl = 600s;
      set beresp.grace = 10s;
      return(deliver);
    } else {
      set beresp.ttl = 31356000s;
      set beresp.grace = 10s;
      return(deliver);
    }

    #--FASTLY FETCH END

    ...
}

マクロよりも前に書かれた処理はマクロよりも先に、後に書かれた処理はマクロよりも後に実行されます。

注意する必要があるのは、Custom VCL を使用する場合に、マクロを取り入れていたとしても消失するデフォルト設定があるということです。
補足の章に具体的な内容を記述していますが、特に vcl_hash を Custom VCL で上書きする際は、デフォルトのハッシュキー計算方法が消失していることに特に注意する必要があります。

VCL 設定方法のベストプラクティス

この章では、前章で説明した内容を基にどのようにして VCL 設定方法を使い分ければ良いかを説明していきます。

自動生成された VCL を「Show VCL」で必ず確認しよう

Fastly の CDN サービスの動作は、どのような形で VCL を記述していても、最終的には「Show VCL」で確認できる VCL を基に決まります。特に複数の設定方法を併用している場合は、

  • べき等でない処理の記述が重複し、ヘッダーに意図しない値が入ったり意図しない遷移が発生したりする
  • Custom VCL にマクロを入れ忘れ、Service configuration や VCL snippets の設定が抜け落ちる

といったミスが起きやすくなります。
VCL 記述順番に問題がないか・記述に重複がないかなどは、すべて「Show VCL」で確認してからにしましょう。

基本的には Custom VCL で設定を記述しよう

多くの設定は Custom VCL で記述するのが一番柔軟性が高く、Git での管理もしやすいため、何か特別な理由がない限りは Service configuration に設定項目があっても Custom VCL を利用しましょう。

たとえば、リクエスト URL パスに応じて自動生成したレスポンスを返したいとき、Fastly によって予約されていないレスポンス番号(600番台)を利用して内容の出し分けが可能です(Fastly Documentation: HTTP status codes and Fastly を参照)。
このとき、非自明なレスポンス番号が複数のサブルーチンに渡って利用されるため、VCL snippets のように単一のサブルーチンに対する記述を複数要すると管理が大変になります。Custom VCL であれば、複数のサブルーチンに渡って記述が可能なので管理がしやすくなります。

sub vcl_recv {
    #FASTLY RECV
    ...
    if (req.url.path == "/") {
        error 601;
    }
    if (req.url.path == "/robots.txt") {
        error 602;
    }
}

sub vcl_error {
    #FASTLY ERROR
    ...
    if (obj.status == 601) {
        set obj.status = 200;
        set obj.response = "OK";
        synthetic {"
            # index.html を記述
        "};
        return(deliver);
    } else if (obj.status == 602) {
        set obj.status = 200;
        set obj.response = "OK";
        synthetic {"
            # robots.txt を記述
        "};
        return(deliver);
    }
}

複雑な設定は Service configuration を活用しよう

設定の中には Custom VCL で設定できないか、できたとしても複雑で間違えやすいということがあります。この場合は Service configuration を活用しましょう。
特に以下に挙げる設定は、Service configuration で設定することを強くお勧めします。

  • Domains
    Custom VCL での設定ができない
  • Origins
    多くのパラメータを設定する必要があり、管理が煩雑。また、バックエンドにクラウドサービスを利用している場合は Shielding が絡んできて、さらに煩雑になる
  • Logging
    ログの送信先は Custom VCL から設定ができない。また、ログフォーマットを Custom VCL で手打ちしようとすると非常に複雑で、ログが意図しない形式で出力されかねない

VCL snippets を利用する場面

VCL snippets は処理の優先順位の管理が大変だったり、Custom VCL と処理を重複して書いてしまったりするリスクがあるため、ほとんどの場合では使用をしなくてよいかと思います。
ただし、以下の状況では VCL snippets を利用すべきです。

他の VCL 設定とは異なるタイミングで一部設定を更新したい場合

たとえば不正アクセス対策として認証システムを一定時間ごとに変更したい場合、認証システムを変更するたびに VCL 設定全体の更新をするのは動作検証の側面からよくありません。
この場合、Dynamic VCL snippets と呼ばれる機能がうまく使用できます。Dynamic VCL snippets では他の VCL の更新からは切り離して Dynamic VCL snippets の部分だけを更新することができるので、他の部分に影響することなく認証システムを入れ替えることができます。

まとめ

Fastly において VCL を記述する方法は、

  • Service configuration: GUI / API を通じて、Fastly が提供する設定を操作する方法
  • VCL snippets: VCL のコードブロックを埋め込む方法
  • Custom VCL: VCL の各サブルーチンの挙動を丸ごと入れ替える方法

の3種類がありました。
基本的には Custom VCL に Fastly のマクロを展開した上で設定を記述し、ドメインの設定や複雑になりすぎる設定は Service configuration を、その他特殊な状況に限り VCL snippets を利用しましょう。

補足

Custom VCL を設定することによって消失する設定

vcl_recv

    if (req.request != "HEAD" && req.request != "GET" && req.request != "FASTLYPURGE") {
      return(pass);
    }

GET・HEAD・PURGE 以外はキャッシュチェックを省略する処理が消失します。

vcl_fetch

  declare local var.fastly_disable_restart_on_error BOOL;

  if (!var.fastly_disable_restart_on_error) {
    if ((beresp.status == 500 || beresp.status == 503) && req.restarts < 1 && (req.request == "GET" || req.request == "HEAD")) {
      restart;
    }
  }
  if(req.restarts > 0 ) {
    set beresp.http.Fastly-Restarts = req.restarts;
  }
  if (beresp.http.Set-Cookie) {
    set req.http.Fastly-Cachetype = "SETCOOKIE";
    return (pass);
  }
  if (beresp.http.Cache-Control ~ "private") {
    set req.http.Fastly-Cachetype = "PRIVATE";
    return (pass);
  }
  if (beresp.status == 500 || beresp.status == 503) {
    set req.http.Fastly-Cachetype = "ERROR";
    set beresp.ttl = 1s;
    set beresp.grace = 5s;
    return (deliver);
  }
  if (beresp.http.Expires || beresp.http.Surrogate-Control ~ "max-age" || beresp.http.Cache-Control ~"(?:s-maxage|max-age)") {
    # keep the ttl here
  } else {
        # apply the default ttl
    set beresp.ttl = 3600s;
  }

以下の設定が消失します。

  • オリジンが500・503エラーを返した時のリトライ設定
  • キャッシュさせない設定
  • キャッシュのデフォルト TTL 設定

vcl_hit

  if (!obj.cacheable) {
    return(pass);
  }

キャッシュオブジェクトがキャッシュ可能でない場合はキャッシュを無視する処理が消失しています。
ただし、 obj.cacheablevcl_hit 内で常に true なので、消失してもよい設定かと思います。

vcl_hash

    set req.hash += req.url;
    set req.hash += req.http.host;

キャッシュのハッシュキー計算で req.url と req.http.host を含めるデフォルト設定が消失します。

Discussion