「WebAssembly」は次世代のJava、Node.jsになる?――「Wasmコンテナ」をKubernetesで動かすCloud Nativeチートシート(23)

Kubernetesやクラウドネイティブをより便利に利用する技術やツールの概要、使い方を凝縮して紹介する連載。今回は、最近注目されている「WebAssembly」について復習しながら、WebAssemblyのアプリケーションをKubernetesで試す方法を紹介する。

» 2022年12月22日 05時00分 公開
[岡本隆史株式会社NTTデータ]

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

 Kubernetesやクラウドネイティブをより便利に利用する技術やツールの概要、使い方を凝縮して紹介する本連載「Cloud Nativeチートシート」。今回は、最近注目されている「WebAssembly」について復習しながら、WebAssemblyのアプリケーションをKubernetesで試す方法を紹介します。

Webブラウザからサーバサイドへ〜注目されるWebAssembly〜

 ブラウザ上で高速にアプリを実行する仕組みとして、WebAssembly(「Wasm」と省略されることもある)が注目されています。W3C(World Wide Web Consortium)で標準化されていることもあり、2017年ごろには既に主要なブラウザはWebAssemblyをサポートしています。さらに「Zoom」「Google Meet」「Google Earth」「Unity」などでWebAssemblyが利用されています。

 WebAssemblyはブラウザ上でコードを高速に動かす仕組みとして登場しましたが、ホスト上でも動作するようになってきており、GoogleやMicrosoft、Amazon.com、VMware、Intel、Dockerなどが非営利組織「Bytecode Alliance」において、WebAssemblyをホスト上で実行するWebAssemblyランタイムを開発しています。2022年9月には、Bytecode Allianceで開発されているWebAssemblyランタイム「Wasmtime」のバージョン1.0がリリースされ、話題になりました。

 JavaやJavaScriptがブラウザでの利用から始まって、JavaアプリケーションサーバやNode.jsのようにサーバサイドに広がったのと同じように、WebAssemblyもサーバサイドでの動作が徐々にターゲットになってきています。WebAssemblyの調査報告書「The State of WebAssembly 2022」のWebAssemblyの用途別利用者数を見ると(下図)、Webの開発に次いで、サーバレス、コンテナに利用している開発者が多いことが分かります。

WebAssemblyの用途別利用者数

 Kubernetesでも、WebAssemblyをサポートしたKubelet「Krustlet」や、WebAssemblyの実行をサポートしたコンテナランタイム「crun」などによって、WebAssemblyのアプリケーションを実行できるようになってきています。本稿では、WebAssemblyについて復習しながらKubernetesの対応状況と、「kind」(「Kubernetes in Docker」の略。Docker上にKubernetesクラスタを構築するツール)とcrunを用いて、WebAssemblyのアプリケーションをKubernetesで実行する方法を紹介します。

 なお、Kubernetesに関するWebAssemblyの状況は日々変化しており、本稿現時点で安定利用できる状況ではありません。本稿は2022年12月時点の内容です。今後の動作を保証するものではないので、あらかじめご了承ください。

WebAssemblyとは?

 JavaScriptはWebブラウザの標準言語として長らく使われてきましたが、AIやデータ分析などのトレンドによって、Webブラウザ上でより高速な処理が求められるようになりました。このような背景の下、JavaScriptをより高速に実行できるようにする手段として「asm.js」が提案されていましたが、asm.jsをさらに進化させた仕組みとして、WebAssemblyが提案され、W3Cで標準化されています。

 WebAssemblyは、仮想命令セットアーキテクチャあるいは低水準プログラミング言語の一種です。WebAssemblyでは、バイナリ形式のWasmバイナリとバイナリを人が読みやすくしたテキスト形式の「WAT」(WebAssembly Text format)の2種類のフォーマットが定義されています。

 WasmバイナリはアーキテクチャやOSに依存せず仮想マシンによって実行可能なコードで、Javaのバイトコードや、.NETにおけるCIL(共通中間言語)に似ています。具体的なWasmバイナリと対応するWATは、「wat2wasm demo」で提供されている、Wasm仮想マシンのシミュレーターで表記と動作を確認できるので、興味がある方は利用してみてください。

 WATはテキストですが、アセンブラに近い低水準言語で、プログラムで利用するには使いにくいです。プログラミング言語としては、C/C++やGo言語、Rust、C#などが利用され、これらの言語で記述したコードをコンパイラでWasmバイナリにコンパイルして利用します。

WebAssemblyの6つの特徴

 WebAssemblyの特徴をもう少し見てみましょう。

1.高速

 JavaScriptは高速化されてきましたが、それでもネイティブのバイナリに比べると遅いです。動的な片付けによる実行時の型チェック、スクリプトのパース、またテキスト言語なのでファイルサイズも大きくなり、読み込みに時間がかかります。

 静的な型付けによって、型チェックが不要です。バイナリ形式でコードを記述することによって構文解析が不要になるので、高速に動作します。また、ガベージコレクションも利用しないので、メモリ管理のオーバーヘッドもありません。

2.ポータビリティ

  OSやCPUアーキテクチャに依存しないので、WebAssemblyランタイムが提供されていれば、どのOS(WindowsやmacOS、Linuxなど)やアーキテクチャ(x86、Arm、Risc Vなど)でも同一バイナリで動作します。

 この特徴を生かして、ツールやミドルウェアの拡張プラグインにも利用され始めています。

3.W3Cによる仕様の標準化とブラウザでの標準サポート

 特定のベンダーやアライアンスによる仕様の策定ではなく、Web仕様の標準化団体W3Cによって仕様が標準化されています。WebAssemblyの仕様は、2022年4月19日に「WebAssembly Core Specification Version 2.0」として最初の公な作業草案が公開されています。

 W3Cで標準化されることによって、ベンダーの戦略や都合に振り回されることはありません。追加でランタイムをインストールする必要があったJavaやAdobe Flashと異なり、多くのブラウザが標準でWebAssemblyをサポートしており、プラグインやアドオンなどを追加する必要がありません。

4.多言語対応

 WebAssemblyは仮想命令セットアーキテクチャを規定しているだけで、プログラミング言語は規定していません。そのため、C/C++やRust、Go言語、C#などの言語でコードを記述し、Wasmバイナリにコンパイルして実行します。

 PythonやRubyのように、インタープリタや仮想マシンがWebAssemblyに移植され、WebAssembly上でスクリプトを動作させられるものも出てきています。先述のThe State of WebAssembly 2022によると、Rust、C++、Go言語の利用割合が高く、2021〜2022年にPythonや「Blazor」※が急速に増加しているのが分かります。

WebAssemblyで利用されている言語の割合

※Blazorは、開発者がC#とHTMLを使用してWebアプリケーションを作成できるようにするオープンソースソフトウェアのWebフレームワークです。「Blazor WebAssembly」というWasmバイナリとしてアプリを開発するフレームワークが提供されています。

 WebAssemblyがサポートする言語と実装、コンパイラの一覧については、Stephen A.氏の「Awesome WebAssembly Languages」によくまとまっているので、興味がある方はご覧ください。

5.高いセキュリティ

 WebAssembly自身は、仮想命令セットアーキテクチャの仕様を定義しており、OSの機能を利用したり、ネットワークにアクセス機能を提供したりしていません。「サンドボックス上で実行すると、サンドボックス外に影響を与えるのが難しい」ということで、高いセキュリティを持っています。

 Javaではクラスローダやリフレクションなどの仕組みによって、脆弱(ぜいじゃく)性のあるコードを読み込んだり、実行されたりすることがありましたが、WebAssemblyはそのような仕組みを持っていないので、脆弱性を生みづらい側面もあります。

6.用途に応じてAPIを定義

 先ほど「OSの機能を持っていない」と記述しましたが、それでは不便過ぎるので、ブラウザで利用する際にJavaScriptと連携する「WebAssembly JavaScript Interface Version 2.0」やホスト上で実行する際にファイルやネットワークアクセスなどのOSの機能を利用する「WebAssembly System Inferface」(WASI)が提供されています。そういう意味では、「5.高いセキュリティ」で記述した内容と矛盾しますが、必要に応じて最小限のAPIを利用するようになっている点で、Javaのように全ての機能がJavaランタイムに入っているのに比べると安全だといえます。

KubernetesにおけるWebAssemblyのサポート

 ここからは、KubernetesにおけるWebAssemblyの対応について解説します。WebAssemblyの使われ方は、次の3つがあります。

1.WebAssemblyアプリケーションの実行

 本稿で取り上げるユースケースですが、その名の通り、WebAssemblyアプリケーションをKubernetes上で実行しますが、幾つか方法があるので、次章で詳しく説明します。

2.Kubernetesのコンポーネントの拡張

 WebAssemblyのプラットフォームフリーの特徴を利用して、コンポーネントの拡張部分をWebAssemblyとして作成しておき、組み込む使い方があります。代表的なものとして、「Istio」のサービスメッシュで利用されるプロキシ「Envoy」のフィルタなどの処理をWebAssemblyで書けるようになっています。

3.ツールの拡張

 ツールの機能を、WebAssemblyを利用して拡張します。使い方としては、上記2と似ているかもしれませんが、外部ツールとして直接Kubernetesには組み込まれない使い方なので、あえて分けました。例えば、脆弱性スキャンツール「Trivy」は、WebAssemblyを利用してスキャンポリシーの定義を拡張できます。

WebAssemblyアプリケーションの実行

 Kubernetes上でWebAssemblyアプリケーションを実行する方法は大きく分けて次の4つが存在します。

KubernetesによるWebAssemblyサポート方式

1.コンテナ上で実行させる

 コンテナイメージにWebAssemblyランタイムを梱包(こんぽう)してアプリケーションを実行する方式です。特にKubernetesの設定を変更することなく利用できるので、一番手軽な方法です。

 コンテナイメージにWebAssemblyランタイムを組み込む必要があり、アーキテクチャが異なる場合は各アーキテクチャに対応したWebAssemblyランタイムを組み込んだイメージを作成する必要があり、コンテナのポータビリティは失われます。例えば、x86のLinux用に作成したイメージはArmやWindowsなどで利用できません。

2.コンテナランタイム(OCIランタイム)で実行させる

 OCI(Open Container Initiative)準拠のコンテナランタイム(OCIランタイム)として利用されている「runc」の代わりにWebAssemblyに対応した「crun」「youki」を利用します(crun、youkiの詳細については、下記のコラムを参照)。

 crunやyoukiを利用することで、Wasmコンテナ内のWebAssemblyのバイナリを直接実行できます。Wasmコンテナ内には、アーキテクチャに依存するランタイムの導入は不要となるので、コンテナのポータビリティは高くなります。crunやyoukiはruncと互換性があり、ランタイムを丸ごと交換すればいいので、アーキテクチャは下記のKrustletを利用する方法より単純です。

 crunやyoukiは通常のコンテナイメージも実行できるので、Wasmコンテナと通常のコンテナを同じノードで実行できます。2022年12月の本稿執筆時時点では、crunでWasmコンテナを利用するには、Wasmランタイム対応のオプションを付けてビルドする必要があるので注意が必要です。

3.containerd shimを利用して実行

 「containerd shim」とは、containerdとOCIランタイムの間にある、コンテナごとに起動されるプロセスです。containerdとOCIランタイムの橋渡しをする役割を持っています。

 この方式は、WebAssemblyランタイムに対応したcontainerd shimを利用することによって、OCIランタイムを介すことなくWebAssemblyアプリケーションを実行させます。

 Docker DesktopではDocker Desktop 4.15からWebAssemblyがTechnology Previewとして、この方式でWebAssemblyをサポートしています。

 この方式をKubernetesで利用する場合、従来のコンテナとWasmコンテナの実行を使い分けるために、「RuntimeClass」を定義し、OCIランタイムとWebAssemblyランタイムを切り替えて実行する必要があります。

4.Workerノード上で実行させる

 Kubeletを、WebAssemblyを実行するKrustletに置き換えます。Krustlet上では従来のコンテナは実行できないので、WebAssemblyは、WebAssembly専用のノード上で実行されることになります。専用のノードを構築する必要があるので、上記2よりリソース利用の効率が悪くなります。

 なおKrustletの開発自体は2022年12月現在、止まっているようです。

 本稿では、上記方法2のcrunでWebAssemblyをKubernetesで利用する方法を解説します。

コラム 次世代OCIランタイムcrunとyouki

 runcは、Go言語で記述されており、長らく標準のOCIランタイムとして利用されてきましたが、ここに来て、C言語やRustで実装したOCIランタイムが注目されています。C言語で実装されたランタイムとしては、Giuseppe Scrivano氏によって開発されてるcrunがあり、RHEL(Red Hat Enterprise Linux) 9.0からデフォルトのOCIランタイムとして利用されるなど、採用が広がっています。

 youkiは、containerの日本語訳の容器(ようき)から名付けられており、名前から連想させられる通り、日本人のToru Komatsu(うたもく)氏を中心に開発されています。youkiは、最近Linuxでも採用された言語Rustで実装されています。正式リリースはまだですがOCIによって提供されているインテグレーションテストを全てパスするなど、かなり実用的なレベルで開発されています。

 crunやyoukiのような新しいOCIコンテナランタイムは、runcに比べ、下記のような特徴があります。

  • コンテナの実行速度が速い
  • 消費メモリが少なくて済む
  • Wasmコンテナをサポートしている

 本稿では、Wasmコンテナの利用にcrunを利用していますが、youkiもWasmコンテナをサポートしているので、興味がある方は挑戦してみてください。


WebAssemblyの実行環境構築

 ここからは、KubernetesでWebAssemblyを実行する環境を構築します。本稿では、kindを利用して、Wasmランタイムの一つ、WasmEdgeを利用したWebAssembly実行環境を構築します。今回は、「Ubuntu 22.04」を利用しました。Dockerが動作すれば、他の環境でも動作すると思うので、チャレンジしてみてください。

Dockerのインストール

 Dockerをインストールします。本稿では、余分な手順を省くので、全てrootユーザーで実行していますが、あらかじめご了承ください。

$ sudo su -
# apt-get update
# apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release
# curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# apt-get update
# apt-get install -y docker-ce

kindのインストール、Kubernetesクラスタの構築

 kindでKubernetesクラスタを構築します。Wasmコンテナを利用するので、Wasmコンテナに対応したノードイメージを指定します。

# curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.17.0/kind-linux-amd64
# chmod +x ./kind
# mv ./kind /usr/local/bin/kind
# kind create cluster --image ghcr.io/liquid-reply/kind-crun-wasm:v1.23.4

 kubectlコマンドをインストールしてクラスタの動作を確認します。

# curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl"
# mv kubectl /usr/local/bin/
# chmod a+x /usr/local/bin/kubectl
# kubectl get all
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
service/kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   24h

 クラスタの動作を確認できました。

動作確認

 Wasmコンテナ対応のKubernetesクラスタを構築できたら、WebAssemblyアプリケーションの動作を確認します。下記コマンドを実行します。

# kubectl run -it --rm --restart=Never wasi-demo --image=wasmedge/example-wasi:latest --annotations="module.wasm.image/variant=compat-smart" --overrides='{"kind":"Pod", "apiVersion":"v1", "spec": {"hostNetwork": true}}' /wasi_example_main.wasm 50000000
Random number: -949523771
Random bytes: [247, 45, 184, 56, 214, 84, 86, 174, 20, 206, 40, 137, 170, 173, 6, 110, 222, 15, 10, 255, 194, 154, 217, 137, 73, 162, 188, 36, 166, 74, 217, 219, 121, 203, 225, 179, 62, 179, 83, 251, 42, 251, 158, 135, 161, 3, 7, 148, 174, 184, 159, 42, 160, 152, 154, 209, 221, 205, 116, 140, 140, 247, 39, 62, 221, 81, 161, 136, 45, 10, 111, 52, 97, 213, 67, 174, 132, 133, 200, 68, 199, 42, 176, 207, 51, 179, 252, 211, 186, 162, 219, 83, 42, 17, 254, 214, 94, 51, 204, 192, 252, 7, 23, 6, 154, 126, 56, 244, 245, 146, 43, 222, 254, 231, 26, 76, 115, 149, 36, 126, 24, 59, 132, 110, 71, 252, 238, 151]
Printed from wasi: This is from a main function
This is from a main function
The env vars are as follows.
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
TERM: xterm
HOSTNAME: kind-control-plane
KUBERNETES_PORT_443_TCP_PORT: 443
KUBERNETES_PORT_443_TCP_ADDR: 10.96.0.1
KUBERNETES_SERVICE_HOST: 10.96.0.1
KUBERNETES_SERVICE_PORT: 443
KUBERNETES_SERVICE_PORT_HTTPS: 443
KUBERNETES_PORT: tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP: tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP_PROTO: tcp
HOME: /
The args are as follows.
/wasi_example_main.wasm
50000000
File content is This is in a file
pod "wasi-demo" deleted

 上記で指定している「wasmedge/example-wasi:latest」は普通のコンテナイメージに見えますが、コンテナ実行時のバイナリにWasmバイナリが指定されてるWasmコンテナです(詳細は、後述の「Wasmコンテナイメージの作り方」を参照)。Podもしくはコンテナに「annotations "module.wasm.image/variant=compat-smart"」を付けることによって、Wasmコンテナとして実行できます。

 ちなみに、アノテーションを付与せずに普通に起動しようとすると、コンテナの実行ファイルとして指定されているWasmバイナリが「実行ファイルではない」と、エラーで怒られます。

# kubectl run -it --rm --restart=Never wasi-demo --image=wasmedge/example-wasi:latest
{"msg":"exec container process `/wasi_example_main.wasm`: Exec format error","level":"error","time":"2022-11-17T06:44:05.361480Z"}
pod "wasi-demo" deleted
pod default/wasi-demo terminated (Error)
Wasmコンテナをアノテーションなしで起動しようとした場合

コラム kindでWasmコンテナを動作させる仕組み

 kindは、Docker上にKubernetesクラスタを構築しているので、コンテナランタイムにDockerを利用していると思われがちですが、Docker内にcontainerdを起動し、そのcontainerdをコンテナランタイムとして利用しています。

 kindのWasmEdge対応のノードイメージは、runcの代わりにWasmEdgeを組み込んだcrunが利用されているので、下図のように、ホストのDockerは何も設定を変更することなく、Wasmバイナリが動作するようになっています。

kindでWasmコンテナを動作させる仕組み

 Wasmコンテナがうまく動作しなかった際のトラブルシューティングとして、筆者は最初このアーキテクチャを理解しておらず、手間取りました。上図の緑とcontainerd(コンテナ上)と青のcontainerd(ホスト上)は、同じcontainerdでも別のバイナリであることに注意してください。

 また、コンテナ上のruncファイルの中身はcrunとなっているので、この点も注意してください。これは、crunとruncが互換性があるのでruncをcrunで上書きして利用することによって、containerdの設定を省いているものと思われます。


Wasmコンテナイメージの作成

 Wasmコンテナの動作を確認できたら、自分でWasmコンテナイメージを作って実行してみましょう。ここでは、言語はRustを利用しますが、C/C++やGo言語などでも使えるので、興味がある方は他の言語でも試してください。

 まずは、マルチステージビルド機能を利用して、コンテナ内でWasmバイナリをビルドしてWasmバイナリのみをコピーしたWasmコンテナイメージを作るようにします。

 適当なディレクトリを作って、下記のようなディレクトリ構成でDockefile, Cargo.tomlとmain.rsをコピーします。ファイル一式は、GitHubから取得できます。

Dockerfile
Cargo.toml
src/
  main.rs
ファイル一覧

 まずは、コードを見ていきます。helloを表示する簡単なRustのサンプルコードです。

use std::env;
fn main() {
  println!("hello");
  for argument in env::args().skip(1) {
    println!("{}", argument);
  }
}
main.rs

 次に、Rustのビルドシステム「Cargo」の設定ファイルを作成します。

[package]
name = "hello"
version = "0.1.0"
authors = ["itmedia"]
edition = "2021"
 
[[bin]]
name = "hello"
path = "src/main.rs"
 
[dependencies]
Cargo.toml

 最後にコンテナを作成するためのDockerfileを作成します。マルチステージビルドを用いて「builder」ステージでWasmバイナリをビルドして空(scratch)のコンテナイメージに生成した「hello.wasm」をコピーします。コンテナ起動時のエントリポイントには、hello.wasmを指定します。

# builder stage
FROM rust:1.65.0 AS builder
 
RUN rustup target add wasm32-wasi
 
WORKDIR /build
COPY Cargo.toml .
COPY src /build/src/
 
RUN cargo build --target wasm32-wasi --release
 
# container image
FROM scratch
 
COPY --from=builder /build/target/wasm32-wasi/release/hello.wasm /hello.wasm
 
ENTRYPOINT ["/hello.wasm"]
Dockerfile

 イメージをビルドして、コンテナレジストリにイメージをアップロードします。ここでは「Docker Hub」(docker.io)にアップロードします。Docker Hubのアカウントを持っていない場合は、あらかじめ取得するか、他のイメージレジストリのURLを指定してください。

# docker build -t docker.io/{アカウント名}/mywasm:latest .
……
# docker login docker.io
(ユーザー名、パスワードでDockerHubにログイン)
# docker push docker.io/{アカウント名}/mywasm:latest

 コンテナレジストリにアップロードできたら、動作を確認します。

# kubectl run -it --rm --restart=Never wasi-demo --image=toraneko/mywasm:latest --annotations="module.wasm.image/variant=compat-smart"
hello
pod "wasi-demo" deleted

kind以外の環境でのWasmコンテナの実行

 kind以外の環境で、Wasmコンテナを実行するには、「containerd/cri-o」をコンテナランタイムとして利用している場合は、内部で利用されているOCIランタイムのruncをcrunやyoukiに指定することで、Wasmコンテナを実行できるようになります。

 WebAssemblyランタイムとして、WasmEdgeをcrunに組み込んでcontainerdで利用する方法が「WasmEdge Runtime - containerd」に記載されているので、興味がある人は挑戦してみてはいかがでしょうか。

 crunのビルドが必要で、上記URL参照先の手順も実行する必要があるので、注意してください。

おわりに

 本稿では、WebAssemblyを復習しながら、KubernetesでWebAssemblyアプリケーションを実行する方法を見てきました。

 kindを利用すれば、Wasmコンテナを簡単に実行できますが、KubernetesでWasmコンテナを実行する方法はまだ、混沌(こんとん)としており、2021年辺りに注目されたKrustletの開発が停止していたり、自身が構築したKubernetes環境でWasmコンテナを実行しようとするとcrunバイナリのビルドが必要だったり、Docker Desktopで新たなWasmコンテナの実行方法が実装されたりなどが現状です。今後の動向が注目されます。

Copyright © ITmedia, Inc. All Rights Reserved.

スポンサーからのお知らせPR

注目のテーマ

Microsoft & Windows最前線2025
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。