Krustletを使ってKubernetesでWebAssemblyアプリケーションを実行する

![krustlet.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705474/e1fb3f73-59a2-26c6-0243-de017c183105.jpeg)


# はじめに

[NRI OpenStandia Advent Calendar 2021](https://qiita.com/advent-calendar/2021/nri-openstandia) 9日目は[Krustlet](https://krustlet.dev/)を取り上げます。
Krustletは[Advent Calendar 8日目で紹介したWebAsembly](https://qiita.com/t_katsumura/items/ff379aaaba6931aad1c4)のアプリケーションをKubernetes上で実行できるようにしたOSSです。

Krustletが登場したことにより、バックエンドのシステムでもコンテナではない別の選択肢(WebAssembly)が増えました。
もちろんKrustletはコンテナをWebAssemblyで置き換えることを目指したものではないため、適材適所で使用する技術の選択は必要になります。

本記事ではKrustletを用いてWebAssemblyのサンプルアプリの動かし、どのような仕組みでWebAssemblyが実行されるかを調べてみました。

# 1. Krustletの概要

## 1.1 Krustletとは

Rustで実装されたKubeletであることから、Krustletと名づけられました。Krustletには[Kubelet API](https://github.com/cyberark/kubeletctl/blob/master/API_TABLE.md)が実装されています。
KrustletはコンテナとWebAssemblyを同じKubernetesクラスタ内で実行することを目指してMicrosoftの[Deis Labs](https://deislabs.io/)により開発が進められてきました。2020年4月にKrustlet [v0.1.0](https://github.com/krustlet/krustlet/releases/tag/v0.1.0)のリリースが[発表され](https://deislabs.io/posts/introducing-krustlet/)、2021年12月時点では[v1.0.0-alpha.1](https://github.com/krustlet/krustlet/releases/tag/v1.0.0-alpha.1)となっています。また、2021年7月には[CNCFのSandboxプロジェクト](https://deislabs.io/posts/towards-krustlet-v1-cncf/)へ加わりました。

![krustlet card.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705474/2413ac9d-dca5-0936-8aee-025a1e50f07a.png)

※ [CNCF Landscape](https://landscape.cncf.io/card-mode?selected=krustlet)より引用

Krustletのゴールとしては以下の2点が掲げられています。

1. WebAssemblyワークロードのKubernetesへのデプロイを簡単にする
2. KubernetesコンポーネントがGoではない別の言語(KrustletではRust)で実装できることをコミュニティに示す。


## 1.2 Krustletの全体像

Krustletを利用する際の全体像を示します。
KrustletはKubelet APIを実装しており、Kubernetesのワーカーノードのように機能させることができます。ただし、Kubeletではコンテナベースのランタイムを用いてコンテナを実行しますが、Krustletでは[WASI(WebAssembly System Interface)](https://github.com/WebAssembly/WASI)ベースのランタイムを用いてWebAssemblyを実行します。
なお、Krustletでは[wasmtime](https://wasmtime.dev/)をベースとしたランタイムが利用されているようです。

KrustletではWebAssemblyワークロードをコンテナレジストリから取得します。それでは、どうやってWebAssemblyをコンテナレジストリへPushするのでしょうか。答えは「コンテナの仕様に則ってWebAssemblyをラップする」です。
WebAssemblyをコンテナ形式にラップするには[wasm-to-oci](https://github.com/engineerd/wasm-to-oci)などのツールを使用します。OCIとは[Open Container Initiative](https://opencontainers.org/)の略語であり、主にコンテナのランタイム仕様(runtime-spec)とイメージ仕様(image-spec)を規定しています。コンテナ形式にラップされたWebAssemblyの仕様は[Wasm Image specifications](https://github.com/solo-io/wasm/tree/master/spec)として規定されています。

![krustlet_cluster.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705474/ca5fa9e4-a3af-ce63-3ff8-c15a1c10f61e.jpeg)


# 2. Krustletの環境を構築する

## 2.1 環境

本記事ではKrustletを用いてKubernetes上でWebAssemblyのサンプルアプリを動かします。
動作確認は以下の環境で行いました。

| # | ソフトウェアスタック | 備考 |
|:-:|:-|:-|
| 1 | Windows 10 Pro 1909 | ホストマシンのOS |
| 2 | Ubuntu 20.04 LTS（WSL2） | Docker Desktopで利用 |
| 3 | Docker Desktop for Windows 3.2.2(61853) |   |
| 4 | Docker Engine 20.10.5 |   |
| 5 | Kubernetes v1.19.7 | Docker Desktopでインストールされるもの |
| 6 | [Krustlet v1.0.0-alpha.1](https://github.com/krustlet/krustlet/releases/tag/v1.0.0-alpha.1) |   |

Windows環境でKrustletを動かす場合、DockerはWSL2ベースで動いている必要があります。
これはDocker Desktopの設定画面から確認できます。

![Docker確認事項](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705474/86a6a6bc-217b-72c3-3e66-a8dd2f8f9bab.png)


## 2.2 Krustletのインストール

Krustletのインストールは非常に簡単です。
[公式サイト](https://docs.krustlet.dev/intro/install/)にも記載がありますが、githubからKrustletの実行ファイルをダウンロードして解凍するだけです。

WSL2のプロンプトを開き、下記のコマンドでKrustletの実行ファイルを取得します。
以降の手順では、ダウンロードした実行ファイル`krustlet-wasi`があるディレクトリで作業を行うことを想定し、パスは通しません。

```:作業ディレクトリの作成とKrustletのインストール
[~]$ mkdir krustlet
[~]$ cd krustlet
[~/krustlet]$ curl -OL https://krustlet.blob.core.windows.net/releases/krustlet-v1.0.0-alpha.1-linux-amd64.tar.gz
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 14.7M  100 14.7M    0     0   537k      0  0:00:28  0:00:28 --:--:--  565k
[~/krustlet]$
[~/krustlet]$ tar -zxvf krustlet-v1.0.0-alpha.1-linux-amd64.tar.gz
README.md
LICENSE
krustlet-wasi
```

## 2.3 KuberentesクラスタにKrustletノードを追加

KrustletノードをKubernetesクラスタへ追加します。
[公式サイト](https://docs.krustlet.dev/howto/krustlet-on-wsl2/)の手順を参考に進めます。

はじめにWSL2のIPアドレスを確認しておきます。
下記の通り、`ifconfig`コマンドで確認するとWSL2のIPアドレスが`172.17.117.108`であることが分かります。

```:WSL2のIPアドレスを確認
[~/krustlet]$ ifconfig eth0
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.117.108  netmask 255.255.255.240  broadcast 172.17.117.111
        inet6 fe80::215:5dff:fe99:97b5  prefixlen 64  scopeid 0x20<link>
        ether 00:15:5d:99:97:b5  txqueuelen 1000  (Ethernet)
        RX packets 6073  bytes 15963285 (15.9 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 1985  bytes 132197 (132.1 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
```

次にKubernetesクラスタにKrustletのノードを追加します。
ノードを追加するにはブートストラップ用のトークンを用意する必要があります。
[bootstrapping](https://docs.krustlet.dev/howto/bootstrapping/)の手順を参考に進めます。

実行するコマンドは下記の通りです。
`bootstrap.sh`という便利なシェルが提供されているので、こちらを実行しています。

```:ブートストラップ用のトークン作成
[~/krustlet]$ curl -OL https://raw.githubusercontent.com/krustlet/krustlet/main/scripts/bootstrap.sh
[~/krustlet]$ chmod 777 ./bootstrap.sh
[~/krustlet]$ ./bootstrap.sh
secret/bootstrap-token-qd115j created
Switched to context "docker-desktop".
Context "docker-desktop" renamed to "tls-bootstrap-token-user@kubernetes".
User "tls-bootstrap-token-user" set.
Context "tls-bootstrap-token-user@kubernetes" modified.
Context "tls-bootstrap-token-user@kubernetes" modified.
```

続いてKubernetesクラスタへKrustletのノード追加リクエストを送ります。
この時に指定できるオプションは[Krustlet Configuration](https://docs.krustlet.dev/topics/configuration/)に記載があります。ここでは次の3項目のみ指定します。

1. `--node-ip`：先ほど取得したWSL2のIPアドレスを指定します
1. `--node-name`：ノード名になるので、任意の名前を指定します
1. `--hostname`：任意の名前を付けますが、のちに利用する証明書の名前が`<ホスト名>-tls`のフォーマットになるので、分かりやすい名前を指定します

```
[~/krustlet]$ ./krustlet-wasi --node-ip 172.17.117.108 --node-name krustlet --hostname krustlet-host
BOOTSTRAP: TLS certificate requires manual approval. Run kubectl certificate approve krustlet-host-tls
```

なお、ノード追加のリクエストを送るコマンド打った後プロンプトは戻ってきません。

この段階ではKrustletからKubernetesクラスタに対してノード追加のリクエストが来ている状態です。
下記のコマンドによりリクエストが来ていることを確認します。このコマンドはWindowsとWSL2のどちらで実行しても構いません。
また、リクエスト名は`<ホスト名>-tls`のフォーマットになります。

```:リクエストの確認
D:\wasm-work>kubectl get CertificateSigningRequest krustlet-host-tls
NAME                AGE     SIGNERNAME                      REQUESTOR            CONDITION
krustlet-host-tls   2m56s   kubernetes.io/kubelet-serving   docker-for-desktop   Pending
```

確かに`krustlet-host-tls`という名前でノード追加のリクエストが来ており、CONDITIONがPendingになっていることが確認できます。
ノード追加のリクエストはKubernetesクラスタ側で手動承認します。
承認コマンドは以下の通りです。承認コマンドはWindowsとWSL2のどちらで実行しても構いません。

```:Krustletノード追加リクエストの承認
D:\wasm-work>kubectl certificate approve krustlet-host-tls
certificatesigningrequest.certificates.k8s.io/krustlet-host-tls approved
```

承認コマンドを実行した際、ノード追加のリクエストを送った側のプロンプトには`received TLS certificate approval: continuing`というメッセージが表示されます。

```:ノードの追加リクエストが承認されたメッセージが表示される
[~/krustlet]$ ./krustlet-wasi --node-ip 172.17.117.108 --node-name krustlet --hostname krustlet-host
BOOTSTRAP: TLS certificate requires manual approval. Run kubectl certificate approve krustlet-host-tls
BOOTSTRAP: received TLS certificate approval: continuing
```

リクエストを承認したことは`CertificateSigningRequest`の状態で確認可能です。
リクエストを削除する際には以下のコマンドを打ちます。
`kubectl delete CertificateSigningRequest krustlet-host-tls`

```:リクエスト一覧を取得
[~]$ kubectl get CertificateSigningRequest
NAME                AGE   SIGNERNAME                      REQUESTOR            CONDITION
krustlet-host-tls   11m   kubernetes.io/kubelet-serving   docker-for-desktop   Approved,Issued
```

最後にKrustletのノードがKubernetesクラスタに追加されていることを確認します。
ノード追加時に`--node-name krustlet`のオプションを指定していたため、`krustlet`という名前でノードが追加されています。また、VERSIONにはKrustletのバージョンである`1.0.0-alpha.1`が表示されています。
CONTAINER-RUNTIMEには`mvp`と表示がありますが、これは[Minimum Viable Product](https://github.com/WebAssembly/design/blob/main/MVP.md)の略であり、まだKrustletに最小限の機能しか実装されていないことを意味しています。

```:Krustletのノードが追加されていることを確認
D:\wasm-work>kubectl get nodes -o wide
NAME             STATUS   ROLES    AGE     VERSION         INTERNAL-IP      EXTERNAL-IP   OS-IMAGE         KERNEL-VERSION                   CONTAINER-RUNTIME
docker-desktop   Ready    master   254d    v1.19.7         192.168.65.4     <none>        Docker Desktop   5.4.72-microsoft-standard-WSL2   docker://20.10.5
krustlet         Ready    <none>   3m38s   1.0.0-alpha.1   172.17.117.108   <none>        <unknown>        <unknown>                        mvp
```

ノードを削除する際には以下のコマンドを実行します。

```:ノードの削除コマンド
D:\wasm-work>kubectl delete node krustlet
```

# 3. Krustletでサンプルアプリを実行する

## 3.1 WASI-SDKのインストール

今回はC言語で記述したプログラムをWebAssemblyにコンパイルします。
はじめにWindowsに[WASI-SDK](https://github.com/WebAssembly/wasi-sdk/releases/tag/wasi-sdk-14)をインストールします。
インストールといっても実行ファイル（wasi-sdk-14.0-mingw.tar.gz）をダウンロードして適当なディレクトリに展開するだけです。また、実行ファイルのパス(wasi-sdk-14.0/bin)を通しておきます。

## 3.2 C言語のサンプルプログラムをWebAssemblyで動かす

まずはKrustletを使わずにサンプルプログラムが動作することを確認します。
適当な作業ディレクトリを作成してサンプルプログラムを記述します。

```c:サンプルプログラム(hello.c)
#include <stdio.h>
#include <unistd.h>

int main()
{
    for(int i=0;i<5;i++){
        printf("Hello, WebAssembly!\n");
    }
    return 0;
}
```

上記をコンパイルするために、WASI-SDKの`clang`を使用します。
サンプルプログラムのあるディレクトリに移動し、`clang`コマンドによりWebAssembly形式`.wasm`へコンパイルします。

```:サンプルプログラムのコンパイル
D:\wasm-work>clang hello.c -o hello.wasm

D:\wasm-work>dir
 ~略~
2021/12/09  06:37               150 hello.c
2021/12/09  06:39            19,218 hello.wasm
```

コンパイルされたhello.wasmを実際に動作させてみます。
[wasmtime](https://wasmtime.dev/)を利用するとコマンドラインからWebAssemblyを実行できます。
以下は実行例です。(※wasmtimeをダウンロードしてパスを通しています。)

```:サンプルプログラムの実行例
D:\wasm-work>wasmtime hello.wasm
Hello, WebAssembly!
Hello, WebAssembly!
Hello, WebAssembly!
Hello, WebAssembly!
Hello, WebAssembly!
```

## 3.3 WebAssemblyをコンテナ化する

WebAssemblyをKrustletで動かすには、WebAssemblyのアプリケーションをコンテナレジストリにPushする必要があります。
[What is a registry, and what is wasm-to-oci?](https://docs.krustlet.dev/intro/tutorial02/)に記載されているようにWebAssemblyをコンテナレジストリへPushするには[wasm-to-oci](https://github.com/engineerd/wasm-to-oci)を利用します。
wasm-to-ociの[実行ファイル](https://github.com/engineerd/wasm-to-oci/releases/tag/v0.1.2)はgithubから入手できます。

DockerHubではWASM OCIイメージが拒否されるようなので、今回はGithubのコンテナレジストリである`ghcr.io`へイメージをPushします。
なんでPushできないやと少し悩みましたが、これについてはwasm-to-ociが明記していました。

> Note that trying to push a WebAssembly module to Docker Hub is not supported at the time of writing this document, as Docker Hub does not accept unknown artifact types.
> https://github.com/engineerd/wasm-to-oci

以下は実行例です。(※wasm-to-ociをダウンロードしてパスを通しています。)

```:WASMをコンテナレジストリへPush
D:\wasm-work>windows-amd64-wasm-to-oci push hello.wasm ghcr.io/t-katsumura/hello-wasm:1.0
time="2021-12-09T06:46:48+09:00" level=info msg="Pushed: ghcr.io/t-katsumura/hello-wasm:1.0"
time="2021-12-09T06:46:48+09:00" level=info msg="Size: 19218"
time="2021-12-09T06:46:48+09:00" level=info msg="Digest: sha256:6cbdece2bba45fddb8f86d4b9dd8c00ff8d4c181aa74ad9ec79a26dd2a07fad8"
```

無事、WebAssemblyをコンテナレジストリへPushすることができました。

# 4. KrustletでWebAssemblyのサンプルアプリを動かす

## 4.1 Podの作成

先ほどコンテナレジストリへPushしたサンプルアプリをKubernetes上で動かすため、マニフェストを記載したyamlファイルを作成します。
マニフェストの内容は下記の通りであり、Podとしてサンプルプログラムを動かします。

```yaml:pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: hello-wasm
spec:
  nodeSelector:
    kubernetes.io/arch: wasm32-wasi
  containers:
  - name: hello-wasm
    image: ghcr.io/t-katsumura/hello-wasm:1.0
    imagePullPolicy: Always
  tolerations:
  - effect: NoExecute
    key: kubernetes.io/arch
    operator: Equal
    value: wasm32-wasi
  - effect: NoSchedule
    key: kubernetes.io/arch
    operator: Equal
    value: wasm32-wasi
```

上記のマニフェストにおいて、`nodeSelector`および`tolerations`を指定することによりWebAssemblyがKrustletのノード上で実行されるように制限しています。

マニフェストを適用するとPodが作成され、WebAssemblyのサンプルプログラムが実行されます。
`kubectl get pod`コマンドでSTATUSを確認すると`ExitCode:0`と表示されていますが、これはサンプルプログラムが正常終了したことを表しています。

```:マニフェストの適用
D:\wasm-work>kubectl apply -f pod.yaml
pod/hello-wasm created

D:\wasm-work>kubectl get pod
NAME         READY   STATUS       RESTARTS   AGE
hello-wasm   0/1     ExitCode:0   0          8s
```

`kubectl logs`コマンドを実行すると「Hello, WebAssembly!」と表示されるはずですが、なぜかエラーが出てしまいました。
これについては追加課題ということで、本記事はここまでとします。

```
D:\wasm-work>kubectl logs hello-wasm
Error from server: Get "https://172.17.117.108:3000/containerLogs/default/hello-wasm/hello-wasm": dial tcp 172.17.117.108:3000: connect: no route to host
```

# まとめ

Krustletを用いるとWebAssemblyをPodとして実行できるできることが分かりました。
また、OCIの仕様に則ってラップすることでWebAssemblyをコンテナのように扱っていることも分かりました。

サンプルアプリの実行を通して単一のアプリケーションの実行方法は理解できましたが、他のアプリケーションをやコンテナとどのように連携させるのかなどはより深堀して検証する必要がありそうです。
マイクロサービスアーキテクチャとの相性も気になるところです。

開発のしやすさ、デバッグのしやすさ、トレーサビリティなど、実システムへ適用する際に無視できない観点を犠牲にしていると使えない技術になってしまうので、そのような点も含めて技術検証が必要だと感じます。

# 参考資料

- https://docs.krustlet.dev/intro/
- https://deislabs.io/posts/introducing-krustlet/