概要
この記事では wasmtime v14.0.3 から利用できるようになったサブコマンドである wasmtime serve
について調査した内容を記載します。
Wasm コンポーネントモデルについて調査していると、コンポーネントモデルのドキュメントに HTTP コンポーネントを実行する方法が紹介されていました。
Web アプリケーションでの活用方法を模索している私としては HTTP Proxy World
などの仕組みに興味があったので、どのような仕様により実装されているかを確認しました。
どんなことができるのか
それでは wasmtime serve
サブコマンドでどんなことができるかを確認していきます。
wasmtime serve
は wasi-http の HTTP proxy worldを実装したコンポーネントを実行するためのサブコマンドとなっています。そのため引数の Wasm ファイルには HTTP Proxy World の実装と公開が必要です。
実装のために必要なインタフェースである WIT は以下の wasi-http のリポジトリに配置してあるのでこの WIT をもとにコードを実装します。
wasi-http は wasi-cloud-core の 1 つで、クラウドプロバイダーやプラットフォーマーが提供するサービスと連携するための汎用的なインタフェースなどが定義されています。その中の HTTP Proxy World では、コアとなる accept()
ループをホスト側の処理とすることで、HTTP リクエストに応じてホストが動的に Wasm インスタンスをスピンアップできる仕組みとなっています。これは柔軟なオートスケーリング(サーバーレス含む)をサポートすることを意図しています。
ここでは一部のソースコードを抜粋したものを利用して実装を確認します。完全なコードは watawuwu/blog-wasmtime-serve を参照してください。この記事のおまけとして、後ほど 2023 年 12 月時点での最新バージョンである wasmtime v15.0.1 での動作手順も記載しています。
Rust の場合は以下のような incoming-handler
インタフェースを持つ handle
関数を実装します。この辺りは FaaS を試したことがある方は HTTP イベントをトリガーとする関数と同じような感覚で実装できると思います。
impl bindings::exports::wasi::http::incoming_handler::Guest for Component {
fn handle(_request: IncomingRequest, outparam: ResponseOutparam) {
let hdrs = Headers::new();
let resp = OutgoingResponse::new(hdrs);
resp.set_status_code(200).expect("fail to set status code");
let body = resp.body().expect("fail to get body");
ResponseOutparam::set(outparam, Ok(resp));
let out = body.write().expect("outgoing stream");
out.blocking_write_and_flush(b"Hello\n")
.expect("fail to write/flush");
drop(out);
OutgoingBody::finish(body, None).expect("fail to finish");
}
}
このソースコードを cargo component build
でビルドすると、Wasm コンポーネントのファイルが作成されます。
wasmtime serve
の引数にビルドしたコンポーネントのファイルを指定し起動すると Web サーバーが起動しデフォルトでは 0.0.0.0:8080
でリッスンします。
試しに HTTP クライアントツールなどで HTTP リクエストすることで通信の確認ができます。
$ wasmtime serve hello.wasm
Serving HTTP on http://0.0.0.0:8080/
$ xh http://0.0.0.0:8080/
HTTP/1.1 200 OK
Date: Wed, 06 Dec 2023 06:46:25 GMT
Transfer-Encoding: chunked
Hello
wasmtime serve
の場合は、wasmtime 自身が Web サーバーを起動することでサービスを提供、またはエミュレートしていることがソースコードで確認できます。
Listening: wasmtime/src/commands/serve.rs
let listener = tokio::net::TcpListener::bind(self.addr).await?;
eprintln!("Serving HTTP on http://{}/", listener.local_addr()?);
let _epoch_thread = if let Some(timeout) = self.run.common.wasm.timeout {
Some(EpochThread::spawn(timeout, engine.clone()))
} else {
None
};
log::info!("Listening on {}", self.addr);
let handler = ProxyHandler::new(self, engine, instance);
loop {
let (stream, _) = listener.accept().await?;
let h = handler.clone();
tokio::task::spawn(async move {
if let Err(e) = http1::Builder::new()
.keep_alive(true)
.serve_connection(stream, h)
.await
{
eprintln!("error: {e:?}");
}
});
}
そして HTTP リクエストのトリガーが発生すると必要なオブジェクトを作成し、実装されたハンドル関数が呼び出され、関数の戻り値を元に HTTP レスポンスされます。
Call Handle: wasmtime/src/commands/serve.rs
Wasm でも Thread や WASI により socket、io など Web アプリケーションに必要な技術要素の取り込みは進んでいます。ただ、こういったリソースにアクセスさせる方法だけでなく、ホスト側にリソースの管理を任せ、Wasm アプリケーション側ではリソースへのアクセスを限定的にさせる方法も良さそうです。
おまけ: 起動までの手順
このサンプルコードは sunfishcode/hello-wasi-http をもとに wasmtime v15 に対応をさせたものとなります。
この手順は Rust だけでなく、cargo component
コマンド、cargo-component-bindings
ライブラリ、そして WIT の一部の仕様を把握してないと理解が難しい部分があり、私もまだ勉強中です。特にcargo component
コマンドと cargo-component-bindings
ライブラリについては理解が浅いため説明を一部省略しています。
まずは cargo component
を利用し、Wasm コンポーネントモデル用のプロジェクトを作成します。
component new
サブコマンドに --reactor
というオプションが付与されていますが、こちらはライブラリを作成するモードのようなものと思っていただければ大丈夫です。
詳細はこちらに説明がありますので参照してください。
$ cargo component new --reactor hello && cd hello
次に wasmtime v15.0.1 バージョンのバイナリで実行したいので、wasmtime のソースコードから HTTP ハンドラーの実装に依存する WIT をコピーしてきます。先ほどの説明では wasi-http/wit の WIT を利用すると記載しましたが、 wasmtime v15.0.1 が依存するバージョンが不明だったので wasmtime リポジトリからコピーしています。
# wasmtime v15.0.1 のソースコードをダウンロードし WIT をコピーする
$ wget -O- https://github.com/bytecodealliance/wasmtime/archive/refs/tags/v15.0.1.tar.gz | tar xzf -
$ cp -a wasmtime-15.0.1/crates/wasi/wit/deps wit
$ rm -rf wasmtime-15.0.1
$ tree .
├── Cargo.toml
├── src
│ └── lib.rs
└── wit
├── deps
│ ├── cli
│ │ ├── command.wit
│ │ ├── environment.wit
│ │ ├── exit.wit
│ │ ├── reactor.wit
│ │ ├── run.wit
│ │ ├── stdio.wit
│ │ └── terminal.wit
│ ├── clocks
│ │ ├── monotonic-clock.wit
│ │ ├── wall-clock.wit
│ │ └── world.wit
│ ├── filesystem
│ │ ├── preopens.wit
│ │ ├── types.wit
│ │ └── world.wit
│ ├── http
│ │ ├── handler.wit
│ │ ├── proxy.wit
│ │ └── types.wit
│ ├── io
│ │ ├── error.wit
│ │ ├── poll.wit
│ │ ├── streams.wit
│ │ └── world.wit
│ ├── random
│ │ ├── insecure-seed.wit
│ │ ├── insecure.wit
│ │ ├── random.wit
│ │ └── world.wit
│ └── sockets
│ ├── instance-network.wit
│ ├── ip-name-lookup.wit
│ ├── network.wit
│ ├── tcp-create-socket.wit
│ ├── tcp.wit
│ ├── udp-create-socket.wit
│ ├── udp.wit
│ └── world.wit
└── world.wit
次にアプリケーション用の WIT に先程コピーしてきた http/proxy
のパッケージを include
する World を定義します。
package watawuwu:hello;
world hello-world {
include wasi:http/proxy@0.2.0-rc-2023-11-10;
}
include
は対象の World と同じ export/import
の設定を持つため、この include
により export incoming-handler;
が設定されます。
そのため、この WIT を利用すると incoming-handler
インタフェースを実装するためのグルーコードが生成されます。
$ cat wit/deps/http/proxy.wit
package wasi:http@0.2.0-rc-2023-11-10;
/// The `wasi:http/proxy` world captures a widely-implementable intersection of
/// hosts that includes HTTP forward and reverse proxies. Components targeting
/// this world may concurrently stream in and out any number of incoming and
/// outgoing HTTP requests.
world proxy {
/// HTTP proxies have access to time and randomness.
import wasi:clocks/wall-clock@0.2.0-rc-2023-11-10;
import wasi:clocks/monotonic-clock@0.2.0-rc-2023-11-10;
import wasi:random/random@0.2.0-rc-2023-11-10;
/// Proxies have standard output and error streams which are expected to
/// terminate in a developer-facing console provided by the host.
import wasi:cli/stdout@0.2.0-rc-2023-11-10;
import wasi:cli/stderr@0.2.0-rc-2023-11-10;
/// TODO: this is a temporary workaround until component tooling is able to
/// gracefully handle the absence of stdin. Hosts must return an eof stream
/// for this import, which is what wasi-libc + tooling will do automatically
/// when this import is properly removed.
import wasi:cli/stdin@0.2.0-rc-2023-11-10;
/// This is the default handler to use when user code simply wants to make an
/// HTTP request (e.g., via `fetch()`).
import outgoing-handler;
/// The host delivers incoming HTTP requests to a component by calling the
/// `handle` function of this exported interface. A host may arbitrarily reuse
/// or not reuse component instance when delivering incoming HTTP requests and
/// thus a component must be able to handle 0..N calls to `handle`.
export incoming-handler;
}
次は cargo-component-bindings(wit-bindgen)
で WIT からグルーコードを生成できるように、Cargo.toml を追加、修正します。
...
[package.metadata.component]
package = "watawuwu:hello"
# package.metadata.component.dependencies ではない点に注意!!
[package.metadata.component.target.dependencies]
"wasi:cli" = { path = "wit/deps/cli" }
"wasi:clocks" = { path = "wit/deps/clocks" }
"wasi:filesystem" = { path = "wit/deps/filesystem" }
"wasi:http" = { path = "wit/deps/http" }
"wasi:io" = { path = "wit/deps/io" }
"wasi:random" = { path = "wit/deps/random" }
"wasi:sockets" = { path = "wit/deps/sockets" }
最後に incoming-handler
インタフェースを実装します。
cargo_component_bindings::generate!();
use bindings::wasi::http::types::{
Headers, IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam,
};
struct Component;
impl bindings::exports::wasi::http::incoming_handler::Guest for Component {
fn handle(_request: IncomingRequest, outparam: ResponseOutparam) {
let hdrs = Headers::new();
let resp = OutgoingResponse::new(hdrs);
resp.set_status_code(200).expect("fail to set status code");
let body = resp.body().expect("fail to get body");
ResponseOutparam::set(outparam, Ok(resp));
let out = body.write().expect("outgoing stream");
out.blocking_write_and_flush(b"Hello\n")
.expect("fail to write/flush");
drop(out);
OutgoingBody::finish(body, None).expect("fail to finish");
}
}
これで cargo component build
でビルドし、wasmtime serve
が実行できます。
$ cargo component build
Compiling proc-macro2 v1.0.70
Compiling unicode-ident v1.0.12
Compiling cargo-component-macro v0.5.0
Compiling bitflags v2.4.1
Compiling wit-bindgen v0.14.0
Compiling quote v1.0.33
Compiling syn v2.0.39
Compiling cargo-component-bindings v0.5.0
Compiling hello v0.1.0 (/Users/wmatsui/dev/src/github.com/zlabjp/zz_work/sig-wasm/2023-12-05_qiita/hello)
Finished dev [unoptimized + debuginfo] target(s) in 3.68s
Creating component /Users/wmatsui/dev/src/github.com/zlabjp/zz_work/sig-wasm/2023-12-05_qiita/hello/target/wasm32-wasi/debug/hello.wasm
$ wasmtime serve target/wasm32-wasi/debug/hello.wasm
Serving HTTP on http://0.0.0.0:8080/
あとは好きな HTTP クライアントのコマンドで 8080 ポートにリクエストし、HTTP ステータスコードが 200 で、HTTP レスポンスボディに Hello
の応答があることを確認します。
$ xh http://0.0.0.0:8080/
HTTP/1.1 200 OK
Date: Wed, 06 Dec 2023 06:46:25 GMT
Transfer-Encoding: chunked
Hello