Cloudflare Workers
in
Production

自己紹介

Motoki Shakagori
(釋迦郡 元気)

  • Org: 株式会社ベースマキナ ソフトウェアエンジニア
  • ❤️: 型、犬(→)
  • Contribution: Neverthrow🙅‍♀️, Hono🔥
  • Linktree: https://linktr.ee/mshaka

注意!

今回のお話はベースマキナの事例ではなく*、前職のSpirのものです!!!

ただ、ベースマキナでもプロダクト以外でWorkersを使っています!

テーマ

  • Cloudflare Workersで普通のtoB SaaSを半年のあいだ開発した経験の共有
  • 普通って?
    • ユーザーはブラウザで使う
    • バックエンド(REST API)がDBとやりとり
  • 結論としては正直全然困らなかったしめちゃくちゃ体験よかった🙌

目次

  • イントロ
    • サービス概要、アーキテクチャ、Workersの採用理由
  • Cloudflare Workersとは
  • フロントエンド
  • Service Binding
  • バックエンド
  • Sentry
  • まとめ

イントロ

サービス概要(1/2)

  • Spir for Agent
  • 人材紹介会社の方が採用面接の日程調整をするときの手間を減らすサービス

サービス概要(2/2)

アーキテクチャ図

なぜWorkersを採用したか?

  • コンテナはデプロイが重くて嫌だった
    • 既存サービスのバックエンドはNode.js on Cloud Run
  • Remixをデプロイしやすかった
  • 開発エコシステムが整備されていて体験がいいしデプロイも速いので(後述)、使ってみたいという気持ちが膨れ上がった
  • 検証段階を過ぎて何か困ることがあったときも脱出は容易だと判断した(後述)

Cloudflare Workersとは

Cloudflare Workersの概要(1/2)

  • Function-as-a-Service
  • V8のIsolateを利用した高速に水平スケールする実行環境
  • Cold start time0秒を宣伝文句にしている
  • グローバルに分散したノードで実行される

Cloudflare Workers の概要(2/2)

  • 公式のサポート言語はJS/Python/Rust/Wasm
  • (余談)JS以外は実質Wasmなので、Wasmコンパイルして頑張れば自力で動かせるはず

WorkersのJSランタイム(1/2)

  • Common Web Platform API(Web standardサブセット)とNode.jsの互換API(一部サポート外)を持つ
  • 最低でも週に1度はリリースされ、Chrome stable最新と同じ機能が使える
  • 後方互換性を完全にサポート

WorkersのJSランタイム(2/2)

  • Node.js向けのライブラリが動く保証はない
  • が、今のところそれで困ったことはない
    • Webフレームワークを除けば、意外にNode.js向けのライブラリは使わない(ブラウザでも動くやつを使う)
    • Expressは動かないかもだけど、Expressで満足できるならHonoでいい

サンプルコード

export default {
  async fetch(request, env, ctx) {
    return new Response("Hello, world!");
  },
};

ローカル開発環境

Wrangler(1/3)

  • Wranglerという大変よくできた公式CLIがdev serverの立ち上げからdeployまでを担う
  • dev serverは本番と同等のランタイムで動く
  • tsも事前のコンパイルなしで実行可能(型検査は別途実行する必要あり)
  • Cloudflare KV, R2などのCloudflareサービスのエミュレータ付き
  • Remixの開発ではVite pluginを通してWorkersのランタイムを使う

Wrangler(2/3)

  • Chromeのdev toolでdebuggerも使える
  • VSCodeでbreakpointを仕込むことも可能

Wrangler(3/3)

テスト

  • テストにはCloudflare公式の@cloudflare/vitest-pool-workersというVitestプラグインがある
  • テストも本番と同じランタイムで実行可能
  • テストの書き方は普通のNode.jsアプリケーションと全く同じ

Frontend

Remix

  • React のメタフレームワーク。SPAも書けるが今回はSSR
  • アダプターを通して様々なランタイムで動くように設計されており、Workersも公式サポート
    • 他にもNode, Deno, Vercelなどなど
  • Remix on Workersは本当に困ることがなくてあまり話すことがない

Remixの責務

  • サーバーサイドでコードを実行できるので、DB呼び出しを含むビジネスロジックも全てRemixの中に書くことも可能
  • 今回の開発では、APIからデータを取得して整形するだけのBFF的な役目に徹する
    • Webアプリ以外のクライアントが生えたときのことを考えるとバックエンドを分離しておいた方が綺麗
    • Workers間の通信が非常に高速で分離するコストが低い

Workers間の通信

Service Binding(1/4)

  • Workers同士の通信は、Service Bindingという特殊な仕組みが使える
  • 実態は単なるコード呼び出しなので、ネットワークのオーバーヘッド無し

Service Binding(2/4)

設定ファイルにちょろちょろっと書くだけで本番でもローカル開発時も使える

// service-aというWorkerからservice-bというWorkerを呼び出す場合
{
  "name": "service-a",
  "services": [
    {
      "binding": "SERVICE_B", // コードから呼びだすときの名前
      "service": "service-b", // デプロイするときにつけた名前
    },
  ],
}

Service Binding(3/4)

インターフェースはfetchなのでHTTP越しのやりとりにしか見えない*

// index.ts
export default {
  async fetch(request, env) {
    return await env.SERVICE_B.fetch(request);
  },
};

* fetch以外のインターフェースを使えるモードもあります

Service Binding(4/4)

  • fetchインターフェースが使われているため、仮に別のクラウドに移ってnetwork越しの通信になったとしてもコードの見た目を変えずに移行可能
  • 呼び出される側のWorkerはパブリックアクセスを無効にすることも可能
  • Wranglerでservice-aより前にservice-bを立ち上げておけばローカルでもエミュレート可能

Backend

Hono

Hono RPC(1/3)

  • クライアント側にRequest/ResponseのTypeScript型を共有する仕組み
  • これによって、RemixのサーバーサイドからのAPI呼び出しが書きやすくなる

Hono RPC(2/3)

// server.ts
import { Hono } from "hono";
const app = new Hono()
  .get("/hello", (c) => c.json({ message: "Hello, World!" }))
  .get("/dog", (c) => c.json({ face: "🐶" }));
export type AppType = typeof app;
export default app;

// client.ts
import { hc } from "hono/client";
import type { AppType } from "./server";
const client = hc<AppType>("http://localhost:8787");
await client.hello.$get().then((res) => res.json()); // { message: string }型
await client.dog.$get().then((res) => res.json()); // { face: string } 型

Hono RPC(3/3)

Service Bindingでも問題なく使える!!!🔥⚡

import { hc } from "hono/client";
import type { AppType } from "./server";

export default {
  async fetch(request, env) {
    const _fetch: typeof fetch = (...args) => env.SERVICE_B.fetch(...args);
    // 本番ではドメインは無視されるが有効なURL文字列を入れないとエラーになる
    const client = hc<AppType>("http://localhost:8787", { fetch: _fetch });
    const res = await client.hello.$get();
    const data = await res.json();
    return new Response(data.message);
  },
};

Sentry

  • @sentry/cloudflareがあるので動く
    • エラーとトレースのみ
    • 現時点ではプロファイルは取れない
  • 分散トレースもバッチリ

まとめ

開発体験のよさ

  • Wranglerによる統合された開発環境、公式Vitestプラグインの存在
  • 高速なデプロイ
  • Workersで使えるRemixとHonoの使いやすさ、Service Bindingとの相性

撤退戦略(1/2)

  • とはいえWorkersはまだまだ枯れてるとは言えないプロダクト
  • 採用にあたって、失敗したらどうするの?という問いに答えを持っておきたかった

撤退戦略(2/2)

  • インフラ載せ替えは難しくないと判断
  • RemixとHonoはそもそもランタイム非依存
    • アダプタを切り替えるだけ
  • Service Bindingはprivate networkに置き換え
    • fetchインターフェースなのでコードの変更は少ない
  • KVなどの他のCloudflareのサービスを使っている場合のみ、置き換え先を探す必要あり

Thank you!!!

よいCloudflare Workersライフを!