見出し画像

Fastly Compute@Edge を用いた CDN の構築

こんにちは、株式会社カウシェの Architect の伊藤です。
本稿では、カウシェが CDN の構築に利用している Fastly Compute@Edge について、「採用した理由」や「テストやデプロイの方法」などを実装例を混じえてご紹介します。

伊藤 雄貴 / @yuki.ito
DeNA を経て 2018 年にメルペイにジョインし、Tech Lead や Architect としてマイクロサービスの開発や組織横断的な技術課題の解決に携わる。カウシェには立ち上げのタイミングから副業として参画し Backend 全般の実装を行う。2022 年 7 月より Architect としてカウシェに正式にジョインし、全社的な技術戦略の意思決定や技術基盤の構築に携わる。

Fastly Compute@Edge とは

Fastly Compute@Edge は Fastly が提供しているコンピューテーションプラットフォームです。
自身で生成する WebAssembly(Wasm) バイナリを Fastly のエッジ上で動作させることができ、Wasm バイナリでバックエンドに対する HTTP リクエスト・レスポンスに変更を加えたり Fastly 上でのキャッシュの設定を行えたりします。
また、既存の Varnish Configuration Language(VCL)ベースの Service では難しい複雑な処理を Rust や JavaScript といった汎用言語で記述することができます。 Fastly 上では VCL ベースの Service と同様に、Compute@Edge も Service として扱われます。

Fastly Compute@Edge を採用した理由

カウシェにて CDN を構築する際に、 VCL ベースの Service と Compute@Edge の Service のどちらを利用するかを検討し、下記のような理由から Compute@Edge の Service を採用しました。

  • 汎用言語(Rust)を用いて処理を記述できる

  • ローカル開発環境や CI 環境でのテストの実行が容易である

汎用言語(Rust)を用いて処理を記述できる

既存の VCL ベースの Service では、VCL を用いて処理を記述できます。VCL を用いた設定は記述やセットアップが容易ではありますが、VCL という Domain-Specific Language をキャッチアップする必要があります。
一方で、Compute@Edge の Service では Rust や JavaScript といった VCL と比べるとより用途の多い汎用言語を用いて処理を記述できます。Rust や JavaScript を利用できることで、cargo や npm などのそれぞれの言語を支えるツールや既存のライブラリを利用できるのも大きなメリットだと考えています。

カウシェでは Compute@Edge で動作する Wasm バイナリの処理を記述するための言語として、下記の理由から Rust を採用しています。

  • Compute@Edge では Rust の SDK が早い段階から提供されていた

  • Compute@Edge のランタイムでも利用されている wasmtime のような、Wasm を支えるツールが Rust で記述されているケースが多い

  • Envoy をはじめとするネットワークプロキシ上で Wasm バイナリを動作させるための規格である Proxy-Wasm においても早い段階で Rust の SDK が存在していた

ローカル開発環境や CI 環境でのテストの実行が容易である

VCL に比べると、Rust や JavaScript のテストの仕組みを利用できる点も大きなメリットです。また、Fastly CLI を用いてローカル環境で Service を実行できる点もテストが実行しやすく良い点です(テストについての詳細は後述します)。

Compute@Edge Service の実装例

Compute@Edge Service の簡単な実装例を紹介します(詳細な実装方法については公式のドキュメントを参照してください)。紹介する実装例は GitHub で公開しているので、手元で確認したい方はぜひチェックアウトしてください。

例として、下記のような Service を実装することを想定します。

  • バックエンドである example.kauche.com にリクエストを流す

  • リクエストのパスが /static/* にマッチする場合にキャッシュする

  • レスポンスのヘッダーに my-awesome-header: my-awesome-value を追加する

Service の雛形は、下記のように Fastly CLI を用いて作成できます。

$ mkdir -p fastly-compute-edge-example/service && cd fastly-compute-edge-example/service
$ fastly compute init

このコマンドにより、下記のような Rust のプロジェクトの雛形が生成されます(Language は Rust を、Starter kit は Empty starter for Rust を選択しています)。

$ tree .
.
├── Cargo.lock
├── Cargo.toml
├── fastly.toml
├── README.md
├── rust-toolchain
└── src
    └── main.rs

1 directory, 6 files

Service の処理は、fastly compute init で生成された /src/main.rs に記述します。

use fastly::{Error, Request, Response};
use regex::Regex;

#[fastly::main]
fn main(mut req: Request) -> Result<Response, Error> {
    // By default, disable caching for all requests.
    req.set_pass(true);

    let path = req.get_url().path();
    let static_path_regex = Regex::new("^/static/.*$")?;

    // Cache static assets.
    if static_path_regex.is_match(path) {
        req.set_pass(false);
    }

    let mut res = req.send("example.kauche.com")?;

    // Add my-awesome-header HTTP header to the response
    res.set_header("my-awesome-header", "my-awesome-value");

    Ok(res)
}

上記のように、Rust を用いて Service に求められる要件を実装します。(カウシェでは静的ファイルに加えて、キャッシュするのに適さない API リクエストにおいても CDN を経由させる構成にしているので、デフォルトではキャッシュを無効にしています。)

テスト

テストでは、実装した Service を Fastly CLI の fastly compute serve コマンドによってローカル環境で起動し、実際に HTTP リクエストを送信してレスポンスが期待通りか否かを検証します。

fastly compute serve は、ローカルで実行する Service がローカルで完結するために、Service の接続先となるバックエンドをローカル起動用のものに上書きできる機能を提供しています。
これは、Compute@Edge Service の設定ファイルである fastly.toml の [local_server.backends."<バックエンドの接続先>"] プロパティの url にローカルでの接続先を記述することで設定できます。
下記の fastly.toml の例では、実装例で利用している example.kauche.com というバックエンドをローカル開発時に backend:5000(docker compose 上での接続先)に置き換えています。

authors = ["110y"]
description = "An example project for Fastly Compute@Edge"
language = "rust"
manifest_version = 2
name = "fastly-compute-edge-example"
service_id = ""

[local_server.backends."example.kauche.com"]
url = "<http://backend:5000>"

カウシェでは docker compose を用いてテスト用の Service とローカル用のバックエンドを起動しています。

---
version: "3.8"

services:

  fastly:
    image: rust:1.61.0-bullseye
    ports:
      - ${PORT:-3000}:3000
    working_dir: /src/fastly-compute-edge-example/service
    volumes:
      - .:/src/fastly-compute-edge-example
      - ./bin/viceroy:/root/.config/fastly/viceroy
    command: ../bin/fastly compute serve --skip-build --addr 0.0.0.0:3000

  backend:
    image: docker pull ghcr.io/110y/echoserver/echoserver:0.0.1

docker compose の backend サービスには筆者が作成している実験用のサーバー(送られてきたリクエストの内容をレスポンスとして返すサーバー)である echoserver を指定しています。
docker compose を用いて起動したローカルの Service に対して、下記のようなテストを Rust で記述し、cargo test コマンドで実行しています。
このテストでは fastly compute serve で立ち上げた Service に対して実際に HTTP リクエストを送信し、レスポンスのヘッダーに期待する値(my-awesome-header: my-awesome-value)が含まれているか否かを検証しています。

#[cfg(test)]
mod tests {
    #[test]
    fn example() {
        let client = reqwest::blocking::Client::new();
        let response = match client.post("<http://fastly:3000/").body("{}>").send() {
            Ok(response) => response,
            Err(e) => panic!("Failed to call the server: {}", e),
        };

        assert_eq!(response.status(), reqwest::StatusCode::OK);

        if let Some(header) = response.headers().get("my-awesome-header") {
            assert_eq!(header, "my-awesome-value");
        } else {
            panic!("my-awesome-header header has not been found")
        }
    }
}

HTTP リクエストの送信には reqwest というライブラリを用いています。

デプロイ

デプロイには TerraformFastly Provider を利用しています。
Fastly CLI でも Compute@Edge の Service をデプロイできます(fastly compute deploy)が、「コマンドの実行」という Imperative な方法ではなく、「コードの記述」による Declarative な方法(Infrastructure as Code)で Service の構成を管理したいため、Fastly CLI の利用は Wasm バイナリのビルド(fastly compute build)やローカル実行環境の起動(fastly compute serve)にとどめ、Terraform を利用しています。
Compute@Edge のデプロイの設定は Terraform Fastly Provider の fastly_service_compute リソースを用いて下記のように記述しています。

resource "fastly_service_compute" "example" {
  name = "example"

  domain {
    name = "..."
  }

  backend {
    # ....
  }

  package {
    filename         = "./service/pkg/fastly-compute-edge-example.tar.gz"
    source_code_hash = filesha512("./service/pkg/fastly-compute-edge-example.tar.gz")
  }
}

package ブロックに fastly compute build で生成した Wasm バイナリのパッケージを指定しています。

おわりに

本稿ではカウシェでの Fastly Compute@Edge 採用事例について、採用理由やテスト・デプロイの方法を混じえてご紹介しました。

Fastly 自身が管理している developer.fastly.com を VCL から Compute@Edge に乗り換えたという記事 も出ており、汎用言語である Rust で処理を記述できてテストも実行しやすい Compute@Edge を採用して良かったと感じています。
また、Wasm の利用は Web にとどまらず、例えばネットワークプロキシである Envoy や、それをベースとした Service Mesh である Istio にも広がっており、汎用的な実行環境として今後も要注目だと筆者は考えています。

弊社カウシェでは「Try First」というバリューを掲げており、本稿で紹介した Fastly Compute@Edge のような比較的新しい技術についても積極的に Try していき、良いものであれば実際に採用していく、という文化が根づいています。
エンジニアにとっては非常にエキサイティングな環境なので、もし弊社にご興味を持っていただいた場合は、ぜひ採用情報をご覧ください。

採用情報

この記事が気に入ったらサポートをしてみませんか?