目次

  1. はじめに
  2. Fastly Compute とは
  3. なぜ VCL から Compute へ?
  4. 移行戦略
  5. 移行で得た恩恵
  6. おわりに

はじめに

株式会社 WinTicket のエンジニアで、Web チームに所属している谷中一生(@azoookid)です。

WINTICKET Web 版(以降 WINTICKET Web)は、SSR(サーバーサイドレンダリング)と CSR(クライアントサイドレンダリング)を組み合わせた Web アプリケーションです。WINTICKET はアプリ版も提供していますが、アプリ内 WebView を含め WINTICKET Web には数多くのアクセスがあります。そのため、CDN として Fastly を利用することで、高パフォーマンスとオリジンサーバーへの負荷低減を実現しています。

WINTICKET Web 独自のキャッシュ戦略を実現したり、キャッシュ効率そのものを高めたりするためのロジックを VCL(Varnish Configuration Language)で記述し、そのメンテナンスを続けてきました。しかし、サービス規模の拡大に伴い、VCL への課題感が増してきたため、より柔軟な処理記述が可能な Fastly Compute への移行を決断しました。

本記事では、ユーザー影響が直に出やすい CDN の移行をいかに「段階的かつ安全に」進めたのか、私たちが実際に工夫した方法を紹介します。

Fastly Compute とは

Fastly Compute は、プログラミング言語を使用した実装を WebAssembly にコンパイルすることでエッジ上で実行できるコンピューティング基盤です。

Fastly VCL サービスと同様にコンテンツキャッシュが可能で、キャッシュレイヤーとして使用できます。一方で、スケーリングを気にしない軽量でシンプルな HTTP エンドポイントとしても活用できます。

最終的に WASM にコンパイルできれば多種多様な言語で実装可能ですが、公式 SDK としては Rust、Go、JavaScript がサポートされています。

なぜ VCL から Compute へ?

VCL への課題感

WINTICKET Web は

  • アプリケーションサーバーと静的ファイル配信サーバーの分岐
  • キャッシュ効率を高めるための、Vary ヘッダの操作
  • 検証環境へのアクセス制御

など様々な処理を CDN 上で行っており、カスタム VCL ファイルは単体で 500 行を超える状態にありました。

検証環境へのアクセス制御に関して、詳しくはCloud RunサイドカーとURLマスクを活用したWINTICKET Webの検証環境改善をご覧ください。

その上で、チームとして VCL に対して以下のような課題を感じていました。

学習コストの高さ

VCL ロジックを適切にメンテナンスするには、VCL 独自のライフサイクルや構文の理解が不可欠です。

Web アプリだけでなく CDN に関してもオーナーシップを持つ体制の WINTICKET Web チームにおいて、VCL や Fastly そのもののライフサイクルを理解することへのハードルから、結果的に CDN に関するメンテナンスが一部メンバーに限られてしまう状態でした。

Web エンジニアに馴染み深い JavaScript の公式 SDK を提供している Fastly Compute の導入によって、CDN 層を触るハードルを少しでも下げられるのではないかと考えました。

メンテナンスコストの高さ

基本的に VCL ロジック自体のテストは困難です。

falco という VCL の独自パーサーを実装している OSS を使用してユニットテストを導入していましたが、一部の read-only な値のオーバーライドが難しく、テストできないケースも存在していました。そのため、ロジック変更時に十分な検証ができないという課題がありました。

このテストのしづらさも、CDN ロジックをプログラミング言語で実装し、適切にモジュール化することで、解決できうる課題です。

A/B テストへの活用

今後の展望として、WINTICKET Web 単体での A/B テスト実現を目指しています。Fastly Compute への移行により、これが実現しやすくなるという点も、移行を決めた大きな理由の1つです。

SSR を行いつつ CDN キャッシュも活用するアプリケーションで A/B テストを実現するには、CDN 上でリクエストごとに外部エンドポイントへ問い合わせ、ユーザーに応じた A/B 情報を取得する必要があります。VCL でもこのような処理は可能ですが、プログラミング言語で記述できる Fastly Compute の方が、より柔軟かつ保守しやすい実装が可能です。

なお、この PoC 実装は弊社インターン生が行ってくれました。詳しくは2週間のインターンで SSR・CDN 環境下での A/B テスト基盤を構築してみたをご覧ください。

移行戦略

CDN の改修は、最悪ユーザー間のキャッシュの混同やサービスダウンにつながるリスクの大きな操作です。
WINTICKET は公営競技の投票サービスであり、お客様の個人情報を扱うほか、昼夜絶え間なくレースがあり、移行によるダウンタイムも避ける必要がありました。

そこで、ユーザーへの影響を最小限に抑えるべく、VCL から Compute への移行は段階的に行うことにしました。

以下、その段階移行手法について詳しく解説します。

ページ単位での段階適用

アクセスされるページパス単位で、少しずつ Compute サービスを本番適用していく方針を採用しました。

以下のようなイメージです。

  • ① 特定のページパス:Compute サービスへルーティングする
  • ② その他のページパス:既存 VCL サービスのロジックを実行する

これを実現するために、Service Chaining という、複数の Fastly サービスをチェーンさせる手法を用いました。

具体的には、このように構成を変更しました。

Before

VCL サービス → オリジン

After

VCL サービス → (①に該当するパスのみ)Compute サービス → オリジン

この構成の上で以下の要件を実現する必要がありました。

  • ①に該当するパスの場合:VCL ではキャッシュもロジック実行も行わず、Compute サービスにその責務を完全に委ねる
  • ②に該当するパスの場合:VCL では今まで通りキャッシュもロジック実行も行い、Compute サービスへはリクエストを流さず直接オリジンへリクエストする

前述のメンテナンスコストの課題もあり、今回の移行においても安全性の観点から極力カスタム VCL ファイルには変更を加えたくありませんでした。

しかし、この移行戦略を実現するためには、前段の既存 VCL サービスにて①に該当するパスかを判定し、その場合は VCL では何もしない処理を追加する必要があります

Conditions、Request settings、Cache settings で VCL のキャッシュを制御する

既存のカスタム VCL ファイルを触ることなく前述の制御を行うために、Fastly VCL サービスの Conditions、Request settings、Cache settings を活用しました。

VCL サービスにおける Conditions に関して、詳しくはFastly ドキュメンテーション/条件の使用をご参照ください。

まず、req.url.path からマッチするパスかどうかを判定する Condition を作成します。

次に、その Condition に対して Action を Pass(do not cache)に設定した Request settings を作成します。Request settings は、Fastly へのリクエストのハンドリングをカスタマイズできる設定です。この設定は最終的に出力される VCL ファイルの vcl_recv サブルーチンに追加され、カスタム VCL の記述より前に発火してくれるため、今回の用途に最適でした。

Fastly コンソールの Request Settings の設定画面。req.url.path ~ '^/keirin'にへのリクエストに対して Action として Pass(do not cache) を指定している

画像のような設定で、以下の VCL が生成されます。これにより、特定のパスにおいて vcl_recv のカスタムロジックを発火させず、return(pass) でキャッシュをスキップできました。

sub vcl_recv {
#--FASTLY RECV BEGIN

  ...中略

  if (req.url.path ~ "^/keirin") {
    set req.backend = COMPUTE_SERVICE; # Compute サービスへルーティング
    return(pass); # キャッシュしないようにする
  }

  ...カスタム VCL ロジック
}

Fastly コンソールの Cache Settings の設定画面。req.url.path ~ '^/keirin'にへのリクエストに対して Action として Pass(do not cache) を指定している

同様に、Action を Pass(do not cache)に設定した Cache settings を作成することで、vcl_fetch でもカスタムロジックの発火を無効化できます。

sub vcl_fetch {
#--FASTLY FETCH BEGIN

  ...中略

  if (req.url.path ~ "^/keirin") {
    set beresp.ttl = 0s;
    return(pass); # キャッシュしないようにする
  }

  ...カスタム VCL ロジック
}

WINTICKET Web の VCL カスタムロジックは、ほとんど vcl_recvvcl_fetch に集約されていました。そのため上記の設定により、カスタム VCL ファイル自体への変更なしに、特定条件において VCL 上でほとんど何もしないことを実現できました。

※ 正確には vcl_passvcl_deliver などは実行されます。

最終的な移行時の構成図は以下になります。

ompute移行の最終アーキテクチャー図。VCLサービスにて特定のパスに該当するかを判定する。該当する場合がComputeを通じてオリジンサーバーへリクエストを転送し、そうでない場合は既存通りVCLのロジックを発火させている

Compute サービス適用範囲を段階的に拡大

新 Compute サービスへルーティングされるパスを増やす際は、先ほどの Conditions にマッチするパスを追加していくだけです。

先ほどは Fastly コンソールの UI を例に解説しましたが、該当 VCL サービスは IaC(Terraform)で管理していたため、適用範囲パスも Pull Request を通して安全に変更できました。

Terraform では以下のように管理します。

resource "fastly_service_vcl" "service" {
  ...中略

  # vcl_recv でキャッシュしないように
  condition {
    name      = "to compute request"
    type      = "REQUEST"
    # ここに適用パスを追加していく
    statement = "req.url.path ~ \"^/keirin\""
  }
  request_setting {
    name             = "to compute request"
    action           = "pass"
    request_condition = "to compute request"
  }

  # vcl_fetch でキャッシュしないように
  condition {
    name      = "to compute cache"
    type      = "CACHE"
    # ここに適用パスを追加していく
    statement = "req.url.path ~ \"^/keirin\""
  }
  cache_setting {
    name            = "to compute cache"
    action          = "pass"
    cache_condition = "to compute cache"
  }
}

パスの適用は、約10段階に分けて少しずつ行いました。はじめはユーザーが直接アクセスすることの少ないページから適用し、特にユーザー情報を含むキャッシュが混在すると大事故になりかねないページへの適用は細分化することで、事故リスクを低減しました。
また、Compute 適用パスを増やしていく際は、本番公開前に Fastly の Staging機能 を活用し、ローカル環境からキャッシュ周りの最終動作検証を行うことで安全性を担保しました。

ゼロダウンタイム移行

ダウンタイムなしでの移行を実現するために、チェイン先の Compute サービスのドメインをワイルドカードドメインにしました。

ユーザー、VCLサービス(www.winticket.jp)、Computeサービス(*.winticket.jp)、オリジンサーバーの順でリクエストが流れる図

そして適用パスを徐々に増やしていき、すべてのパスが Compute サービスに適用されたら、VCL サービスを deactivate します。これにより、www.winticket.jp ドメインへのアクセスが自動的に *.winticket.jp の Compute サービスへルーティングされるようになります。

VCLサービスはdeactivateされている。ユーザー、Computeサービス(*.winticket.jp)、オリジンサーバーの順でリクエストが流れる図。

その後、Compute サービスのドメインを www.winticket.jp に変更することで、ゼロダウンタイムでの移行を実現できました。

移行で得た恩恵

メンテナンス性の向上

プログラミング言語で CDN ロジックを記述できるようになったことで、メンテナンス性が大幅に向上しました。テストが書けるようになり、また TypeScript という Web エンジニアが慣れ親しんだ言語を使用できることで、チームとしても CDN ロジックのメンテナンスへのフットワークが軽くなりました。

柔軟なログ送信

従来、VCL の構文で取得できる値のみログ送信が可能でした。一方で、Compute ではプログラマティックなログ送信が可能です。たとえば、特定のリクエストに対してのみ、ユーザー ID などの任意の情報を付与してログ送信するといったことが簡単に実現できます。

エッジデータストレージによる動的データの扱いやすさ

Compute サービスは、Config Store、KV Store、Secret Store といったエッジ上のデータストレージからシームレスかつ高速にデータの読み書きができます。

特に、これまで VCL に直接埋め込んでいた環境依存の値を Config Store に格納することで、コード内の環境分岐が減り、可読性が向上しました。さらに、Config Store を Feature Toggle のように扱い、必要なタイミングでコンソールから値を変更することで機能の出し分けが動的にできるようになったことも利点の1つです。

おわりに

以上のようなステップで、WINTICKET Web は長年運用してきた VCL に別れを告げ、Fastly Compute へ完全移行しました。

VCL に比べ Fastly Compute は移行事例が少なく、苦労する点も多かったですが、Fastly サポートエンジニアの方々に多大なサポートをいただき、今回の移行を実現できました。この場を借りて御礼申し上げます。

また、本記事の内容で Fastly 様主催技術イベント「Fastly Yamagoya Night」にて登壇もさせていただきました。ご興味がある方は登壇資料も併せてご覧ください。

次回は、移行後の JavaScript SDK を使用した Fastly Compute アプリケーションの実装についてご紹介する予定です。

お楽しみに!