ブログに戻る

フォロー&ご登録

VCL から Compute に developer.fastly.com を移行する方法

Andrew Betts

Principal Developer Advocate, Fastly

Fastly でシステムを構築する場合、Developer Hub でかなりの時間を費やす可能性があります。先月、私たちは VCL プラットフォームから Compute に移行しました。以下に、その移行方法と、そこから学んだことをご紹介します。

まず、VCL からの移行が必ずしも正しいとは限らないことを強調しておきます。VCL プラットフォームとコンピューティングプラットフォームの両方が積極的にサポートおよび開発されており、VCL にはコードなしの高速なサイトを実現する優れた既定値が設定されているため、セットアップが容易です。ただし、エッジで複雑なタスクを実行する場合は、おそらく Compute の汎用的なコンピューティング機能が必要になります。

新しい作業に取り掛かる前に、VCL で実行している作業をコンピューティングサービスに移行することをお勧めします。この投稿では、developer.fastly.com で使用されるすべてのパターンを VCL から Compute に移行する方法をご紹介します。

下準備

既存の developer.fastly.com サービスは、VCL を使用した、中断したくない運用サービスであるため、最初の手順として、テストできる新しい Compute サービスを作成します。そのためには、まず Fastly CLI をインストールし、新しいプロジェクトを初期化します。

> fastly compute init

Creating a new Compute project.

Press ^C at any time to quit.

Name: [edge] developer-hub
Description: Fastly Developer Hub
Author: devrel@fastly.com
Language:
[1] Rust
[2] AssemblyScript (beta)
[3] JavaScript (beta)
[4] Other ('bring your own' Wasm binary)
Choose option: [1] 3
Starter kit:
[1] Default starter for JavaScript
    A basic starter kit that demonstrates routing, simple synthetic responses and
    overriding caching rules.
    https://github.com/fastly/compute-starter-kit-javascript-default
Choose option or paste git URL: [1] 1

✓ Initializing...
✓ Fetching package template...
✓ Updating package manifest...
✓ Initializing package...

Initialized package developer-hub to:
        ~/repos/Developer-Hub/edge

Developer Hub は主に Gatsby フレームワークを使用する JavaScript アプリケーションなので、Compute コードの記述にも JavaScript を選択しました。init コマンドによって生成されるアプリは完成して動作する状態になっているので、すぐに本番環境にデプロイして開発 - テスト - 反復のサイクルを進めるに値します。

> fastly compute publish
✓ Initializing...
✓ Verifying package manifest...
✓ Verifying local javascript toolchain...
✓ Building package using javascript toolchain...
✓ Creating package archive...

SUCCESS: Built package 'developer-hub' (pkg/developer-hub.tar.gz)


There is no Fastly service associated with this package. To connect to an existing service add the Service ID to the fastly.toml file, otherwise follow the prompts to create a service now.

Press ^C at any time to quit.

Create new service: [y/N] y

✓ Initializing...
✓ Creating service...

Domain: [some-funky-words.edgecompute.app] 

Backend (hostname or IP address, or leave blank to stop adding backends): 

✓ Creating domain some-funky-words.edgecompute.app'...
✓ Uploading package...
✓ Activating version...

Manage this service at:
        https://manage.fastly.com/configure/services/*****************

View this service at:
        https://some-funky-words.edgecompute.app


SUCCESS: Deployed package (service *****************, version 1)

これで、Fastly のエッジから提供される稼働サービスが完成し、瞬時に変更をデプロイできるようになりました。では、移行を始めましょう。

Google Cloud Storage

Developer Hub の主なコンテンツは Gatsby サイトであり、構築して Google Cloud Storage にデプロイされ、静的にホストされます。最初にバックエンドを追加することから始めてみましょう。

fastly backend create --name gcs --address storage.googleapis.com --version active --autoclone

次に、コンピューティングアプリのメインソースファイル (この場合は src/index.js) を編集して、GCS からコンテンツを読み込みます。まず、ファイルの内容全体を次のように置き換えます。

const BACKENDS = {
  GCS: "gcs",
}
const GCS_BUCKET_NAME = "fastly-developer-portal"

async function handleRequest(event) {
  const req = event.request
  const reqUrl = new URL(req.url)

  let backendName

  backendName = BACKENDS.GCS
  reqUrl.pathname = "/" + GCS_BUCKET_NAME + "/production" + reqUrl.pathname

  // Fetch the index page if the request is for a directory
  if (reqUrl.pathname.endsWith("/")) {
    reqUrl.pathname = reqUrl.pathname + "index.html"
  }

  const beReq = new Request(reqUrl, req);
  let beResp = await fetch(beReq, {
    backend: backendName,
    cacheOverride: new CacheOverride(["GET", "HEAD", "FASTLYPURGE"].includes(req.method) ? "none" : "pass"),
  })

  if (backendName === BACKENDS.GCS && beResp.status === 404) {
    // Try for a directory index if the original request didn't end in /
    if (!reqUrl.pathname.endsWith("/index.html")) {
      reqUrl.pathname += "/index.html"
      const dirRetryResp = await fetch(new Request(reqUrl, req), { backend: BACKENDS.GCS })
      if (dirRetryResp.status === 200) {
        const origURL = new URL(req.url) // Copy of original client URL
        return createRedirectResponse(origURL.pathname + "/")
      }
    }
  }

  return beResp
}

const createRedirectResponse = (dest) =>
  new Response("", {
    status: 301,
    headers: { Location: dest },
  })

addEventListener("fetch", (event) => event.respondWith(handleRequest(event)))

このコードはパブリック GCS バケットの前にあるために推奨されるパターンを実装しています。では、手順を追って見ていきましょう。

  • 定数を定義します。BACKENDS オブジェクトは後続のバックエンドに対してルーティングを拡張可能にするための優れた方法です。ここでの値 "gcs" は、先ほどバックエンドに指定した名前と一致します。

  • 便宜上、req にクライアントリクエスト event.request を割り当て、URL クラスを使用して req.url を解析された URL オブジェクト reqUrl に変換します。

  • GCS バケット内の適切なオブジェクトに到達するためには、パスの先頭にバケット名を追加します。私たちの環境では GCS バケットに本番用以外のブランチも格納しているため、'production' を追加する必要がありました。

  • 着信リクエストが / で終了した場合は、バケット内のディレクトリインデックスページを見つけるために 'index.html' を追加するのが妥当です。

  • Google からのレスポンスが 404 で、クライアントのリクエストが / で終わらなかった場合は、パスに '/index.html' を追加して再試行します。これで機能する場合は、外部リダイレクトを返し、クライアントに / を追加するように指示します。

  • 最後に、リクエストハンドラー関数をクライアントの 'fetch' イベントにアタッチします。

コンパイルしてデプロイします。

> fastly compute publish
✓ Initializing...
✓ Verifying package manifest...
✓ Verifying local javascript toolchain...
✓ Building package using javascript toolchain...
✓ Creating package archive...

SUCCESS: Built package 'developer-hub' (pkg/developer-hub.tar.gz)

✓ Uploading package...
✓ Activating version...

SUCCESS: Deployed package (service *****************, version 3)

さて、some-funky-words.edgecompute.app をブラウザに読み込むと、Developer Hub が表示されます。出だしは順調です。

カスタム 404 ページ

ページが存在する場合は問題ありませんが、存在しないページを読み込もうとすると問題が起こります。

Gatsby は「page not found (ページが見つかりません)」ページを生成し、GCS バケットに「404.html」としてデプロイします。クライアントリクエストが Google からの 404 エラーレスポンスを指示した場合、そのコンテンツを提供するだけで済みます。

if (backendName === BACKENDS.GCS && beResp.status === 404) {
  
  // ... existing code here ... //

  // Use a custom 404 page
  if (!reqUrl.pathname.endsWith("/404.html")) {
    debug("Fetch custom 404 page")
    const newPath = "/" + GCS_BUCKET_NAME + "/404.html"
    beResp = await fetch(new Request(newPath, req), { backend: BACKENDS.GCS })
    beResp = new Response(beResp.body, { status: 404, headers: beResp.headers })
  }
}

オリジンから直接取得した beResp のステータスコードは 200 (成功) であるため、レスポンスコードを返さないことに注意してください。代わりに、GCS レスポンスからの本文ストリームを使用して、新しい "404" レスポンスを作成します。このパターンの詳細については、バックエンド統合ガイドをご覧ください。

前述の場合と同様に、fastly compute publish コマンドを実行してデプロイすると、「page not found (ページが見つかりません)」エラーの見映えがはるかに良くなります。

リダイレクト

developer.fastly.com には多数のリダイレクトもあるため、次は VCL からの移行です。私たちはすでに Edge Dictionary を使用してそのデータを保存しています。1つは正確なリダイレクト用、もう1つはプリフィックス付きのリダイレクト用です。これらは、リクエストハンドラーの開始時に読み込むことができます。

async function handleRequest(event) {
  const req = event.request
  const reqUrl = new URL(req.url)
  const reqPath = reqUrl.pathname
  const dictExactRedirects = new Dictionary("exact_redirects")
  const dictPrefixRedirects = new Dictionary("prefix_redirects")

​​  // Exact redirects
  const normalizedReqPath = reqPath.replace(/\/$/, "")
  const redirDest = dictExactRedirects.get(normalizedReqPath)
  if (redirDest) {
    return createRedirectResponse(redirDest)
  }

  // Prefix redirects
  let redirSrc = String(normalizedReqPath)
  while (redirSrc.includes("/")) {
    const redirDest = dictPrefixRedirects.get(redirSrc)
    if (redirDest) {
      return createRedirectResponse(redirDest + reqPath.slice(redirSrc.length))
    }
    redirSrc = redirSrc.replace(/\/[^/]*$/, "")
  }

正確なリダイレクトの場合は単純ですが、プリフィックス付きリダイレクトの場合、ループで繰り返し処理を行い、パスが空になるまで一度に1つずつ URL セグメントを削除し、辞書で徐々に短くなるプリフィックスを検索する必要があります。目的のリダイレクトが見つかった場合、クライアントリクエストの不一致部分がリダイレクトに追加されます (/source => /destination は /source/foo のリクエストを /destination/foo にリダイレクトします)。

ホスト名の正規化

ある時点で、developer.fastly.com も fastly.dev として利用できるようにすると便利だと思いつき、fastly.dev をリクエストするユーザーを VCL サービスで developer.fastly.com にリダイレクトすることにしました。次にそれを移行しましょう。まず、ファイルの先頭でサイトの正規ドメインを定義します。

const PRIMARY_DOMAIN = "developer.fastly.com"

次に、リクエストハンドラーでホストヘッダーを読み取り、必要に応じてリダイレクトします。テスト中、私たちは実際にこれを random-funky-words.edgecompute.app-style ドメインに設定しました。これは、リクエストハンドラーの最初の宣言の直後、リダイレクトコードの前に配置するのが妥当です。

async function handleRequest(event) {
  // ... existing code ... //
  const hostHeader = req.headers.get("host")

  // Canonicalize hostname
  if (!req.headers.has("Fastly-FF") && hostHeader !== PRIMARY_DOMAIN) {
    return createRedirectResponse("https://" + PRIMARY_DOMAIN + reqPath)
  }

VCL サービスが安全でない HTTP 要求に対して TLS リダイレクトを実行している可能性もあります。Compute はこれを自動的に処理し、安全な接続の存在が見つかるまでコードにリクエストを渡すことはなくなるため、移行する必要はありません。

このコードをテストするには、サービスに2つ目の非正規ドメインを接続する必要があります。私たちは fastly domain create でこれを実行します。

fastly domain create --name testing-fastly-devhub.global.ssl.fastly.net --version latest --autoclone

これは、Fastly が割り当てたドメインの優れたユースケースなので、DNS に (まだ) 手を付ける必要はありませんブラウザで testing-fastly-devhub.global.ssl.fastly.net にアクセスすると、サービスによってプライマリドメインにリダイレクトされます。

レスポンスヘッダー

次に、VCL サービスが GCS から取得した応答に対して行っている変更に着目しました。Google は、クライアントに公開したくない x-goog-generation などのレスポンスに一連のヘッダーを追加します。将来、バックエンドでさらにヘッダーを追加する可能性があるため、許可リストに基づいてヘッダーをフィルタリングするのが妥当です。まず、ファイルの先頭で、許可するレスポンスヘッダーを定義します。

const ALLOWED_RESP_HEADERS = [
  "cache-control",
  "content-encoding",
  "content-type",
  "date",
  "etag",
  "vary",
]

その後、バックエンドフェッチの直後に、返されたヘッダーをフィルタリングするコードを挿入することができます。

// Filter backend response to retain only allowed headers
beResp.headers.keys().forEach((k) => {
  if (!ALLOWED_RESP_HEADERS.includes(k)) beResp.headers.delete(k)
})

逆に、クライアントレスポンスで表示したいヘッダー (例 : Content-Security-Policy) がある場合には、そのヘッダーを追加する必要があります。これらのヘッダーの多くは、HTML レスポンスでのみ必要になります。

if ((beResp.headers.get("content-type") || "").includes("text/html")) {
  beResp.headers.set("Content-Security-Policy", "default-src 'self'; scrip...")
  beResp.headers.set("X-XSS-Protection", "1")
  beResp.headers.set("Referrer-Policy", "origin-when-cross-origin")
  beResp.headers.append(
    "Link",
    "</fonts/CircularStd-Book.woff2>; rel=preload; as=font; crossorigin=anonymous," +
      "</fonts/Lexia-Regular.woff2>; rel=preload; as=font; crossorigin=anonymous," +
      "<https://www.google-analytics.com>; rel=preconnect"
  )
  beResp.headers.set("alt-svc", `h3-29=":443";ma=86400,h3-27=":443";ma=86400`)
  beResp.headers.set("Strict-Transport-Security", "max-age=86400")
}

ヘッダーの追加と削除は、VCL サービスとコンピューティングサービスの両方で一般的なユースケースです。

バックエンドのパススルー

VCL サービスで実行するもう1つの処理として、GCS 以外のバックエンドにいくつかのリクエストを渡し、クラウド関数やその他のサードパーティーサービスを呼び出します。例えば、Developer Hub の検索エンジンに Swiftype を使用しているため、検索 API を developer.fastly.com ドメイン内で使用できるようにするには、新しいバックエンドを追加し、そのバックエンドをターゲットにするために特定のリクエストパスを結び付けます。

まず、Fastly CLI を使用してバックエンドを追加します。

fastly backend create --name swiftype --address search-api.swiftype.com --version active --autoclone

次に、そのバックエンドを参照できる定数をソースコードに追加します。

const BACKENDS = {
  GCS: "gcs",
  SWIFTYPE: "swiftype",
}

最後に、バックエンド選択コードを更新し、必要に応じて Swiftype を選択するルーティングロジックを追加します。

let backendName
if (reqPath === "/api/internal/search") {
  backendName = BACKENDS.SWIFTYPE
  reqUrl.pathname = "/api/v1/public/engines/search.json"
} else {
  backendName = BACKENDS.GCS
  reqUrl.pathname = "/" + GCS_BUCKET_NAME + "/production" + reqUrl.pathname

  // Fetch the index page if the request is for a directory
  if (reqUrl.pathname.endsWith("/")) {
    reqUrl.pathname = reqUrl.pathname + "index.html"
  }
}

他のバックエンドでもこの処理を繰り返すことができます。例えば、SentryFormkeep にも同じ手法を使用します。このパターンの詳細については近日中にご説明します。

GitHub Actions を使用したデプロイ

私たちの VCL サービスはソースコード管理ツールで管理されていなかったので、それを修正する良い機会になりました。Compute のバージョンは Developer Hub のメインソースと共に保存されるため、バックエンドへのデプロイに失敗した場合にエッジロジックへの変更が送信されないように、エッジのリリースをバックエンドの更新と調整してエッジリリースを依存させることができます。

CI ワークフローに2つのジョブを追加しました。1つのジョブは PR 用のエッジアプリを構築します。

  build-fastly:
    name: C@E build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v2
      - name: Set up Fastly CLI
        uses: fastly/compute-actions/setup@main
      - name: Install edge code dependencies
        run: cd edge && npm install
      - name: Build Compute@Edge Package
        uses: fastly/compute-actions/build@main
        with:
          project_directory: ./edge/

もう1つのジョブでは、GCS のデプロイが完了した時点でエッジアプリをデプロイします。

  update-edge-app:
    name: Deploy C@E app
    runs-on: ubuntu-latest
    if: ${{ github.ref_name == 'production' }}
    needs: deploy
    steps:
      - name: Checkout Code
        uses: actions/checkout@v2
      - name: Install edge code dependencies
        run: cd edge && npm install
      - name: Deploy to Compute@Edge
        uses: fastly/compute-actions@main
        with:
          project_directory: ./edge/
        env:
          FASTLY_API_TOKEN: ${{ secrets.FASTLY_API_TOKEN }}

次のステップ

移行が完了したので、VCL サービスと同じ機能が使用できるようになりました。しかし、Developer Hub の前に Compute があるので、すべてのクラウド機能をエッジコンピューティングコードに直接移動し、エッジでより負荷の高い作業を開始できます。

Compute へのサービスの移行は案外に簡単です。独自の移行を計画している場合は、Developer Hub 用に変換する必要があったパターンの多くを網羅した、VCL から Compute への移行のための専用ガイドと、幅広いユースケースをカバーするデモ、サンプルコード、チュートリアルを含むソリューションライブラリを参照してください。

まだ Compute を試したことがない方は、Fastly のサーバーレス・エッジ・コンピューティング・プラットフォームがエッジでのより高速かつ安全で、パフォーマンスの高いアプリケーションの構築にどのように役立つかをこちらでご覧ください。