天然パーマです。

Cloudflare Workersのランタイム「workerd」を触ってみた

昨日、Cloudflare WorkersのランタイムがOSSとして公開されました。 その名も「workerd」。「ワーカー・ディー “worker dee”」と発音するらしいです。

早速触ってみました。では、上記ブログ記事とGitHubのリポジトリに書いてあるworkerdについての説明を一部抜粋して紹介しつつ、動かしてみた件について紹介します。

一部DeepLで翻訳して加工した部分があります。

3つの特徴

workerdには3つの使い方があるよーと言ってます。

  1. アプリケーション・サーバー - Cloudflare Workersをセルフホストできます。
  2. 開発ツール - テストとかサーバーをローカルでできます。これまでCloudflare WorkersをWranglerでローカルエミュレートするにはMiniflareを使っていましたが、それを置き換えると思われます。
  3. プログラマブルなプロキシ - リバースでもフォワードでも。ネットワークリクエストをインターセプトしたり加工したり、ルーティングしたりします。バックエンドオリジンがあるCDNの使い方のことだと思います。

紹介

デザイン方針。

  • サーバーファースト、CLIとかGUIじゃなくてサーバーです!その点、DenoとかBunと違います。
  • fetchに代表されるようなWebスタンダードを採用しています。
  • Nanoservices - 後述
  • Homogeneousデプロイ - 後述
  • Capability bindings - 後述
  • Always backwards compatible - 後述

workerdはベータです

以下翻訳。

Workerd のコードのほとんどは Cloudflare Workers で何年も使われていますが、workerd の設定フォーマットとトップレベルのサーバーコードは真新しいものです。私たちはまだ、これを実運用した経験があまりありません。そのため、粗削りな部分があり、もしかしたらとんでもないバグがあるかもしれません。実運用へのデプロイは自己責任でお願いします(ただし、うまくいかなかった場合は私たちに教えてください!)。

ベータなので以下の通りです。

  • エラーログがちゃんとしてません。
  • 各環境でのバイナリビルドが不十分です。
  • マルチスレッドは未実装です。
  • パフォーマンス・チューニングをしていません。したら"Hello World"に関して2倍よくなります。
  • Durable Objectsはインメモリしかサポートしていません。
  • Cache APIは未実装です。
  • Cron Triggerのエミュレーションはまだサポートしていません。
  • Workersにパラメータを渡すことができません。
  • Devtoolsによるインスペクションに対応していません。
  • テストが不十分です。
  • ドキュメントが不十分です。

注意!wokerdは「hardened sandbox」ではありません

workerd は、各 Worker を分離して、アクセスするように設定されたリソースにのみアクセスできるようにしようとします。しかし、workerd 単体では、実装バグの可能性に対して適切な深層防護がありません。悪意のあるコードを実行するために workerd を使用する場合、仮想マシンなどの適切な安全なサンドボックス内で実行する必要があります。特に Cloudflare Workers ホスティングサービスは、さらに多くの多重防御を使用しています。

Nanoservices

Microservicesに対する形で「Nanoservices」をworkerdは提案。というか既に実装されているし、Cloudflareではそのような思想で運用しているっぽい。

ブログ記事より抜粋、DeepLで翻訳します。

しかし、マイクロサービスには代償があります。従来はライブラリを高速に呼び出していたものが、ネットワークを介した通信が必要になります。オーバーヘッドが増えるだけでなく、この通信にはセキュリティや信頼性を確保するための設定や管理も必要です。これらのコストは、コードベースがより多くのサービスに分割されればされるほど、大きくなります。最終的には、コストが利益を上回ります。

ナノサービスは、独立したデプロイの利点を、ライブラリ呼び出しに近いオーバーヘッドで実現する新しいモデルです。workerd を使用すると、多くの Worker を同じプロセスで実行するように設定することができます。各 Worker は別々の「アイソレート」で実行され、他から独立して動作しているように見えます。各アイソレートは別々のコードを読み込み、独自のグローバルスコープを持ちます。しかし、ある Worker が明示的に別の Worker にリクエストを送信すると、送信先の Worker は実際には同じスレッドでゼロレイテンシで実行されます。つまり、より関数呼び出しに近い形で実行されます。

関数呼び出しに近いと言われれば想像が容易いです。

Homogeneous deployment

どう訳すのか分かりませんが、概念は理解できます。 簡単に言うと「全てのデプロイ先に全く同じ中身をデプロイする」ということです。 このサーバー・コンテナをみても、同じです。 workerdに限らず、Cloudflareは以前からこの手法をとっているとのことです。

典型的なマイクロサービス・モデルでは、ローカル・ネットワークで接続されたマシンのクラスタ上で動作するコンテナに、さまざまなマイクロサービスをデプロイすることができます。各サービスに割り当てるコンテナの数を手動で選択するか、リソースの使用状況に基づいて何らかの形で自動スケーリングを構成することができます。

そこで、workerdは「全てのマシンで全てのサービスを動かす」という代替手段を提案します。

workerd のナノサービスは、一般的なコンテナよりもはるかに軽量です。その結果、1 台のサーバで数百、数千といった非常に多くのサービスを実行することが全く合理的です。これはつまり、あなたのフリート内のすべてのマシンに、すべてのサービスを単純にデプロイできることを意味します。

その結果こうなります。

Homogeneous deploymentは、個々のサービスのスケーリングを心配する必要がないことを意味します。その代わり、クラスタ全体にリクエストをロードバランスし、必要に応じてクラスタをスケールさせるだけでよいのです。全体として、必要な管理作業の量を大幅に削減することができます。

Always backwards compatible

これはこれまでのCloudflare Workersでも採用されていた方法です。

Cloudflare Workers では、実稼働中の Worker を絶対に壊さないという厳格なルールがあります。この後方互換性へのこだわりは、workerd にも及びます。

workerd は Worker の互換性日付システムを共有して、壊れる変更を管理します。すべての Worker は"compatibility date"を設定する必要があります。そしてランタイムは、API がその日付と全く同じように動作することを保証します。

“compatibility date"を指定すれば、ライブラリをアップデートしたとしても その時点でのランタイムAPIを使うことになります。 これで、APIの変更によるアプリケーションへの影響がありません。


以上、概念的なことをGitHubリポジトリのREADME、ブログ記事から紹介しました。 これらを踏まえて実際に触ってみました。その件について書きます。

Hello World

まずHello Worldしてみましょう。

workedはnpmライブラリとして公開されているので、npm installyarn addで入ります。 Cloudflare Workers向けのフレームワークのHonoも入れます。

yarn init -y
yarn add -D workerd
yarn add hono

次にコードを書きます。当然ながら、Cloudflare Workersで使うコードです。 TypeScriptで書きます。

import { Hono } from "hono";

const app = new Hono();

app.get("/", (c) => c.text("Hello worked"));

export default app;

workerdはJavaScriptを読むのでこれをesbuildでビルドします。 もちろん、JavaScriptで直で書けばこの工程は必要ないです。

esbuild --bundle --format=esm --outfile=dist/worker.mjs src/index.ts

最後に.capnpで設定を書きます。

using Workerd = import "/workerd/workerd.capnp";

const config :Workerd.Config = (
	services = [
		(name = "main", worker = .mainWorker),
	],

	sockets = [
		# Serve HTTP on port 8080.
		( name = "http",
			address = "*:8080",
			http = (),
			service = "main"
		),
	]
);

const mainWorker :Workerd.Worker = (
	compatibilityDate = "2022-09-17",

	modules = [
		( name = "dist/worker.mjs", esModule = embed "dist/worker.mjs" ),
	]
);

上記した通りcompatibilityDateも指定してます。

これで完了。いよいよworkerdを実行します。

workerd serve config.capnp

はい、何も表示されませーん。 でも、HTTPサーバーは動いています!

やりました!紛れもなくworkerdで動いています。 Node.jsでも、Denoでも、Bunでもなくworkerdのランタイムで動いています!

全部動く

Honoにはいくつかの機能があるので、動かしてみます。

まずBasic認証から。

動いた!これでcrypto周りの挙動が正しいことが分かります。 JSXも動かしてみましょう。

動きました!中身Cloudflare Workersなので当然っちゃー当然なのですが、楽しいです。

ベンチマーク

“Hello World"のベンチマーク取ります。今回使ってるHonoはマルチランタイムなので、こういう時便利です。以下でとりました。

  • Hono on workerd
  • Hono on Node.js
  • Hono on Deno
  • Hono on Bun
  • Express on Node.js

結果。

DenoとBunはめちゃくちゃ速いのが分かっていたのでこんな感じです。 それらを除くと、「Hono on Node.js」より遅いけど、Expressよりは速い、ですね。 パフォーマンス・チューニングをしていなので、この結果は無難だと思います。 そういえばworkerdはDenoやBunみたいに「速さ」をオシにしていないませんね。

KVとDurable Objectsが使える

単純なHTTPサーバーだけではなく、Cloudflare Workersの機能であるKVとDurable Objects実装されています。KVを使ってみました。

まずはKV用のコードを書きます。簡単なカウンターです。

type Bindings = {
  KV: KVNamespace;
};

const app = new Hono<{ Bindings: Bindings }>();

app.get("/count", async (c) => {
  const value = await c.env.KV.get("count");
  const count = Number(value || "0") + 1;
  c.env.KV.put("count", count.toString());

  return c.json({
    count: count,
  });
});

次にKVを使えるように、設定ファイルに追記します。

const config :Workerd.Config = (
	services = [
		(name = "main", worker = .mainWorker),
		(name = "kv", disk = ( path = "kv", writable = true, allowDotfiles = false ) )
	],
  # ...
);

const mainWorker :Workerd.Worker = (
	# ...
  		bindings = [
		( name = "KV", kvNamespace = ( name = "kv" ) ),
	],
);

disk / path で分かるようにデータの保存先のパスを指定します。 なので、今回指定したkvというディレクトリがないと実行時にエラーがでます。 では、いよいよ実行。

やりました!KVもローカルで動きました!

WranglerでCloudflare Workersの開発をやってきた人は「そんなのWrangler/Miniflareでできてたじゃん」って考えるかもしれませんが、これはまさしくworkerdで動いているのです!

複数のWorkersを立ち上げられる

さて、「Nanoservices」と聞いて、ピンときたのですが、 現状のworkerdでも複数のサービスというかWorkersを立ち上げることができます。

const config :Workerd.Config = (
	services = [
		(name = "main", worker = .mainWorker),
		(name = "sub", worker = .subWorker),
		(name = "kv", disk = ( path = "kv", writable = true, allowDotfiles = false ) )
	],

	sockets = [
		# Serve HTTP on port 8080.
		( name = "http",
			address = "*:8080",
			http = (),
			service = "main"
		),
		( name = "http",
			address = "*:8081",
			http = (),
			service = "sub"
		),
	]
);

こうやれば80008001で2つのサービスを立ち上げられます!!

バイナリ

workerd compileコマンドでバイナリを吐けます。

Wrangler

Cloudflare Workersの開発用CLI「Wrangler」は早速、このworkerdを同封したリリースをしました。

ローカルの開発サーバーにExperimentalで使えます。 通常はローカル用にはMiniflareが起動するのですが、--experimental-localオプションを付けます。 今のところService Workersモードじゃないと動かないのですが、とにかく動きます。

やたー。まだだいぶバギーですが、今後Miniflareに置き換わるでしょう。 おそらく当初の一番大きなユースケースがこのローカル開発向けです。

まとめ

以上、駆け足で紹介してきました。 昨夜の深夜リリースされたので、現状このくらいの知識です。 やれることはCloudflare Workersそのものなので、アプリケーションレベルではどうってことない話ですが、セルフホストできたり、ローカルの開発に使えたりするのは楽しいです。

なんというか、Cloudflare Workersの利点って「CDNのエッジで実行されるから云々」って話を聞きますが、僕はどちらかというとこの独自のランタイムがあって、そしてそれを取りまくWranglerがあって、KVやD1などのミドルウェアが充実している。そしてそれがいわゆる「サーバーレス」のDXのよさにつながっているのが特徴だと思います。

まだ未実装、やりきれていない点がたくさんあるので、これからも注目です。