✍️

Cloudflare WorkersのJS RPCを理解する

2024/05/07に公開

前置き

4月の第1週に行われたCloudflare Developer Week 2024でAIやデータベース関係のアップデートの影に隠れ、WorkersをつくってるKenton氏のブログが2つ投下されました。

https://blog.cloudflare.com/workers-environment-live-object-bindings
https://blog.cloudflare.com/javascript-native-rpc

そして「JS RPC」という機能が追加されました。

https://developers.cloudflare.com/workers/runtime-apis/rpc

これが一見地味なんですが、非常に楽しい未来を想像できるので、書いてみます。というか以前chimameさんが書いた記事でだいぶ理解できるのですが、もう少し噛み砕いて書いてみます。

https://zenn.dev/chimame/articles/f86db24897be6a

Bindings

Cloudflareにはいくつもプロダクトがあります。ストレージのR2、データベースD1、KVストアのKVなどです。そしてそれらに繋ぐ方法が「Bindings」という方法です。このBindingsで繋げられるものはたくさんあります。

  • AI
  • Analytics Engine
  • Browser Rendering
  • D1
  • Environment Variables
  • Hyperdrive
  • KV
  • mTLS
  • Queues
  • R2
  • Rate Limiting
  • Secrets
  • Service bindings
  • Vectorize

この概念と手段がとてもよいです。Kentonが書いたブログから引用するとこういうことです。例えばKVというプロダクトのBindingsがあります。これをCloudflare Workersから利用するのはめちゃくちゃ簡単です。

export default {
  async fetch(request, env, ctx) {
    return new Response(await env.MY_KV.get('content'))
  }
}

fetchの第2引数のenvMY_KVというのが生えてそれがそのままKVオブジェクトになっています。KVオブジェクトとはTypeScriptの型でいうとKVNamespaceで、get()put()delete()などを備えていて、そのまま使えて、実際のKVデータベースにアクセスできます。唯一やることはwrangler.tomlに以下を書くだけです。

[[kv_namespaces]]
binding = "MY_KV"
id = "KVデータベースを識別するID"

一方、Bindingsを使わないでKVにつなぐとしたらどんな方法になるでしょうか。一例は以下です。

// SDKのクライアントになるのかな??
import { KV } from 'cloudflare:kv'

export default {
  async fetch(request, env, ctx) {
    // データベースにつなぐのにシークレットキーが必要??
    // それらは環境変数から来るのか??
    const myKv = KV.connect('my-kv-namespace', env.MY_KV_AUTHKEY)
    return new Response(await myKv.get('content'))
  }
}

これには2つ問題があります。

  1. セキュリティ - キーの管理、キーの処理を書かなくてはいけない
  2. 開発者体験 - SDKを使わないといけない、connectの処理は必要

これを解決しているのがBindingsというわけです。ぼくはシンプルに書けるBindingsという方法がとても好きです。

Service Bindings

さて一方、Cloudflare WorkersにはService Bindingsという、複数のWorker同士をつなぐ方法があります。これはAというWorkerがBというWorkerにインターナルなfetchをかけて呼び出し、その結果をAが使うというものです。以下のサンプルを見ると分かりやすいでしょう。

export default {
  async fetch(request, env, ctx) {
    const authResponse = await env.AUTH_SERVICE.fetch('https://auth/getUser', { headers: request.headers })
    const userInfo = await authResponse.json()
    return new Response('Hello, ' + userInfo.name)
  }
}

これは便利なんですが、いまいち使いにくいという状況がありました。

JS RPC

JS PRCについてですが、理解する方法が2つの方向からできます。この2つです。

  1. 自分でカスタムBindingsをつくれる!
  2. Service Bindingsが簡単にできる!

固有名詞ばかりなので難しいかもしれませんが、コードは簡単です。足し算を計算をするWorkerを作ってみましょう。

import { WorkerEntrypoint } from 'cloudflare:workers'

export default class Calc extends WorkerEntrypoint {
  async add(a: number, b: number) {
    return a + b
  }
}

これで足し算をするWorkerが定義できて、他のWorkerからBindingとして使うことができるようになるのです!

他のWorkerのプロジェクトのwrangler.tomlにKVの時と同じような設定を書きます。といっても、上記のプロジェクト名service、プログラム内でどのような名前で扱うか?というbindingです。

[[services]]
binding = "CALC"
service = "calc"

コードから呼び出してみます。

export default {
  async fetch(request, env) {
    const result = await env.CALC.add(1, 2)
    return new Response(`Result is ${result.toString()}`)
  }
} satisfies ExportedHandler<Env>

このようにJavaScriptのオブジェクトを扱うのと同じように、機能を呼び出しています。これが「JS RPC」と呼ばれている所以です。

TypeScriptサポート

このCALC.add()というメソッドは適切に設定すればちゃんと型がつきます。素晴らしい。

SC

例: データベースを扱うAPI

いくらでもアイデアが浮かぶのですが、例えば、D1をデータベースとするAPI、超簡単なブログサービスのWorkerを定義できます。

import { WorkerEntrypoint } from 'cloudflare:workers'
import { drizzle } from 'drizzle-orm/d1'
import { posts } from './schema'

interface Env {
  DB: D1Database
}

export default class Blog extends WorkerEntrypoint<Env> {
  async createPost({ title, body }: { title: string; body: string }) {
    const db = drizzle(this.env.DB)
    const uuid = crypto.randomUUID()
    return await db
      .insert(posts)
      .values({
        id: uuid,
        title,
        body
      })
      .returning()
  }
}

これを利用するにはこう。

import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'

//...

const app = new Hono<Env>()

const schema = z.object({
  title: z.string(),
  body: z.string()
})

app.post('/', zValidator('form', schema), async (c) => {
  const { title, body } = c.req.valid('form')
  await c.env.BLOG.createPost({
    title,
    body
  })
  return c.redirect('/blog')
})

export default app

いい感じですね!DBを使った例はchimameさんのが詳しいです。

https://zenn.dev/chimame/articles/f86db24897be6a

例のリポジトリ

これが例として作ってるリポジトリです。

https://github.com/yusukebe/js-rpc-examples

  • 計算Workrer Calc
  • BlogサービスWorker Blog
  • どちらも呼び出すUI付きアプリ Web

となっています。

マーケットプレイス

Kenton氏の記事に夢が書いてあって、叶う可能性があるのですが、ユーザーがつくったカスタムBindigsを流通させるマーケットプレイスみたいなのができるんじゃないかと。面白いですね。ちなみに、今、ユーザーがつくったBindingsはそのユーザーしか使えませんが、他のユーザーが使えるようになるかもです。

まとめ

以上、JS RPCについての理解をするための記事でした。具体的な実装はドキュメントを見てください。今のところプロジェクトのセットアップが面倒ですが、実装自体は簡単です。また、マーケットプレイス以外にも夢が広がる機能なので、夢を膨らませてみてください。

Discussion