🎁

Docker+Wasm Technical PreviewからWASMに入門する

2022/11/26に公開

少し前ですが、Docker の Technical preview として WASM (WebAssembly) のサポートが発表されました。
https://www.docker.com/blog/docker-wasm-technical-preview/

普段から Docker を利用していますが、そもそも WASM が何なのか? WASM をサポートしたことによって何ができるようになるのか? がわかっていなかったので、一通り調べてみました。

WASMとは

まずは、WASM 自体の理解を深めるために、下記のドキュメントにざっと目を通します。

WASM とはバイナリコードの仕様で、そのフォーマットに対応したバイナリを実行する Stack-based な仮想マシンのことを指します。

開発初期は、ブラウザ(JS)の高速化が目的で、それを念頭に置いたような記載もドキュメントにあります。
https://developer.mozilla.org/en-US/docs/WebAssembly/Concepts#what_is_webassembly

WebAssembly is a new type of code that can be run in modern web browsers and provides new features and major gains in performance.

ブラウザで JS のコードが実行される中で、3D ゲームや VR など低遅延で高いパフォーマンスが必要とされるようなユースケースを想定していると言えるでしょう。

... however, when trying to use JavaScript for more intensive use cases like 3D games, Virtual and Augmented Reality, computer vision, image/video editing, and a number of other domains that demand native performance (see WebAssembly use cases for more ideas).

MDN's WebAssembly pagesには、Web ブラウザ上で動作するという記載がありますが、これは上記の背景を踏まえた記述です。

WASM の特徴としては、下記が挙げられます。
https://github.com/WebAssembly/design#overview

  • Efficient and fast: WASM のコードは「near-native speed」で動く
  • Portability: Web の標準のようなバージョンレス、機能テスト済み、後方互換性のある状態で設計されており、バイナリであることも踏まえて様々な環境で実行可能
  • Secure: WASM のバイナリはホストから分離された環境で実行される

WASM のドキュメントで挙げられているユースケースを見てみると、Games などの Inside the browser の項目もある一方で、Outside the browserの項目もあります。
https://webassembly.org/docs/use-cases/

WASIとは

non-browser (outside the browser) な環境、具体的には OS 上で WASM を動かすために利用されるのが WASI (WebAssenbly Interface)です。
https://github.com/WebAssembly/WASI

そもそも、WASM 自体は前述した Portability などの VM として優秀な特徴も持ち合わせつつ、C/C++, Rust, AssemblyScript など様々な言語からWASMにコンパイル可能でしたが、ブラウザでの利用 = JS への組み込みが前提でした。

一方で、WASM の VM としての優秀な仕様を踏まえて、ブラウザの外 (WASI のケースでは OS) で利用するために WASI (WebAssenbly Interface) が出てきました。

そのため、WASI の High Level Goals には、Browser に関する記載がひとつもありません。

Define a set of portable, modular, runtime-independent, and WebAssembly-native APIs which can be used by WebAssembly code to interact with the outside world.

具体的には、WASI は WASM - Host OS 間のインターフェース = system call の標準を定義していて、これに対応することで WASM が普通のプロセスのように動かせるようになります。

https://github.com/bytecodealliance/wasmtime/blob/main/docs/WASI-overview.md#portable-system-interface-for-webassembly

WASI is starting with a basic POSIX-like set of syscall functions, though adapted to suit the needs of WebAssembly, such as in excluding functions such as fork and exec which aren't easily implementable in some of the places people want to run WebAssembly, and such as in adopting a capabilities-oriented design.

WASI に関する詳細な内容は下記の記事や動画が参考になりそうです。
https://hacks.mozilla.org/2019/03/standardizing-wasi-a-webassembly-system-interface/

https://www.youtube.com/watch?v=ggtEJC0Jv8A

DockerのWASMサポートによってできること

WASM/WASI について概要を理解したところで、Docker の Technical preview の記事に戻ります。
記事の内容を踏まえると、下記のようなことができるようになります。

  • 1: WASM のバイナリを docker 上で実行する
    • つまり、Rust, C/C++ から WASM のバイナリにコンパイルしたアプリケーションコードが docker container 上で実行できる
  • 2: WASM のイメージを Docker image 同様に扱う (image の push / pull)

この記事では 1 についてのみ説明をします。

2 に関してはこの記事では特に説明をしませんが、下記の記事が参考になります。
https://nigelpoulton.com/getting-started-with-docker-and-wasm/

WASM のバイナリを docker 上で実行するということですが、内部的にはどのように実現してるのでしょうか?

Docker engineのcontainerd対応

intergration に関する説明の部分では、下記のような画像が利用されていました。

wasm

通常の構成が画像の左側に記載されています。
ざっくりと説明すると、high-level runtime である containerd と、low-level runtime である runc の仲介役としての containerd-shim がいます。
runc は low-level runtime で、Linux kernel とやりとりして実際にリソースの隔離 = コンテナの作成を実現しています。

コンテナを利用した開発ツールである docker ですが、コンテナイメージやコンテナの実行状態の管理などを行う containerd を利用できるようになっています。
https://www.docker.com/blog/extending-docker-integration-with-containerd/

WASM 対応の Technical preview では、containerd image storeの有効化、つまり containerd の利用が必須になっています。

Important note #2: This preview has the containerd image store enabled and cannot be disabled. If you’re not currently using the containerd image store, then pre-existing images and containers will be inaccessible.

containerd に関するより詳細な説明は下記の記事などにお任せするとして、WASM 対応の構成の説明に戻ります。

https://medium.com/nttlabs/docker-to-containerd-4f3a56e6f2b6
https://medium.com/nttlabs/containerd-shim-in-rust-77d0047ad06e

WASMサポートがどのようにして実現されているのか

docker で WASM のバイナリを動かす際には、内部的には下記のようになっています。

wasm

サンプルとして記載されていた docker コマンドを見ると、下記のようなオプションが付与されています。

  • --runtime=io.containerd.wasmedge.v1 : containerd-wasm-shim を使用することを Docker エンジンに通知する
  • --platform=wasi/wasm32 : 使用するイメージのアーキテクチャを指定する
docker run -dp 8080:8080 \
    --name=wasm-example \
    --runtime=io.containerd.wasmedge.v1 \
    --platform=wasi/wasm32 \
    michaelirwin244/wasm-example

実際に試してみる

せっかくなので、Technical preview の docker engine で遊んでみましょう。

下記を参考に、WASM 対応した Docker engine を EC2 上で Setup して、WASM の Application として build されている image を docker container として動かしてみます。
https://github.com/chris-crone/wasm-day-na-22/tree/main/server

$ docker -v
Docker version 20.10.21, build baeda1f
$ docker compose version
Docker Compose version v2.12.2

一応、検証の際に利用した EC2 を立てるための Terraform のコードも置いておきます。
https://github.com/taxintt/docker-on-ec2-for-testing-wasm-integration

Technical preview の docker のインストールまでは、EC2 立てた段階で終わっているので WasmEdge と containerd-shim-wasmedge (WASM 対応の containerd-shim)のインストールを行います。

$ curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh | sudo bash -s -- -e all -p /usr/local
Detected Linux-x86_64
WasmEdge Installation at /usr/local
Fetching WasmEdge-0.11.2
...
var? wasmedge-tensorflow
0.11.2 0.11.2
Installation of wasmedge-tensorflow-0.11.2 successfull
var? wasmedge-tensorflow-lite
0.11.2 0.11.2
Installation of wasmedge-tensorflow-lite-0.11.2 successfull
WasmEdge binaries accessible

$ ls -la /usr/local/bin/
total 15396
drwxr-xr-x  2 root root    4096 Nov 26 02:04 .
drwxr-xr-x 10 root root    4096 Nov 26 02:04 ..
-rwxr-xr-x  1 root root   18520 Nov  3 16:00 show-tflite-tensor
-rwxr-xr-x  1 root root   14392 Nov 26 02:04 wasmedge
-rwxr-xr-x  1 root root 7223696 Nov  3 16:00 wasmedge-tensorflow
-rwxr-xr-x  1 root root 8477856 Nov  3 16:00 wasmedge-tensorflow-lite
-rwxr-xr-x  1 root root   14392 Nov 26 02:04 wasmedgec

$ wget https://github.com/second-state/runwasi/releases/download/v0.3.3/containerd-shim-wasmedge-v1-v0.3.3-linux-amd64.tar.gz

...

$ tar -zxvf containerd-shim-wasmedge-v1-v0.3.3-linux-amd64.tar.gz 
containerd-shim-wasmedge-v1
$ sudo mv containerd-shim-wasmedge-v1 /usr/local/bin/

それが終わったら、手元で docker daemon を build します。
README.md には wasmedge のブランチに checkout するようになっていますが、wasmedge のブランチは削除されてしまったので、代わりに c8d のブランチを利用します。
https://github.com/rumpl/moby/pull/102

# checkout wasmedge
$ git clone https://github.com/rumpl/moby.git && cd moby && git checkout wasmedge && make binary
Cloning into 'moby'...
remote: Enumerating objects: 347015, done.
remote: Counting objects: 100% (163/163), done.
remote: Compressing objects: 100% (73/73), done.
remote: Total 347015 (delta 100), reused 137 (delta 89), pack-reused 346852
Receiving objects: 100% (347015/347015), 184.92 MiB | 14.79 MiB/s, done.
Resolving deltas: 100% (230573/230573), done.
error: pathspec 'wasmedge' did not match any file(s) known to git

# checkout c8d
$ git clone https://github.com/rumpl/moby.git && cd moby && git checkout c8d && make binary
Cloning into 'moby'...
remote: Enumerating objects: 347015, done.
remote: Counting objects: 100% (167/167), done.
remote: Compressing objects: 100% (75/75), done.
remote: Total 347015 (delta 103), reused 139 (delta 91), pack-reused 346848
...
 => [binary-base 7/7] WORKDIR /go/src/github.com/docker/docker                                       0.0s
 => [build-binary 1/1] RUN --mount=type=cache,target=/root/.cache     --mount=type=bind,target=.,  117.6s
 => [binary 1/1] COPY --from=build-binary /build/bundles/ /                                          2.4s
 => exporting to client                                                                              1.2s
 => => copying files 203.34MB  

終わったら、containerd-snapshotter feature を有効化した上でビルドした docker daemon を background で実行させましょう。

$ sudo vi /etc/docker/daemon.json
$ cat /etc/docker/daemon.json 
{
  "features": {
    "containerd-snapshotter": true
  }
}

$ ps aux | grep "docker"
root        2248  1.6  5.7 2199436 232648 ?      Ssl  03:32   0:23 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
ubuntu     19203  0.0  0.0   7004  2088 pts/0    S+   03:56   0:00 grep --color=auto docker
$ nohup sudo -b sh -c "./bundles/binary-daemon/dockerd -D -H unix:///tmp/docker.sock --data-root /tmp/root --pidfile /tmp/docker.pid"

# backgroudでdocker daemonのprocessが動いている
$ ps aux | grep "docker"
root        2248  2.7  5.8 2199436 232968 ?      Ssl  03:32   0:23 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
root       19002  0.0  0.0  11892  3680 pts/0    S    03:46   0:00 sudo -b sh -c ./bundles/binary-daemon/dockerd -D -H unix:///tmp/docker.sock --data-root /tmp/root --pidfile /tmp/docker.pid
root       19003  0.0  0.0  11892   888 pts/1    Ss+  03:46   0:00 sudo -b sh -c ./bundles/binary-daemon/dockerd -D -H unix:///tmp/docker.sock --data-root /tmp/root --pidfile /tmp/docker.pid
root       19004  0.0  0.0   2888  1008 pts/1    S    03:46   0:00 sh -c ./bundles/binary-daemon/dockerd -D -H unix:///tmp/docker.sock --data-root /tmp/root --pidfile /tmp/docker.pid
root       19005  0.6  1.4 1357212 56256 pts/1   Sl   03:46   0:00 ./bundles/binary-daemon/dockerd -D -H unix:///tmp/docker.sock --data-root /tmp/root --pidfile /tmp/docker.pid
ubuntu     19091  0.0  0.0   7004  2080 pts/0    S+   03:46   0:00 grep --color=auto docker

最後に、動かしている docker daemon (docker endpoint) を指定するために docker context を作成 + スイッチして、docker コマンドを実行します。
Image は examples にも記載のある、michaelirwin244/wasm-exampleを利用します。
https://github.com/docker/docs/blob/main/desktop/wasm/index.md#usage-examples

$ docker context ls
NAME        DESCRIPTION                               DOCKER ENDPOINT               KUBERNETES ENDPOINT   ORCHESTRATOR
default *   Current DOCKER_HOST based configuration   unix:///var/run/docker.sock                         swarm
wasm                                                  unix:///tmp/docker.sock                             
$ docker context use wasm
wasm
Current context is now "wasm"

$ docker run -dp 8080:8080 \
  --name=wasm-example \
  --runtime=io.containerd.wasmedge.v1 \
  --platform=wasi/wasm32 \
  michaelirwin244/wasm-example
Unable to find image 'michaelirwin244/wasm-example:latest' locally
2a58923a21cb: Download complete 
130eeaf02640: Download complete 
e049f00c5289: Download complete 
88b5b1b44a1e5a63cb4bbc77e02583d0c776a5ab05e2bcf3c50b4ba942d84017
$ docker ps
CONTAINER ID   IMAGE                          COMMAND              CREATED         STATUS         PORTS                                       NAMES
88b5b1b44a1e   michaelirwin244/wasm-example   "hello_world.wasm"   9 seconds ago   Up 8 seconds   0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   wasm-example
$ curl localhost:8080
Hello world from Rust running with Wasm! Send POST data to /echo to have it echoed back to you

最後に

WASM の勉強がてら、WASM 対応の docker engine 使って WASM を docker 上で動かしてみました。
個人的には、Rust などで書いたコードをコンパイルして OCI image としてビルドしてレジストリに push するところまでやってみたいのと、proxy-wasm も気になっています。

Discussion