Cloudflare の JavaScript ランタイム「workerd」を動かしてみる

はじめに

Publickey で以下の記事を見つけて、どんなものか動かしてみた記録です。

https://www.publickey1.jp/blog/22/cloudflare_workersjavascriptwasmworkerdnanoserviceshomogeneous_deployment.html

詳しい解説は上記の記事を見てもらえればと思いますが、私が特に魅力を感じたのは以下の点でした。

標準準拠でロックインはされない

workerdはサーバ向けのJavaScript/WebAssemblyのランタイムですが、基本的にはWebブラウザが備えているAPIを実装しています。

と同時にDenoやNode.jsなどと共に今年立ち上げた非Webブラウザ系JavaScriptランタイムのコード互換を実現するワークグループの標準に従うとしており、コードのworkerdへのロックインは起こらないとしています。

おことわり

workerd は現時点で Beta 版です。
機能が不足していたり、破壊的変更が行われる可能性がありますので、ご注意ください。

workerd is Beta

WARNING: This is a beta. Work in progress.

この記事の動作確認では、コミットハッシュ a2376c4 を使用しています。

workerd を動かす

README の Getting Started に従ってセットアップしています。

1. リポジトリをクローン

まずはリポジトリをクローンします。

$ git clone git@github.com:cloudflare/workerd.git
$ cd ./workerd/

2. Bazel (Bazelisk) のインストール

ビルドには Bazel というツールが必要になるようです。
README のリンクを開くと Bazelisk というツールをおすすめされていたので、そちらをインストールします。

macOS を使っているので Homebrew で簡単にインストールできました。

$ brew install bazelisk

3. Xcode のバージョン確認

README を見ると、macOS の場合は Xcode 13 以降がインストールされている必要があるようです。
私の場合は 14.0.1 が入っていたので、バージョンだけ確認して終わりました。

4. workerd のビルド

bazelisk を使って workerd をビルドします。

$ bazelisk build -c opt //src/workerd/server:workerd

...

INFO: Elapsed time: 2512.784s, Critical Path: 53.88s
INFO: 8341 processes: 3309 internal, 5031 darwin-sandbox, 1 local.
INFO: Build completed successfully, 8341 total actions

大体42分ぐらいかかっています。長い。

補足

README では bazel コマンドを使うように書かれていますが、今回は bazelisk をインストールしたので、コマンドを置き換えています。

- $ bazel build ...
+ $ bazelisk build ...

5. パスを通す

ビルドした workerd のバイナリは (Repository root)/bazel-bin/src/workerd/server/workerd に出力されています。
このバイナリをパスの通ったところに移動 (or コピー) すればOKです。

(が、今回は一時的に試してみたかったので、一旦以下のようにパスを通しました)

$ export PATH="$(pwd)/bazel-bin/src/workerd/server/:${PATH}"

6. 動作確認

バージョンを確認するコマンドで、動作するかをチェックします。

$ workerd --version
workerd 2022-09-26

以上で、準備は完了です。

動かしてみる

リポジトリの /samples ディレクトリにサンプルが4つ用意されていたので動かしてみます。

Hello world

helloworldhelloworld_esm の2種類が用意されていますが、書き方の違いだけで動作的には同じでした。
以下のコマンドで実行できます。

$ workerd serve ./samples/helloworld/config.capnp
## or
$ workerd serve ./samples/helloworld_esm/config.capnp

http://localhost:8080/ にアクセスすると動作確認できます。

$ curl http://localhost:8080/
Hello World

静的ファイル配信

static-files-from-disk というディレクトリは静的なファイル配信のサンプルのようです。
以下のコマンドで実行できます。

$ cd samples/static-files-from-disk/
$ workerd serve ./config.capnp

http://localhost:8080/ にアクセスすると動作確認できます。

Result

ちなみに --directory-path site-files="(ディレクトリパス)" を指定すると配信したいディレクトリを変更できるようです。

$ workerd serve samples/static-files-from-disk/config.capnp --directory-path site-files="$(pwd)/samples/static-files-from-disk/content-dir/"

(設定ファイルのコメント に書いてありました)

チャット

durable-objects-chat というディレクトリは、チャットができるサンプルのようです。
以下のコマンドで実行できます。

$ workerd serve ./samples/durable-objects-chat/config.capnp

http://localhost:8080/ にアクセスすると動作確認できます。

Chat capture

ディレクトリ名に入っているように Durable Objects という機能を使っているようです。
初めて聞いたのでよく分かっていませんが、状態を永続化しておく機能のようです。

Durable Objects are currently supported only in a mode that uses in-memory storage

README に書かれていましたが、現時点ではメモリに保存するので永続化する事はできないようです。

その他のメモ

hello world の中身

addEventListener('fetch', ...) と書かれているように、(fetch) イベントに対してハンドラを登録するという形で実装するようです。
Service Worker の書き方と同じ感じなので、馴染みやすいですね。

// samples/helloworld/worker.js
addEventListener('fetch', event => {
  event.respondWith(handle(event.request));
});

async function handle(request) {
  return new Response("Hello World\n");
}

また esm の方だと fetch を含むオブジェクトを export default しているようです。

export default {
  async fetch(req, env) {
    return new Response("Hello World\n");
  }
};

workerd のコマンド

workerd --help を見てみると、compileserve の2種類のコマンドのみが用意されているようです。

$ workerd --help
Usage: workerd [<option>...] <command> [<arg>...]

Runs the Workers JavaScript/Wasm runtime.

Commands:
  compile  create a self-contained binary
  serve    run the server

See 'workerd help <command>' for more information on a specific command.

Options:
    --verbose
        Log informational messages to stderr; useful for debugging.
    --version
        Print version information and exit.
    --help
        Display this help text and exit.

それぞれのコマンドの説明も見てみます。

$ workerd help serve
Usage: workerd serve [<option>...] <config-file> [<const-name>]

Serve requests based on a config.

...

$ workerd help compile
Usage: workerd compile [<option>...] <config-file> [<const-name>]

Builds a self-contained binary from a config.

...

serve の方は、これまで動作確認をしてきたようにサーバーを立ち上げるために使うようです。

compile の方は、ランタイムを含むバイナリとして出力してくれるもののようです。
実際に Hello world のサンプルコードでコンパイルしてみました。

$ workerd compile samples/helloworld/config.capnp > helloworld
$ ./helloworld 

http://localhost:8080/ にアクセスするとレスポンスが返ってきました。

$ curl http://localhost:8080/
Hello World

容量的にもランタイムが含まれていそうですね。

$ wc -c ./helloworld 
 81258168 ./helloworld
 
$ wc -c ./bazel-bin/src/workerd/server/workerd
 81257684 ./bazel-bin/src/workerd/server/workerd

deno compile に似ているな、と思いました。

おわりに

また誰でも使える JavaScript ランタイムが増えて、盛り上がってきている感じがして非常に嬉しいですね。
特に標準APIをベースにして、ロックインしないことを宣言したランタイムが出てきたことで、よりブラウザ外でのAPI互換性が高まってくれることを期待しています。(参考)

まだベータ版なので色々不足しているところが多いですが、今後も動向を見守って行きたいと思います。