Cloudflare WorkersのService bindingsの現状整理
2023年7月版ってことで。
これなに
とあるWorkerから、別のWorkerを呼び出せる仕組み。
前にも書いてるけど、デプロイしたWorkerAから別のWorkerBを呼びたいときがあったとしても、HTTP経由でさえそれができない場合があった。
Cloudflare Workersで、Workerから別のWorkerを呼びたい - console.lealog();
複数のWorkerでマイクロサービス的な構成をしたい場合にもれなく不便やったけど、それができるようになる。
加えて、HTTP(インターネット)経由じゃなく、Cloudflare内の特別な経路を通るため、パフォーマンスも安定するよって触れ込み。
KVや他のバインディングと同じように登録しておくと、さもそこにWorkerがあるかのように`fetch()`できる。
export default <ExportedHandler<{ MY_SVC: Fetcher }>> { async fetch(req, env) { return await env.MY_SVC.fetch(req); }, };
制限と特徴
Service bindingsをアーキテクチャに加えるか判断するために、知っておくべきことたちを、ドキュメントからサマリしておく。
- CPUリソースは、親(呼び出し元)のWorkerと共通
- `waitUntil()`か`await`しないと、親と共に消える
- サービスのネストは32層まで
などなど、基本的な理解として、呼び出し元の親Workerのリソースを消費するってところがポイント。
つまり、こういうサービスを書いたとして、
// Sub export default { fetch: async () => new Response("Service!"), }
それをService bindingsで呼び出す場合も、
// Main export default { fetch: async (req, env) => env.MY_SVC.fetch(req), }
そのサービスのコードをモジュールとして使う場合も、
// Main import MY_SVC from "..."; export default { fetch: async (req) => MY_SVC.fetch(req), }
できることは一緒ってことかと。(ネットワーキングのルートは違う)
つまり、CPUヘビーな処理を逃がす目的では使えない!
HTTP経由じゃない分のパフォーマンス向上はあるかもしれないが、そういう意味での違いは、Workerごとのバンドルサイズ制限(free: 1MB / Paid: 10MB)を回避できるってことくらい?
`wrangler dev`の対応状況
良し悪しはさておき、これを採用する場合は、ローカルで`wrangler dev`することになるはず。
手順としては、
というように、複数のプロセスを立ち上げると自動で連携されて、ローカルで確認できるようになる。
注意点としては、メインの呼び出す側をどう起動したかによって、その挙動が変わるってところ・・・。
呼び出す側の親のWorkerの状態としては、この3つのパターンがあって、
それぞれこんな感じの対応だった。
- O: main:dev@local - sub:dev@local - X: main:dev@local - sub:dev@remote - X: main:dev@local - sub:deployed - X: main:dev@remote - sub:dev@remote - X: main:dev@remote - sub:dev@local - O: main:dev@remote - sub:deployed - O: main:deployed - sub:deployed
つまり、親がローカルだとローカルにあるサービスにしかつなげられないし、親がリモートだと実際にデプロイされてるサービスにしかつなげられない。
- `dev --remote`でサービスを立てても、それにアクセスする術がない
- `dev --remote`と`dev --local`を混ぜることもできない
という、かなりわかりにくい上に厳しい感じで、Issueもお察し。
RFC: Multi-worker development · Issue #1182 · cloudflare/workers-sdk · GitHub
その他
TypeScriptの型
`env.MY_SVC`をどういう型にすればいいかというと、`Fetcher`っていう型らしい。
(ただし`env.MY_SVC.constructor.name`は、ローカルでは`Object`のまま)
`Fetcher`は`fetch()`のほかに`connect()`っていうメソッドもあって、わかる人にはそういうことかって感じ。
`req.url`どうなる
サービスは`fetch()`で呼び出すわけではあるが、そのリクエストの行き先はそのサービスって決まってるわけで。
ならリクエストされた側として、URLのオリジンとかどうなってんの?って思ったので調べてみたところ、
- リモートでは、呼び出し元と同じオリジンが入る(親と同じ)
- ローカルでは、そのサービスの起動オリジンのまま・・・
- オリジン以降のパスやらは、`env.MY_SVC.fetch()`時に渡した内容になる
サービスを呼び分けたい場合は、リクエストのパスか、ヘッダーか、ボディで。
ブラウザでは許されるが、CFWの`fetch()`(というか`Request`もか)は、フルのURLでしか指定できないので、そこは注意。
// NG env.MY_SVC.fecth("/foo/bar?x=1&yy"); // OK env.MY_SVC.fecth("http://fake-host.example.com/foo/bar?x=1&yy"); // OK const req = new Request(request, { body: "..." }) env.MY_SVC.fecth(req);
なんでインターフェースを`fecth()`にしたんや?って気持ちはある。
おわりに
- バンドルサイズの制限を回避したい
- チームの都合などで、モジュールでの共有は難しい
- HTTP経由になんらかの懸念がある
って場合にだけ(というかまぁ、マイクロサービスしたいときだけ?)、(なんかいろいろ不安定なのを)がんばって使えばいいけど、そうでないなら使わないやつかな・・・。
それでもがんばりたいあなたのために、拙作の`wrangler dev`ブリッジでは、local/remoteを混ぜて使えるようにしておきました。