🔥

Cloudflare WorkersのService BindingsとHono RPCを組み合わせる

2024/06/03に公開

はじめに

本記事では、Cloudflare Workers間の通信にService Bindingsを使いながら、Hono RPCで静的型の恩恵を受ける方法を紹介します。それが可能であるという事実自体はHonoの公式ドキュメントにも記載されてはいるのですが、その強力さとは裏腹にあまり注目されていないように思われます。

また、最後にWeb standardへの準拠がもたらすメリットについての筆者の所感を述べます。

Hono RPC

公式ドキュメントに則りHono RPCについて簡単に説明します。すでに理解されている方は飛ばしてしまっても問題ありません。
Hono RPCは、APIの仕様をTypeScriptの型としてクライアント側に共有する仕組みです。これによって、Hono側で普通にAPIを実装するだけで静的型のサポートを受けながらAPIを呼び出すことができます。
以下は公式のコード例に少し手を加えたものです。

server.ts
const route = app.post(
  '/posts',
  zValidator(
    'form',
    z.object({
      title: z.string(),
      body: z.string(),
    })
  ),
  (c) => {
    // ...
    return c.json(
      {
        ok: true,
        message: 'Created!',
      },
      201
    )
  }
)

export type AppType = typeof route
client.ts
import type { AppType } from './server.js'
import { hc } from 'hono/client'

const client = hc<AppType>('http://localhost:8787/')
const res = await client.posts.$post({
  form: {
    title: 'Hello',
    body: 'Hono is a cool project',
  },
})

もしクライアントのコードでtitlenumberの値を渡したら型エラーになります。ノリはtRPCに近いですが、こちらは普通にHTTPをしゃべるAPIを書いているだけです。OpenAPIからのコード生成に比べても非常にシンプルなことがおわかりいただけるかと思います。

Hono RPCはここで説明した以上に魅力的な機能を備えていますが、そちらは公式ドキュメントやHono作者ご自身の記事に譲ることにして、Service Bindingsの説明に移ります。

Service BindingsとHono RPC

Service Bindingsは、Cloudflare Workersがネットワークを経由せず他のWorkerを呼び出せるようにする仕組みです。Service Bindingsを設定した複数のWorkersは、Cloudflareの同一サーバーの同一スレッド内で実行されます。これによってオーバーヘッドが極めて少ない通信が可能となるという夢のようなソリューションです。設定も設定ファイルに数行書くだけなので極めて簡単です。

さて、Service BindingsはRPC[1]とHTTPの二種類のインターフェースをサポートしています。そして、後者のHTTPを使うと、Hono RPCをそのまま利用することができるのです。

main.ts
export default {
  fetch: (req: Request, env: Env) => {
    const client = hc<AppType>('/', { fetch: env.ANOTHER_WORKER.fetch.bind(env.ANOTHER_WORKER) })
  }
}

先ほどのコード例との違いは、hcの第二引数にfetch関数を渡しているところです。
env.ANOTHER_WORKERは、「こういう名前(ここではANOTHER_WORKER)で他のWorkerをService Bindingで呼び出すよ〜」と設定ファイルに書くとWorkerのランタイムが渡してくれる特殊なオブジェクトです。このオブジェクトは、Web標準のfetch APIを実装したfetchというメンバーを持っています。HonoのRPCはカスタムfetch関数を引数で受け取ることができるため、Service BindingとHono RPCは問題なく組み合わせることができます。

これは非常に強力です。リッチに型がついているので見た目はライブラリを使っているようにしか見えないのに、HTTPサーバーとしゃべっています。しかし、ただのHTTPではなくネットワークを介さないのでやりとりは非常に高速です。

Web standardへの準拠がもたらすportability

この構成の魅力はそれだけに尽きないと筆者は考えています。それはportability[2]です。

Service BindingsはCloudflareのソリューションですが、アプリケーションとしてはHTTPをしゃべっているだけなので、Cloudflare Workers以外のインフラに乗せ替えても、hcの引数を変更するだけで動きます。また、Honoも様々なランタイムをサポートしており、ランタイム毎のアダプタをすげかえるだけで動きます[3]。仮にCloudflare Workersでは満たすことが難しいビジネス要件が現れた場合でも、例えばGoogle CloudやAWSのコンテナソリューションへの移行も比較的少ないコストで可能なはずです。これが、先ほど述べたportabilityの意味です。

筆者がこの構成で組んでみて強く感じたのは、standardというものの強さです。というのも、Honoが様々なランタイムをサポートできているのは、HonoのコアのコードがWeb standard APIsしか使っていないからです。そもそも、Service BindingsとHonoを組み合わせることができるのも、両者がfetch APIを使っていたからに他なりません。

「最悪別のインフラに乗せればいい」という逃げ道は非常に大きな意味を持ちます。Cloudflare Workersは非常に魅力的なソリューションですが、他社のコンテナベースのソリューションに比べるとまだまだ実績が少なく、ランタイムの制約もいくつかあるので、使ってみたくてもなかなか採用に踏み切れないというのが現実かもしれません。しかし、Web standard準拠のライブラリ群を使っていれば(例えばRemixもそこに含まれるでしょう)、「とりあえず挑戦してみてダメだったら撤退する」という戦略も現実味を帯びてくるでしょう。

脚注
  1. RPC(紛らわしいですがHonoのではありません)の方がより柔軟なソリューションかもしれませんが、後述のportabilityという観点ではHTTPの方が優れていると筆者は考えます。RPCではWorkersでしか動かないコードが増えてしまうのです。 ↩︎

  2. (2024/06/05 12時追記)WinterGCにならい、"interoperability"と表現した方が適切だったかもしれません ↩︎

  3. もちろん、Honoと無関係な部分でランタイム固有のコードを書いていれば話は変わります ↩︎

Discussion