Cloudflare WorkersとHono🔥を学ぶ
はじめに
Cloudflare Workersをキャッチアップします。Cloudflare Workers上で動くHonoというフレームワークの評判がとても良いです。
せっかくなのでクラウドフレアのドキュメントを読みつつ、Honoのソースコードを読みつつの、Honoっぽいフレームワークを作成してキャッチアップしていきたいと思います。
Cloudflareとは
CDNの提供から始まり、強力なWAFを提供している企業というイメージでした。最近はCloudflare Workersというエッジコンピューティングが人気を集めており、それ以外にもドメインレジストラ、オブジェクトストレージ、サーバーレスDB、KVストア、キューイングなどのサービスも提供しています。
ニューヨーク証券取引所に上場しています。
Cloudflare Workersとは
エッジコンピューティングのサーバーレスプラットフォームです。世界各地のデータセンターにWebサーバーを設置することで、リクエスト元のロケーション近いサーバーでそのリクエストを処理し、レスポンスを返すことができます。(地理的な優位性)
また、似たサービスにAWS LambdaやGCP Cloud Runなどがあります。これらはコンテナを使用したリソース管理をしているため、コールドスタートなどのオーバーヘッドが生じてきます。*1
反面、Cloudflare WorkersはV8のIsolateと呼ばれる機能でサンドボックス環境を作成しコンテキスト管理をしているため、コールドスタートそのものが発生しません。*2
また、Node.jsではなくV8をそのままJavaScriptの実行環境として使用しているのでNode.jsより高速なようです。(Node.jsも内部ではV8を使用している)
*1 AWS Lambda@Edge vs. Cloudflare Workers
*2 How Workers works#Isolates
Cloudflare Workers考察
120か国以上にデータセンターを構えているようで、日本でも東京、大阪、福岡、那覇の4都市にデータセンターを持っています。
クラウドフレアが提供するエッジロケーション上で動くDBやストレージ、キャッシュサービスなどと連携もしやすいはずで、パフォーマンスを気にした薄いバックエンド(BFFやWebサーバーなど)を配置するには、最適解の1つになりそうです。
(Next.jsなど使用するとき、それ用に最適化されたVercelなど利用した場合はまた違ってくるかもです…)
Honoとは
ブラウザなどで使用されているWeb標準の技術を使用して作られた軽量でシンプルなWebアプリケーションフレームワーク。Web標準に則っておりCloudflare Workersを始め、多くのプラットフォームで動作します。また、公式のNode.jsアダプターを使用することでNode.js上でも動かすことができるそうです。
Hono感想
Honoは「シンプルで使いやすいフレームワーク」という噂は聞いていましたが、その通りだと思いました。私自身も重厚なフレームワークはそれ自体に振り回されてしまうので、薄いフレームワークを好みます。実装もExpress.jsを使ったことがある人ならすぐにキャッチアップできそうです。
また、両面TypeScriptで開発している場合、得たい恩恵の1つに「フロントエンドとバックエンドの型の共有」があります。これを実現するためtRPCを使用することがありました。zodを使用した型の共有の恩恵に比べればtRPCの癖は許容範囲だと思っていました。
しかしHonoでは同じようなことをとてもシンプルに行えます。これには「あれ、だったらtRPCじゃなくて良きかな?」と感じました。*1
Node.js上でのパフォーマンスはExpress以上、Fastify未満*2 とのことなので、今後はNode.js上でも選択肢に入ってくるかもしれません。
また、どの言語でBFFやWebサーバーを作っていると「認証、セキュリティヘッダー、ログ、リクエストID、包括的なエラー処理」あたりの実装は必要になってくると思います。
ここあたりが使いやすいインターフェースで元から提供されているのも嬉しかったです。
そして何より、ドキュメントがシンプルで最高でした。
*1 見よ、これがHonoのRPCだ
*2 Honoの今の状況
Honoのソースコードを読んでみた。
噂通りとてもシンプルです。1時間ほどあればアウトラインを掴むことができると思います。
クラウドフレアのキャッチアップ目的でしたが、クラウドフレアと言うよりもTypeScriptによるシンプルで使いやすいフレームワークの設計を学ばせていただきました。
Honoを参考にフレームワークっぽいものを作ってみた
練習で作ったので実用性0です。Honoと似たインターフェースを持つフレームワークを目指しました。
/src/hiba
がhonojs/honoにあたるディレクトリ。
/src/middlewares
がhonojs/middlewareにあたるディレクトリ。
それぞれをindex.tsから使用しています。
Hiba
なんとなく響きがHonoっぽかったので火花のHibaにしました(笑)
以下がほぼ全実装です。
ルーティング、コンテキスト、エラー処理、ミドルウェア機能のみを提供しています。
ルーティング
Honoに習って以下のインターフェースで抽象化しています。
src/hiba/router/poor-router.ts
に実態はありますが、Httpメソッドとパスの完全一致でルートを返すだけの貧相なルーターです。/:id
などのパラメーターは使用できません(泣)
こういったリッチなルーティングを使用する場合はitty-routerなどを使用してルーターの実態を作成するのが手軽でしょうか。
パスの登録は上記の用に内部で保持しているルーターにaddしているだけです。
こんな感じでパスとハンドラを登録できます。
ルーターの実態はHibaのコンストラクタで指定できます。
検証
GET/POST
※ GETとPOSTにしか対応していません。
$ curl http://localhost:8787/hello -i
HTTP/1.1 200 OK
Content-Length: 34
Content-Type: application/json
x-hello: hello world
{"hello":"world from development"}%
$ curl http://localhost:8787/hello -i -X POST -s | grep 'HTTP/1.1'
HTTP/1.1 201 Created
コンテキスト
Honoと同じように、コンテキストに使い勝手のよいフィールドやメソッドを持たせます。ルーターに登録されているハンドラに、都合の良いコンテキストを渡たしています。
検証
c.json/c.env
$ curl http://localhost:8787/hello -i
HTTP/1.1 200 OK
Content-Length: 34
Content-Type: application/json
x-hello: hello world
{"hello":"world from development"}%
Cloudflare Workersの環境変数
環境変数の管理
Cloudflare Workersの設定値はwrangler.tomlに記載し、worker-configuration.d.ts
でEnvの型を拡張します。
シークレットに関してはnpx wrangler secret put <KEY>
で登録し、環境変数同様にworker-configuration.d.ts
でEnvの型を拡張して使用できます。
なを、ローカル環境では.dev.vars
に記載することで環境変数を上書きできます。
$ npx wrangler secret put ENVIRONMENT
⛅️ wrangler 3.78.7
-------------------
✔ Enter a secret value: … **********
🌀 Creating the secret for the Worker "hiba"
✨ Success! Uploaded secret ENVIRONMENT
$ npx wrangler secret list
[
{
"name": "ENVIRONMENT",
"type": "secret_text"
}
]
エラー処理
上記の用に設定することで包括的なエラー処理が設定できます。
仕組みとしてはハンドラーの実行のTryCatchで設定したエラー処理を呼んでいます。
検証
404/500
$ curl http://localhost:8787/notfound -i
HTTP/1.1 404 Not Found
Content-Length: 9
Content-Type: text/plain;charset=UTF-8
Not Found%
$ curl http://localhost:8787/error -i
HTTP/1.1 500 Internal Server Error
Content-Length: 21
Content-Type: text/plain;charset=UTF-8
Internal Server Error%
個人的な考え
サーバーサイドでのエラーの扱い方
.
├── server
│ ├── api
│ ├── usecase
│ ├── domain
│ └── infra
私は上記のような構成を取ることが多いです。
そのさい、ExpressやHono(つまりAPI)の関心事は/apiに閉じ込めたいです。
ですが、エラーはどこのレイヤーからでもスローすることを許可し、上位のレイヤーでそのレイヤー都合で扱ってくれると良いと思っています。
なので、今回Hiba内ではなく呼び出し元でエラーを定義し、呼び出し元のエラーハンドラーで良きに処理しています。
ミドルウェア
ルーター同様に内部で保持している配列にaddしています。
ハンドラ実行時に再帰的に登録されているミドルウェアを実行していきます。
こんな感じでミドルウェアが登録できます。全ルートに適用させることしかできません笑
認証用のミドルウェアです。
$ curl http://localhost:8787/hello -i -H "Authorization: Bearer 123"
このようなリクエストを想定しています。
declareでContextにフィールドを記載することでContextの型を拡張しています。
Cloudflare Workersでも利用可能なAsyncLocalStorageとDIコンテナを合わせるともっと面白いことができそうです 🤭
検証
middleare123/auth
middleare123
// app.use(auth);
app.use((c, next) => {
console.log("middleware 1 start");
const resp = next();
console.log("middleware 1 end");
return resp;
});
app.use((c, next) => {
console.log("middleware 2 start");
const resp = next();
console.log("middleware 2 end");
return resp;
});
app.use((c, next) => {
console.log("middleware 3 start");
const resp = next();
console.log("middleware 3 end");
return resp;
});
$ curl http://localhost:8787/hello -i
# コンソールで…
middleware 1 start
middleware 2 start
middleware 3 start
/hello
middleware 3 end
middleware 2 end
middleware 1 end
auth
$ curl http://localhost:8787/hello -i
HTTP/1.1 401 Unauthorized
Content-Length: 12
Content-Type: text/plain;charset=UTF-8
Unauthorized%
$ curl http://localhost:8787/hello -i -H "Authorization: Bearer 123"
HTTP/1.1 200 OK
Content-Length: 34
Content-Type: application/json
x-hello: hello world
{"hello":"world from development"}%
デプロイ
エントリーポイントでExportedHandler<T>["fetch"]
を満たすフィールドを設置することで、Cloudflare Workers上に公開できます。
Hibaがsatisfies ExportedHandler<Env>;
に合格しているのでHibaをexportすることで上記の仕様を満たすことができます。
// https://developers.cloudflare.com/workers/get-started/guide/#3-write-code
export default {
async fetch(request, env, ctx) {
return new Response("Hello World!");
},
};
ちょっと分かりづらかったと思いますが、公式では上記の用に至ってシンプルです。
npm run deploy #(wrangler deploy)
これはクラウドフレアが提供しているCLIの機能ですが、難しい設定を入れずともdeployコマンドだけで済むの良いですね。
実際にはCIに組み込むことになると思いますが、それでいても簡単にできそうです。
検証
本番環境でhello
$ curl https://hiba.1728394857362134.workers.dev/hello -i -H "Authorization: Bearer 123"
HTTP/2 200
date: Sun, 22 Sep 2024 08:53:02 GMT
content-type: application/json
content-length: 33
x-hello: hello world
report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=v9Zftm2cvro99Bj5yxjTIvarybZrUDE1Qrq%2BbVVBi7Us%2BQ523lhEqBvCuI4EsIdmNCO3MLp%2BhtGW3v0NSdHo75WcUjInxaDBEqX4IRz9QP1o7%2FRqEYcZgizhTMe0OrZNzyWD7WuA02gks93ffvS43MBRt98%3D"}],"group":"cf-nel","max_age":604800}
nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
server: cloudflare
cf-ray: 8c710ff2b943d497-NRT
{"hello":"world from production"}%
最後に
- R2(S3互換オブジェクトストレージ)
- D1(リレーショナルデータベース)
- KV(KeyValueストア)
ここあたりもWebアプリケーションを作っていると定番な構成要素だと思います。時間あるときにこれらの可用性やレプリケーションの仕様なども見てみたいですね 🙆♂️
Discussion