Docker+Wasm Technical PreviewからWASMに入門する
少し前ですが、Docker の Technical preview として WASM (WebAssembly) のサポートが発表されました。
普段から Docker を利用していますが、そもそも WASM が何なのか? WASM をサポートしたことによって何ができるようになるのか? がわかっていなかったので、一通り調べてみました。
WASMとは
まずは、WASM 自体の理解を深めるために、下記のドキュメントにざっと目を通します。
WASM とはバイナリコードの仕様で、そのフォーマットに対応したバイナリを実行する Stack-based な仮想マシンのことを指します。
開発初期は、ブラウザ(JS)の高速化が目的で、それを念頭に置いたような記載もドキュメントにあります。
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 の特徴としては、下記が挙げられます。
- Efficient and fast: WASM のコードは「near-native speed」で動く
- Portability: Web の標準のようなバージョンレス、機能テスト済み、後方互換性のある状態で設計されており、バイナリであることも踏まえて様々な環境で実行可能
- Secure: WASM のバイナリはホストから分離された環境で実行される
WASM のドキュメントで挙げられているユースケースを見てみると、Games などの Inside the browser
の項目もある一方で、Outside the browser
の項目もあります。
WASIとは
non-browser (outside the browser
) な環境、具体的には OS 上で WASM を動かすために利用されるのが WASI (WebAssenbly Interface)です。
そもそも、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 が普通のプロセスのように動かせるようになります。
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 に関する詳細な内容は下記の記事や動画が参考になりそうです。
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 に関してはこの記事では特に説明をしませんが、下記の記事が参考になります。
WASM のバイナリを docker 上で実行するということですが、内部的にはどのように実現してるのでしょうか?
Docker engineのcontainerd対応
intergration に関する説明の部分では、下記のような画像が利用されていました。
通常の構成が画像の左側に記載されています。
ざっくりと説明すると、high-level runtime である containerd と、low-level runtime である runc の仲介役としての containerd-shim がいます。
runc は low-level runtime で、Linux kernel とやりとりして実際にリソースの隔離 = コンテナの作成を実現しています。
コンテナを利用した開発ツールである docker ですが、コンテナイメージやコンテナの実行状態の管理などを行う 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 対応の構成の説明に戻ります。
WASMサポートがどのようにして実現されているのか
docker で WASM のバイナリを動かす際には、内部的には下記のようになっています。
- Docker engine で containerd を有効化することで、containerd-shim を利用できるようにする
- WASM ランタイムのWasmEdgeを containerd-wasm-shim 経由で操作する
- WasmEdge はOCI準拠のインターフェースを提供しているので、Dockerなどのコンテナツールから動かせる
- shim は OCI アーティファクトから Wasm モジュールを抽出し、WasmEdge ランタイムを使用して実行する役割を担う
- GitHub にも、WASM 対応の containerd-shim の実装がある
- 上記の新しい shim を使用できるようにする、docker のフラグサポートを追加している
--runtime=io.containerd.wasmedge.v1
サンプルとして記載されていた 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 として動かしてみます。
$ docker -v
Docker version 20.10.21, build baeda1f
$ docker compose version
Docker Compose version v2.12.2
一応、検証の際に利用した EC2 を立てるための Terraform のコードも置いておきます。
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 のブランチを利用します。
# 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
を利用します。
$ 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