Wasm を利用したフレームワーク Spin について（後編）

## はじめに

[前編](https://qiita.com/takuhiro/items/d825c46942321a271679)ではSpinの概要とアプリケーションの作成・実行方法について紹介しました。
後編では個人的に気になった部分をもう少し深掘りしていきます。

**この記事では執筆時点の最新版であるspin v2.0.1を使用しています。**

## Wasm外部とのデータの受け渡し

Wasmモジュールはサンドボックスで実行されるためデフォルトで外部とのデータのやりとりができませんが、`spin.toml`で明示的に設定を追加することで、環境変数の取得、ファイルの読み書き、外部へのHTTPリクエスト等の機能を使用できます。
これらの機能を利用する際はWASIとSpin SDKを利用します。ビルドされたWasmモジュールはWASIやSpinが定義したAPIでSpinランタイムを介することで外部とのデータの受け渡しが可能になります。

例えば、ハンドラ内で環境変数を利用する場合は、以下のように`spin.toml`のコンポーネント設定で`environment`を指定します。
```spin.toml
[component.hello-http-go]
source = "main.wasm"
allowed_outbound_hosts = []
environment = { FOO = "bar" }
[component.hello-http-go.build]
command = "tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go"
```

ハンドラをGoで実装する場合、通常のプログラムと同様に`os.Getenv()`で取得できます。
```main.go
...
    foo := os.Getenv("FOO")
...
```

このコードが動作するのは、`tinygo build -target=wasi...`でWASI対応のwasmモジュールにコンパイルされるためです。
作成されたハンドラのwasmバイナリを解析すると`wasi_snapshot_preview1`の`environ_get`という関数をインポートして使用していることがわかります。ハンドラ側で`environ_get`を呼び出した時にSpinランタイムが`spin.toml`で設定された環境変数を返すというわけです。

```sh
$ wasm-tools print main.wasm | grep environ_get
  (import "wasi_snapshot_preview1" "environ_get" (func $__imported_wasi_snapshot_preview1_environ_get (;4;) (type 6)))
                                            call $__imported_wasi_snapshot_preview1_environ_get
```

次に外部へのHTTPリクエストの例をみてみます。（参考: [Sending Outbound HTTP Requests](https://developer.fermyon.com/spin/v2/go-components#sending-outbound-http-requests)）
デフォルトでは全ての外部への通信が拒否されるので、`spin.toml`で`allowed_outbound_hosts`を指定します。

```spin.toml
[component.hello-http-go]
source = "main.wasm"
allowed_outbound_hosts = ["random-data-api.fermyon.app"]
[component.hello-http-go.build]
command = "tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm main.go"
```

ハンドラのHTTPリクエストの実装は以下のようになります。
Go標準の`net/http`ではなく、Spin SDKが提供するhttpモジュールを使用します。

```main.go
import (
...
    spinhttp "github.com/fermyon/spin/sdk/go/v2/http"
)

spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) {
    resp, _ := spinhttp.Get("https://random-data-api.fermyon.app/animals/json")
...
```

ビルドしたwasmモジュールを解析すると`wasi-outbound-http`の`request`という関数をインポートして使用しているのがわかります。

```
$ wasm-tools print main.wasm | grep outbound_http
  (import "wasi-outbound-http" "request" (func $__wasm_import_wasi_outbound_http_request (;4;) (type 13)))
                        call $__wasm_import_wasi_outbound_http_request
```

これは名前にwasiと入っていて紛らわしいですが、WASI公式ではなくSpinで独自に定義している関数になります。
- [wasi-outbound-http.wit](https://github.com/fermyon/spin/blob/7b8af17676a9a5099e037c914e04dac6acd337ed/sdk/go/wit/wasi-outbound-http.wit)
- [http-types.wit](https://github.com/fermyon/spin/blob/7b8af17676a9a5099e037c914e04dac6acd337ed/sdk/go/wit/http-types.wit)

> // This is a temporary workaround very similar to https://github.com/deislabs/wasi-experimental-http.
// Once asynchronous functions, streams, and the upstream HTTP API are available, this should be removed.

とあるように、現在のWASIではネットワーク関連の機能が不足しているため、Spin側でワークアラウンドとして提供しているものです。
[こちらのロードマップ](https://bytecodealliance.org/articles/webassembly-the-updated-roadmap-for-developers)によると次のWASI preview 2でHTTP、preview 3で非同期・ストリームが実装されることになっています。これらのAPIを含むWASIがリリースされ、各言語のツールチェーンがそれをサポートするようになれば、このような独自APIは少なくなっていくと予想されます。

:memo: [こちら](https://developer.fermyon.com/spin/v2/rust-components#sending-outbound-http-requests)を参考にRustで実装すると以下のように`wasi:http/outgoing-handler`が使用されていました。言語ごとのSDKによって対応状況が違うようです。

```sh
$ wasm-tools print target/wasm32-wasi/release/send_outbound.wasm | grep outgoing-handler
  (import "wasi:http/outgoing-handler@0.2.0-rc-2023-10-18" "handle" (func $_ZN8spin_sdk3wit4wasi4http16outgoing_handler6handle10wit_import17h4bc6be1fb9219efdE (;43;) (type 10)))
```

## ハンドラの実装タイプ

Spinでは各プログラミング言語でハンドラ（関数）を実装しWebAssembly形式にコンパイルして実行しますが、ハンドラの実装方法には以下の2つのパターンがあります。

- [コンポーネントモデル](https://developer.fermyon.com/spin/v2/http-trigger#authoring-http-components)
- [WAGI（WebAssembly Gateway Interface）](https://developer.fermyon.com/spin/v2/http-trigger#http-with-wagi-webassembly-gateway-interface)

### コンポーネントモデル

Spinはハンドラの実装に[WebAssemblyコンポーネントモデル](https://component-model.bytecodealliance.org/)を利用しています。
以下のwitで定義されたインタフェースを利用して、トリガ -> ハンドラ -> outboundな各種アクセスの呼び出しを行っています。

- https://github.com/fermyon/spin/tree/main/wit

:memo: 前述の通り、Go SDKでは[こちらのwit](https://github.com/fermyon/spin/blob/7b8af17676a9a5099e037c914e04dac6acd337ed/sdk/go/wit)が使われているようです。

### WAGI

コンポーネントモデルをサポートしていないプログラミング言語では、CGIライクな[WAGI](https://github.com/deislabs/wagi)という仕組みでハンドラを実装しています。
Spinランタイムが受けたHTTPリクエストをWasm(WASI)モジュールの環境変数や標準入力として渡し、Wasm(WASI)モジュールの標準出力をHTTPレスポンスとして返す仕組みです。

## Spinアプリケーションのパッケージ管理

現状のSpinアプリケーションの配布方法にも触れておきます。
Spinアプリケーションはメタデータ等の各種設定とWebAssemblyバイナリ、画像などの静的アセットで構成されています。これらをパッケージング・配布するための仕組みとして以下の2つをサポートしています。

- Bindle
  - 関連オブジェクトを「コレクション」として管理するための"Aggregate Object Storage"
  - https://deislabs.io/posts/introducing-bindle/
- OCIレジストリ

BindleはFermyon Platform/Fermyon Cloudで現在利用されていますが、[こちらのプロポーザル](https://github.com/fermyon/spin/blob/main/docs/content/sips/008-using-oci-registries.md)によると、まだ実験的なプロジェクトで、マネージド・サービスがない、スケーリングで未解決の問題がある、といった運用上の課題があるため、汎用的なアーティファクトにも対応し既に各クラウドプロバイダでも普及しているOCIレジストリを使用する方法をサポートしたようです。
OCIレジストリに対応することで、Docker Hub、GitHub Container Registry(GHCR)といった既存のマネージドサービスを利用できるため、Spinアプリケーションの配布が容易になります。

`spin build`した後に`spin registry push`でGHCRにpushする例です。 (参考: [Spin Apps in Registries](https://developer.fermyon.com/spin/v2/registry-tutorial))
```sh
# イメージ作成
$ spin registry push ghcr.io/takuhiro/hello-http-rust:0.1.0
Pushing app to the Registry....                                                                                                                                                                                 Pushed with digest sha256:f5d5a2959582bf2d5b59a027c7bce4a34a62ac5c459305ae8cfab9c870bf7cd6

# イメージを指定して起動
$ spin up -f ghcr.io/takuhiro/hello-http-rust:0.1.0

Serving http://127.0.0.1:3000
Available Routes:
  hello-http-rust: http://127.0.0.1:3000 (wildcard)
```

イメージのマニフェストは以下のようになっていました。
```json
{
        "config": {
                "digest": "sha256:aabedf0b094944b344cecbffcfa0a9172e905a5d156bdf67c4c6cbb6d27d1d92",
                "mediaType": "application/vnd.oci.image.config.v1+json",
                "size": 78
        },
        "layers": [
                {
                        "digest": "sha256:6ef24b375b68caac830008302d83c787cce2c87afd86d98a7b622524bcd5298a",
                        "mediaType": "application/vnd.wasm.content.layer.v1+wasm",
                        "size": 2335413
                },
                {
                        "digest": "sha256:5f9ca81fda69965b4b2ee938e54bf7c230663d4327fe3806422f43f42a2d96c0",
                        "mediaType": "application/vnd.fermyon.spin.application.v1+config",
                        "size": 592
                }
        ],
        "schemaVersion": 2
}
```

Spinの設定とwasmモジュールが別々のレイヤで保存されているのがわかります。

:memo: Bytecode AllianceではWasmパッケージ用のレジストリプロトコル[warg](https://warg.io/)を策定中です。[このプロジェクトが成熟してきたらSpinでもサポート予定である](https://github.com/fermyon/spin/blob/main/docs/content/sips/008-using-oci-registries.md#q-how-does-this-effort-relate-to-the-bytecode-alliance-registry-project)ことが言及されています。

## まとめ

後編ではWASI周りなど、Spinの個人的に気になる部分を紹介しました。
WASIのpreview 2、preview 3、wargなどの登場でSpinも更に変化していくと思いますが、サーバサイドでのWasm活用事例として参考になる部分が多いので今後もキャッチアップしていきたいと思います。