Cloudflare WorkersのJS RPCを理解する
前置き
4月の第1週に行われたCloudflare Developer Week 2024でAIやデータベース関係のアップデートの影に隠れ、WorkersをつくってるKenton氏のブログが2つ投下されました。
そして「JS RPC」という機能が追加されました。
これが一見地味なんですが、非常に楽しい未来を想像できるので、書いてみます。というか以前chimameさんが書いた記事でだいぶ理解できるのですが、もう少し噛み砕いて書いてみます。
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引数のenv
にMY_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つ問題があります。
- セキュリティ - キーの管理、キーの処理を書かなくてはいけない
- 開発者体験 - 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つです。
- 自分でカスタムBindingsをつくれる!
- 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()
というメソッドは適切に設定すればちゃんと型がつきます。素晴らしい。
例: データベースを扱う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さんのが詳しいです。
例のリポジトリ
これが例として作ってるリポジトリです。
- 計算Workrer Calc
- BlogサービスWorker Blog
- どちらも呼び出すUI付きアプリ Web
となっています。
マーケットプレイス
Kenton氏の記事に夢が書いてあって、叶う可能性があるのですが、ユーザーがつくったカスタムBindigsを流通させるマーケットプレイスみたいなのができるんじゃないかと。面白いですね。ちなみに、今、ユーザーがつくったBindingsはそのユーザーしか使えませんが、他のユーザーが使えるようになるかもです。
まとめ
以上、JS RPCについての理解をするための記事でした。具体的な実装はドキュメントを見てください。今のところプロジェクトのセットアップが面倒ですが、実装自体は簡単です。また、マーケットプレイス以外にも夢が広がる機能なので、夢を膨らませてみてください。
Discussion