SpinKubeでAKS上にWasm実行環境を構築

# はじめに
最近WebAssembly(Wasm)の話題をよく耳にします。  
JavaScript以外の言語でもブラウザで動くアプリが作れる技術くらいの認識だったのですが、軽量かつ環境非依存で使えるということで、最近ではサーバーサイドでも使うとか、何ならDockerから呼び出せるとか、随分適用範囲が広がっているそうです。  
そこで今回はWasmをk8s上で利用できるSpinKubeの環境構築について調査しました。  
SpinKubeを利用するにあたって、SpinやWasmコンテナというものを使いますので、そちらを先に紹介していきたいと思います。
# Spinとは
SpinKubeを使うためには、まずFermyon社が開発しているSpinというWasmフレームワークでアプリを実装する必要があります。  
WasmランタイムとしてWasmtimeやWasmEdgeといったものがありますが、Spinを使うとWasmtimeをランタイムとしてFaaS指向のアプリを作ることが可能となります。  
Spinの設定ファイルでルートとWasmバイナリを関連づけておき、そのルートへのリクエストをトリガーとしてWasmを実行します。  
この機能により複数のWasmバイナリを1つのアプリとして使用することが可能です。例えば以下の設定ではhello_spin.wasmとhello_spin2.wasmを別々のルートに割り当てています。
```toml: spin.toml
spin_manifest_version = 2

[application]
name = "hello-spin"
version = "0.1.0"

[[trigger.http]]
route = "/hello"
component = "hello-spin"

[component.hello-spin]
source = "target/wasm32-wasi/release/hello_spin.wasm"
allowed_outbound_hosts = []

[[trigger.http]]
route = "/hello2"
component = "hello-spin2"

[component.hello-spin2]
source = "target/wasm32-wasi/release/hello_spin2.wasm"
allowed_outbound_hosts = []
```
このトリガーと複数のWasmバイナリを紐づけてアプリを作るという所が、Spinと他のWasmランタイムとの大きな違いになります。  
一方でアプリ開発にSpin SDKを要求されるため、Spin以外では実行できず可搬性が下がる、という欠点があります。
```Rust:Spin用のコード(Rust)
// Spin SDKが必要
use spin_sdk::http::{IntoResponse, Request, Response};
use spin_sdk::http_component;

#[http_component]
fn handle_hello_spin(req: Request) -> anyhow::Result<impl IntoResponse> {
    println!("Handling request to {:?}", req.header("spin-full-url"));
    Ok(Response::builder()
        .status(200)
        .header("content-type", "text/plain")
        .body("Hello, my spinapp")
        .build())
}
```
``` shell-session:spinコマンドで実行(成功)
# サーバー起動
$ spin up
Logging component stdio to ".spin/logs/"

Serving http://127.0.0.1:3000
Available Routes:
  hello-spin: http://127.0.0.1:3000/hello
```

```shell-session:wasmedgeで実行(エラー)
$ wasmedge wasm32-wasi/release/hello_spin.wasm 
[2024-10-01 11:12:30.097] [error] instantiation failed: unknown import, Code: 0x302
[2024-10-01 11:12:30.097] [error]     When linking module: "wasi:io/streams@0.2.0" , function name: "[resource-drop]output-stream"
[2024-10-01 11:12:30.097] [error]     At AST node: import description
[2024-10-01 11:12:30.097] [error]     At AST node: import section
[2024-10-01 11:12:30.097] [error]     At AST node: module
```

その他、RedisやMQTTをトリガーとしたアプリを作成することも可能だそうです。

https://developer.fermyon.com/spin/v2/redis-trigger

https://github.com/spinkube/spin-trigger-mqtt

# Wasmコンテナ ～runwasiの利用～
Spinに限らずWasmアプリはWasmランタイムから実行する必要があります。  
そのためサーバー側にWasmアプリをデプロイする際は、以下のような方法が考えられます。  
1. コンテナ等で、WasmアプリとWasmランタイムをまとめ、一緒にデプロイする
1. 仮想マシン等でWasmランタイムが動いている環境にWasmアプリをデプロイする
1. WasmをホスティングしてくれるPaaSサービスへデプロイする

最初に候補となる1ですが、Wasmアプリをランタイムと一緒にコンテナとしてまとめても、大してWasmのメリットを受けられません。最初からWasmでないアプリをコンテナ化した方が楽ですし、おそらくパフォーマンスも良いと思います。  
かといって、WasmにはDockerのコンテナイメージのようにアプリ配信の仕組みがなく、2を実現するのも容易ではありません。  
3は既にいくつか利用可能なPaaSサービスがありますが、基本的にWasmアプリのみを実行できるものなので、実際に使えるかはユースケース次第となります。

ここで紹介したいのがcontainerdからWasmを実行できる[runwasi](https://www.publickey1.jp/blog/23/containerdwebassemblyrunwasiwebassembly.html)という機能です。  
これを使うと、コンテナの仕組みを利用しつつ、Wasmランタイムはインストール済のものを使うことができるようになります。1と2のあいのこのような方法ですね。
仕組みとしては、コンテナイメージの中にWasmアプリと各種設定ファイルだけを入れておきコンテナとして立ち上げると、containerdがshim(containerdがコンテナ管理に利用するサブプロセス)経由でWasmランタイム(Spinを含む)を起動してWasmアプリを実行してくれるものとなっています。  

![runwasi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3830694/47f1c3ef-7dbc-e4a8-5c47-be69cd839934.png)

runwasiを使うことにより、これまでに構築されたコンテナ向けのシステムをそのまま活用することができ、以下のようなことが実現できます。
- コンテナレジストリを用いたWasmコンテナイメージの配信
- Dockerやk8sからのWasm実行
- コンテナネットワークを通じ、非Wasmコンテナとの通信

要はコンテナ内にランタイムを入れずともWasmアプリをコンテナとして実行できるようになる、と認識してもらえば大体OKです。

なお、コンテナイメージにまとめる方式による[Wasmコンテナ配信の標準化が進んでいる](https://www.publickey1.jp/blog/24/webassemblywasm_oci_artifact_layout.html)らしく、今後Wasmアプリの配信はこのやり方が主流となりそうです。

Wasmコンテナはベースイメージもscratchで良いのでイメージサイズが数MB、コンテナレジストリで圧縮された状態では1MBを切ることも珍しくありません。
Wasmランタイムまで入れるコンテナと比較するとこんな感じになります。  

![wasmcontainer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3830694/69d8fb5e-784f-ac68-3562-a66376cf6c7d.png)

イメージサイズが小さくなるとコンテナレジストリの費用が少なくなる、データ転送が速くなる、データ転送料金も下がるといいことずくめです。

更にSpinでは、コマンド1つでこのWasmコンテナを作成しレジストリにPushすることも可能です。
```
$ spin registry --build push ttl.sh/myapp:24h
```

# SpinKubeとは
SpinKubeはk8s上に構築するSpinアプリの実行環境です。2024年にFermyon社からCNCFに寄贈されました。  
SpinKubeの中にはrunwasi向けのshimが含まれており、k8s上でSpinアプリを使えるようにしています。  
shimだけでは利用するための設定が煩雑になるため、より簡単に使えるようKubernetes Operatorやカスタムリソースも用意されています。  
このSpinKubeをインストールすれば、k8s上でSpinアプリが実行できるようになります。  

https://www.spinkube.dev

# SpinKube環境構築
では本題のSpinKubeの環境を構築していきます。  
AKS(Azure Kubernetes Service)上に作る方法が公式ドキュメントとして用意されていたので、それに沿って実施します。  

https://www.spinkube.dev/docs/install/azure-kubernetes-service/

## 前提条件
このドキュメントはローカルPC上に以下のことができていることを前提とします。  
- Azure CLIのインストール  
- kubectlコマンドのインストール  
- helmコマンドのインストール  
- Azure CLIを使って、管理者権限を持ったAzureサブスクリプションへログイン  

## AKS環境構築
AKSの環境を構築します。Azure CLIから作ります。  
```shell-session
$ az group create --name rg-spin-operator --location japaneast

$ az aks create --name aks-spin-operator --resource-group rg-spin-operator \
    --location japaneast \
    --node-count 1 \
    --tier free \
    --generate-ssh-keys
```

kubectl, helmからAKSにアクセスできるよう認証情報も取得しておきます。  
```shell-session
$ az aks get-credentials --name aks-spin-operator --resource-group rg-spin-operator
```

## AKS上にSpinKubeを設定
### カスタムリソース定義を設定
SpinKubeで使うカスタムリソース定義(CRD)を設定します。  
```shell-session
$ kubectl apply -f https://github.com/spinkube/spin-operator/releases/download/v0.3.0/spin-operator.crds.yaml
```
これでSpinApp, SpinAppExecutorといったCRDが作成されます。  
Spinアプリを実行する時はここで定義されたSpinAppリソースを作成します。  

### RutimeClassを設定
RuntimeClassとしてwasmtime-spin-v2を定義します。  
```shell-session
$ kubectl apply -f https://github.com/spinkube/spin-operator/releases/download/v0.3.0/spin-operator.runtime-class.yaml
```

### cert-managerをインストール
cert-managerでTLSを扱えるようにします。  
```shell-session
# Install cert-manager CRDs
$ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.3/cert-manager.crds.yaml

# Add and update Jetstack repository
$ helm repo add jetstack https://charts.jetstack.io
$ helm repo update

# Install the cert-manager Helm chart
$ helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --version v1.14.3
```

### KWasmをインストール
KWasmはコンテナ上に作成したWasm実行環境を使って、Wasmコンテナを利用できるようにするKubernetes Operatorです。
SpinKube用のnode-installerを指定してインストールします。
```shell-session

# Add Helm repository if not already done
$ helm repo add kwasm http://kwasm.sh/kwasm-operator/
$ helm repo update

# Install KWasm operator
$ helm install \
  kwasm-operator kwasm/kwasm-operator \
  --namespace kwasm \
  --create-namespace \
  --set kwasmOperator.installerImage=ghcr.io/spinkube/containerd-shim-spin/node-installer:v0.15.1

# Provision Nodes
$ kubectl annotate node --all kwasm.sh/kwasm-node=true
```
こうすることで、各ノードでSpin用のshimが使えるように設定してくれます。  
直接ノードにWasmランタイムやshimをインストールせずとも、仮想的にrunwasiを使えるようにしている、と理解しています。

### Spin Operatorインストール
Spin OperatorはSpin用のOperatorです。  
最初に定義したカスタムリソースを管理・運用してくれます。  
```shell-session
$ helm install spin-operator \
  --namespace spin-operator \
  --create-namespace \
  --version 0.3.0 \
  --wait \
  oci://ghcr.io/spinkube/charts/spin-operator
```

### Shim Executorの作成
Shim ExecutorとしてSpinAppExecutorのリソースを作成します。  
```shell
$ kubectl apply -f https://github.com/spinkube/spin-operator/releases/download/v0.3.0/spin-operator.shim-executor.yaml
```
このShim Executorが何なのか正直なところ良くわからないのですが、マニフェストファイルの中身を見た限り、おそらくSpinアプリをどのshimで動作させるかや、SpinアプリのDeploymentを作成するかなど、Spin Operatorの動作を定義しているように見えます。  

ここまででSpinKubeの設定は終わりとなります。  

## SpinKubeでSpinアプリを実行
### ネット上のサンプルを実行
ネット上にサンプルのマニフェストファイルが公開されているのでそれをデプロイします。  
``` shell-session
$ kubectl apply -f https://raw.githubusercontent.com/spinkube/spin-operator/main/config/samples/simple.yaml
```
実行するとSpinアプリが立ち上がりますので、ポートフォワードしてcurlでアクセスすると応答が返ってきます。  
```shell_session
$ kubectl port-forward services/simple-spinapp 8080:80 &

$ curl http://localhost:8080/hello
Hello world from Spin!
```
k8s上の動作としてはSpinAppのリソースが作成され、それに合わせて同名のDeploymentリソースとClusterIP Serviceリソースが自動的に作成されています。
```shell_session
$ kubectl get spinapp
NAME             READY   DESIRED   EXECUTOR
simple-spinapp   1       1         containerd-shim-spin

$ kubectl get deploy
NAME             READY   UP-TO-DATE   AVAILABLE   AGE
simple-spinapp   1/1     1            1           3h48m

$ kubectl get pod
NAME                              READY   STATUS    RESTARTS   AGE
simple-spinapp-84f5c498db-5qljh   1/1     Running   0          3h48m

$ kubectl get svc
NAME             TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
kubernetes       ClusterIP   10.0.0.1       <none>        443/TCP   4h43m
simple-spinapp   ClusterIP   10.0.179.122   <none>        80/TCP    3h48m
```

### アプリを外部に公開
今回はAKS上に構築しているので、LoadBalancer Serviceを作成して外部に公開してみます。  
Deploymentができているので、これをターゲットにLoadBalancerのリソースを作成します。
```shell_session
$ kubectl expose deployment simple-spinapp --type=LoadBalancer --name=simple-spinapp-lb --port=80 --target-port=80
service/simple-spinapp-lb exposed

$ kubectl get svc
NAME                TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)        AGE
kubernetes          ClusterIP      10.0.0.1       <none>         443/TCP        5h17m
simple-spinapp      ClusterIP      10.0.179.122   <none>         80/TCP         4h22m
simple-spinapp-lb   LoadBalancer   10.0.103.217   4.182.129.12   80:31383/TCP   86s
```
LoadBalancerサービスを作成すると、Azureが自動的にグローバルIPアドレスを割り当ててくれます。  
これでWasmアプリに外部から接続できるようになります。
ブラウザから接続してみると、確かにアプリから応答があります。

![browser.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3830694/4ab55c37-83f4-3039-de67-9d5c0df7177f.png)


今回は検証していませんが、Ingressを設定している環境ではIngressをClusterIPサービスにつなぐことも可能だと思います。

### 自作アプリをSpinKube上で実行
Spin用のrustアプリを作ります。以下のコマンドでSpinアプリを初期化します。
```shell-session
$ spin new myapp
Pick a template to start your application with: [Page 1/2] 
  http-c (HTTP request handler using C and the Zig toolchain)
  http-empty (HTTP application with no components)
  http-go (HTTP request handler using (Tiny)Go)
  http-grain (HTTP request handler using Grain)
  http-php (HTTP request handler using PHP)
  http-py (HTTP request handler using Python)
> http-rust (HTTP request handler using Rust)
  http-swift (HTTP request handler using SwiftWasm)
  http-ts (HTTP request handler using Typescript)
  http-zig (HTTP request handler using Zig)
  redirect (Redirects a HTTP route)
  redis-go (Redis message handler using (Tiny)Go)
```
myappディレクトリに移動し、src/lib.rsを開いてメッセージを書き換えます。
Fermyonとなっていたところをmy spinappに書き換えています。

```rust:src/lib.rs
use spin_sdk::http::{IntoResponse, Request, Response};
use spin_sdk::http_component;

/// A simple Spin HTTP component.
#[http_component]
fn handle_hello_spin(req: Request) -> anyhow::Result<impl IntoResponse> {
    println!("Handling request to {:?}", req.header("spin-full-url"));
    Ok(Response::builder()
        .status(200)
        .header("content-type", "text/plain")
        //.body("Hello, Fermyon")
        .body("Hello, my spinapp")
        .build())
}
```
このアプリをコンテナレジストリにpushします。コンテナレジストリはttl.shを使用します。  
レジストリへのpushはspinコマンドからできるようになっています。

```shell_session
$ spin registry push --build ttl.sh/hello-spin:24h
Building component hello-spin with `cargo build --target wasm32-wasi --release`
    Finished `release` profile [optimized] target(s) in 0.13s
Finished building all Spin components
Pushing app to the Registry...
Pushed with digest sha256:
```
完了したら以下のようなマニフェストファイルを作成しkubectlからapplyします。
```yaml: myspinapp.yaml
apiVersion: core.spinoperator.dev/v1alpha1
kind: SpinApp
metadata:
  name: my-spinapp
spec:
  image: "ttl.sh/hello-spin:24h"
  replicas: 1
  executor: containerd-shim-spin
```
```shell-session
$ kubectl apply -f myspinapp.yaml 
spinapp.core.spinoperator.dev/my-spinapp created
```
これで自作アプリをデプロイできました。
ポートフォワードを設定してアプリが実行できることを確認できます。
```
$ kubectl port-forward services/my-spinapp 8080:80 &
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80
$  curl http://localhost:8080/
Hello, my spinapp
```

## 利用上の注意
今回KWasmというKubernetes Operatorを利用しました。このKWasmですが実は開発・評価用であり、本番用に使うことが推奨されていません。

https://kwasm.sh/

> This project is meant to be used for development or evaluation purpose. Your nodes may get damaged!

必然的にKWasmを利用する今回の環境構築方法は、評価・開発用として使うべきものになります。
ただ、SpinKubeのサイトを見ると、KWasmはruntime class managerというものに置き換わるそうです。

https://www.spinkube.dev/docs/overview/

> SpinKube is a new open source project that streamlines the experience of developing, deploying, and operating Wasm workloads on Kubernetes, using Spin in tandem with the runwasi and <b>runtime class manager (formerly KWasm)</b> open source projects.

といいつつruntime class managerはまだリリース版がでていないようで、まだしばらくKWasmを使う必要がありそうですが。

https://github.com/spinkube/runtime-class-manager

いずれにせよ、今回参考にしたドキュメントは将来的にruntime class managerを使ったものに改訂される可能性が高そうです。  
本番環境で利用するのはそれ以降にしたほうがよさそうです。

# おわりに
k8s上のWasm実行環境であるSpinKubeをAKSに設定してみました。思ったよりトラブルなく環境構築できたことは驚きでした。  
k8s上でWasmコンテナが動かせるようになると既存の資産との連携も取りやすく、活用の幅が広がるのではないかと思います。  
今後はWasmを使ってもう少し複雑なことに挑戦してみようと思います。  
ここまで読んで頂きありがとうございました。