Honoを使ってCloudflare Multi Workers環境を作る
はじめに
複数の Cloudflare Workers 間を Cloudflare Network 内で通信できる Service Bindings の機能を使って、マイクロサービスの環境を作ります。
外部に露出させる gateway アプリが 1 つ、内部にある複数の microservice アプリが複数ある構成です。
先に成果物を貼っておきます。
Monorepo環境を構築
今回は、複数の Cloudflare Workers を 1 つのリポジトリで管理するために、Monorepo を作ります。
今回は pnpm と turborepo を使いますが、yarn や npm を使ったり、lerna などのツールを使っても構いません。
パッケージ共通のツールはプロジェクトルートにインストールしておきます。
{
"devDependencies": {
"turbo": "^1.10.9",
"wrangler": "^3.1.2"
},
"scripts": {
"dev": "turbo dev"
}
}
dev でパッケージを平行に起動するために、turborepo を設定します。
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"dev": {
"dependsOn": ["^dev"]
}
}
}
Workers のコードは、workers ディレクトリにパッケージとして配置します。
packages:
- "workers/*"
node_modules
.turbo
pnpm install
gatewayアプリを作成
このサービスは唯一外部に露出させ、他の内部プライベートサービスと通信するための gateway アプリです。
create hono という Hono のテンプレートプロジェクトを生成できる CLI ツールを使います。
workers ディレクトリに移動して
npm create hono@latest gateway
# cloudflare-workers を選択
とすると、gateway ディレクトリにプロジェクトが生成されます。
生成された README.md と package.json 内の devDependencies にある wrangler は root にあるので削除しても大丈夫です。
monorepo 管理のため、package.json の name フィールドを gateway としておきます。
{
+ "name": "gateway",
...
}
- name = "my-app"
+ name = "gateway"
compatibility_date = "2023-01-01"
+ [dev]
+ port = 1234
wrangler.toml に workers アプリ名と、dev 時のポート番号を設定しておきましょう。
pnpm install で依存関係を Monorepo にインストールします。pnpm dev で指定された URL に Hello Hono! と表示されれば OK です。
microserviceアプリを作成
本来は複数の microservice が生えますが、今回は 1 つだけ private-service という名前の worker を workers フォルダ以下に作成します。
gateway をコピペして、private-service ディレクトリを作成して package.json と wangler.toml を書きます。
{
+ "name": "private-service",
...
}
- name = "gateway"
+ name = "private-service"
compatibility_date = "2023-01-01"
[dev]
- port = 1234
+ port = 1235
あとは / にアクセスしたときのレスポンスをわかりやすいように変えておきます。
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello Private Service!'))
export default app
/ にアクセスすると Hello Private Service! と返すようにしました。
tree
.
├── node_modules
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── turbo.json
└── workers
├── gateway
└── private-service
ここまででこんな感じです
Service Binding
Gateway設定
Service Binding を設定するために、gateway の wrangler.toml に services フィールドを追加します。
name = "gateway"
compatibility_date = "2023-01-01"
+ services = [
+ { binding = "PRIVATE_SERVICE", service = "private-service" }
+ ]
[dev]
port = 1234
このとき、service にはバインド先サービスの wrangler.toml の name フィールドに設定した値を指定します。
Hono設定
Hono では Service Binding を型安全に使うことができるので、それを使ってみます。
import { Hono } from 'hono'
+ type Bindings = {
+ PRIVATE_SERVICE: Fetcher;
+ };
- const app = new Hono()
+ const app = new Hono<{ Bindings: Bindings }>()
app.get('/', (c) => c.text('Hello Hono!'))
export default app
Bindings という型を定義して、Hono の Generics に渡します。
このとき、Bindings のキーは wrangler.toml の services フィールドに設定した binding の値と同じにしておきます。
Gatewayを書く
/private にアクセスしたら、private-service に転送させてレスポンスをパススルーするようにします。
import { Hono } from 'hono'
type Bindings = {
PRIVATE_SERVICE: Fetcher;
};
const app = new Hono<{ Bindings: Bindings }>()
app.get('/', (c) => c.text('Hello Hono!'))
+ app.get('/private/*', async (c) => {
+ const res = await c.env.PRIVATE_SERVICE.fetch(c.req.raw)
+ return res
+ })
export default app
/private のリクエストがそのまま private-service に転送されるので、/private にアクセスすると Hello Private Service! と返ってくるようにサブパスに配置します。
import { Hono } from 'hono'
- const app = new Hono()
+ const app = new Hono().basePath('/private')
これでサービス間の通信ができます。
Privateにする
private-service は、gateway からのみアクセスできるようにします。
まずは内部通信用に TOKEN を設定しておきます。
両方のサービスに .dev.vars を作って、その中に INTERNAL_TOKEN を設定します。
INTERNAL_TOKEN = "THIS_IS_A_SECRET_TOKEN_FOR_INTERNAL_REQUEST"
Gateway側でトークンを付与する
gateway 側で private-service にアクセスするとき、このトークンを付与します。
import { Hono } from 'hono'
type Bindings = {
PRIVATE_SERVICE: Fetcher;
+ INTERNAL_TOKEN: string;
};
const app = new Hono<{ Bindings: Bindings }>()
app.get('/', (c) => c.text('Hello Hono!'))
app.get('/private/*', async (c) => {
- const res = await c.env.PRIVATE_SERVICE.fetch(c.req.raw)
+ const res = await c.env.PRIVATE_SERVICE.fetch(c.req.raw, {
+ headers: {
+ 'x-custom-token': c.env.INTERNAL_TOKEN,
+ },
+ })
return res
})
export default app
Private Service側でトークンを検証する
private-service 側で、gateway からのアクセスであることを検証します。
import { Hono } from 'hono'
+ type Bindings = {
+ INTERNAL_TOKEN: string;
+ };
- const app = new Hono().basePath('/private')
+ const app = new Hono<{ Bindings: Bindings }>().basePath('/private')
+ app.use('*', async (c, next) => {
+ const token = c.req.headers.get('x-custom-token')
+ if (token !== c.env.INTERNAL_TOKEN) {
+ return c.text('Unauthorized', 401)
+ }
+ await next()
+ })
...
これで、gateway からのアクセスでない場合は、Unauthorized というレスポンスを 401 で返すようになりました。
Deployする
gateway と private-service をそれぞれ pnpm run deploy でデプロイします。
prod 環境には INTERNAL_TOKEN が設定されていないので、dashboard か wrangler cli で設定するのを忘れないようにしましょう。
gateway 側のデプロイされた URL から /private にアクセスすると、Hello Private Service! と返ってきます。
一方で直接 private-service 側の URL にアクセスすると Unauthorized と返ってくるようになってれば OK です。
そして INTERNAL_TOKEN が外側からは見えないようになっているはずです。
おわりに
いかがだったでしょうか。これがある程度無料で動かせるのはすごいですよね...gateway 側で色々し放題なのが素晴らしいです。
例えば Binding 先の Workers に立てた Remix の SSR 結果を gateway 一定時間キャッシュするようにして ISR のようなことをしても面白そうです。
更新頻度の低い API を Cache + Purge 式にしても良さそう。
Hono の開発体験もめっちゃ良かったので Service Binding を使うときは是非使ってみてください。
参考
Discussion