Linuxコンテナの「次」としてのWebAssembly、の解説
はじめに
WASMをブラウザの外で動かすトレンドに関して「Linuxコンテナの「次」としてのWebAssemblyの解説」というタイトルで動画を投稿したのですが、動画では話しきれなかった内容をこちらの記事で補完したいと思います。
2022年もWebAssembly(WASM)の話題が多く発表されましたが、そのひとつにDocker for DesktopのWASM対応があります。FastlyやCloudflareもエッジ環境でWASMを動かすソリューションを持っていますし、MSのAKS(Azure Kubernetes Service)でもWASMにpreview対応しています。WASM Buildersでも2023年のWASMの予想としてWASMのアプリケーションランタイム利用に関して言及されました。
WASMといえば元々ブラウザ上で高速にC++のコードなどを実行するところから始まっているわけですが、今回はブラウザの「外」で動かすWASMをLinuxコンテナの「次」 という視点から考えてみます。
TL;DR
- コンテナ界隈の成熟と共にLinux由来のユーザランドの管理が足枷に
- WASM/WASIを使う事でアプリケーションとインフラをより明確に分離
- WASMをDocker/k8sのエコシステムに統合したりCDN Edgeのより高集約な基盤として利用
WebAssemblyってなんだっけ?
WebAssembly(WASM:わずむ) はブラウザ上でセキュアにC++などから作られたプログラムを高速に実行するためにNaCl(Google Native Client)とEmscripten及びasm.jsの後継として生まれました。去年でちょうど5周年でその歴史を振り返る特設サイトもあります。またWASM登場までの歴史は以下の記事がメチャクチャ参考になりました。
その特徴として以下を上げることができます。
- Portable
- Language Free
- Lightweight
- Sandbox / Secure
これらの特徴はWASMが.NETやJavaのようなバイトコードベースの仮想マシンであることに由来します。CやRustなど任意の言語からバイトコードであるWASMに変換して、ブラウザ上のランタイム実行します。特にWASMは「信頼できない開発者によるコードを安全に実行する」ために「サンドボックス機能」[1]を備えているのが重要です。またJITコンパイル等も備えるためOSやCPUによらず実行でき、ネイティブバイナリに匹敵する速度で実行されます。[2]
ref: https://wasmlabs.dev/articles/docker-without-containers/
ちなみにWASMはアセンブラのようなテキスト表現があります。LISPのようなS式で表すことが出来ます。以下は32bit整数を2受け取って合計し32bit整数として返すWATです。
(module
(func (export "addTwo") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add)
)
スタックマシンなので比較的シンプルではありますが、さすがに通常人間が手で書くものでは無く、デバック等が目的ですね。
WASM自体はファイルアクセスやネットワークアクセス、ついでにDOMアクセスとかも含めて直接実行できません。自分自身を操作する方法しかないので、それ以外はホスト側(ランタイム側)で実行されます。OSのシステムコールみたいな感じですね。そして、ランタイム側で明示的に許可されて渡されたものだけが実行できるためセキュアなわけです。マルウェアが大事なローカルファイルにアクセスしたりNW経由で情報を送れないってことですね。なんだかAndroidやiOSの「アプリの権限」にも似ています。また仮想マシンであるためメモリ的にも独立していますし、他のVM系言語とは異なり他の領域にはアクセスさせないサンドボックスを徹底しています。
ref: https://hacks.mozilla.org/2019/11/announcing-the-bytecode-alliance/
実行においても脆弱性の温床になるメモリ系の検証に気を使っているようで、以下の記事が参考になりました。
後述するWASIを含めて、WASM界隈はセキュリティとサンドボックスにかなり意識を注いでいるコミュニティだと思います。
WASI、ブラウザのその先へ
Bytecode Allianceとナノプロセス
WebAssembly System Interface(WASI:わし) はブラウザの「外」でWASMを動かすための標準仕様でBytecode Allianceによって策定されています。
Bytecode Allianceはナノプロセスというgemやnpmのようなモジュールに脆弱性が混ざり込んでも各々のモジュール呼び出しを低オーバーヘッドのSandboxに包むことでアプリ全体をセキュアにする、というわりと壮大な目標をもって各種の仕様策定を進める団体です。
ref: https://hacks.mozilla.org/2019/11/announcing-the-bytecode-alliance/
Intel、Mozilla、Red Hat、Fastlyによって設立され今ではDockerやGoogle,Microsoftなど多くの団体が所属しています。WASIやモジュールのリンク、インタフェースの標準化等を頑張っています。WASM/WASIに準拠したランタイムであるWasmtimeの開発もしておりファジングテストに飽き足らずコアモジュールは形式検証を実施するというセキュリティへの意地が見えます。
ちなみに2023/01時点ではWASI-Core等の標準化にとどまっており、ナノプロセスに必須のリンク周りの仕様はまだ標準化が出来ていない認識です。今後に期待ですね!
WASIとは?
さて、そんな壮大な目標に向けて作られたものの一つWASIとしてブラウザの外で動かすときの標準的な実行方法を定義したのがWASIです。WASMをブラウザで動かす場合にはホストはブラウザなのである程度自ずとサンドボックスになりますし提供するべき機能も限定的です。そのためEmscriptanを使って結構ハック的に実装されていたようです。WASIではそれらを標準化しブラウザに限定しないキレイな仕様にしています。
ref: https://wasmlabs.dev/articles/docker-without-containers/
WASIはCloudABIのcapability-based securityという思想を下敷きにPOSIXライクなAPIをCore APIを提供しています。ブラウザとは異なり、ファイルアクセス等を提供するといってもフル権限ではなく限定したSandboxとして提供します。
ref: https://inzkyk.xyz/mozilla_hacks/wasi/
この当たりは今と昔のセキュリティ事情の違いによるものです。MacやAndroid/Chromebookを除けばUNIXやLinuxの系譜は現在はサーバサイドで利用されることが多いでしょう。そのためPOSIXで想定されているマルチユーザのためのファイルアクセス権による制御ではセキュリティが不十分です。なぜなら守るべきが 「他人からの不正な操作」 ではなく 「自身による不正なプログラムの実行」 に変わっているからです。そのためファイルディスクリプタを渡してそれ以外の権限は与えないなどよりきめ細やかな対応を実施しています。また、WASIではWASI libcを提供することで従来のコードを修正することなく移植を可能にしますが、完全に同等の機能を提供するわけではなくWASMに該当する概念がない場合やセキュリティ上問題のある機能は提供されていません。
こうして複数の言語をポータブルかつセキュアに実行できるのがWASM/WASIです。
Javaじゃダメなの?
WASMの特徴の一つであるポータブルという点から、WASIのようにそれを外で動かす、という話になると「Javaじゃダメなんだっけ?」というコメントを見かけることも多いです。もちろん、Javaあるいは.NETのようなもので良いケースはあると思いますが 「コンテナの次」 というコンテキストではそれらよりもメリットがあります。それは軽量さとサンドボックスです。
サーバサイドで実行するようなコンテナの場合はサーバレスの運用になる事も多いですが、この場合はメモリのフットプリントの軽さや起動速度というのが非常に重要になります。GCが無いのもこの点では優位かもしれません。Java、特にサーバサイドJavaは異なる方向で進化してきたので、それらはあまり得意じゃないですね。またSandboxという点も、k8sやクラウドではマルチテナントになる可能性が高く、Sandboxとしてアプリケーションが隔離/制限されているのは非常に重要です。JavaにもSecurityManagerがありますがApplet亡き今、あまり使われる事は無いですし、なによりコミュニティレベルでサンドボックスを前提にしてる分けでもないので、これらはJavaではなくWASM/WASIを選ぶ大きな動機になります。
いずれにしてもWrite once, Run anywareの点だけでJavaとの競合と考えるべきではないでしょう。
コンテナへと至る道 - ランタイム環境の抽象化
さて、ここまでWASM/WASIの話をしてきたので、もう一つの軸であるコンテナの話をしましょう。なお、ここでいうコンテナは特に明言がなければOCIコンテナ(Dockerコンテナ)の事を指しています。OCIコンテナは必ずしもLinuxだけをサポートする訳ではありませんが、表題ではイメージしやすいようにLinuxコンテナ、という言い方をしています。
DockerはVMwareのようなHWの仮想化よりも一段抽象度の高いプロセスのレベルでアプリケーションランタイムを抽象化する技術です。本番環境ではk8s(ないしはそれをベースにしたサーバレス環境)とセットで使う事が多いと思います。Dockerに関しては以前記事にまとめているので良ければご覧ください。
古の時代には我々はベアメタルマシンに直接アプリケーションをデプロイしていました。物理マシンは性能の全てを使い切る事が出来ますが、相乗りさせると他のアプリとリソース分離が難しく時代とともにスペックを余らせるケースも増えてきました。そして何よりHWを購入して納品してラックに設置してOSやMWのインストールを実施する必要がありリソースの増減が極めて面倒なインフラでした。
次にVMWareのようなハイパーバイザベースの仮想化が普及しました。これはH/Wが仮想化されています。これによりベアメタルよりも柔軟なリソース構成が組めるためH/Wのリソース効率も高くなり何より(プールがあれば)VMをElasiticに増減出来るようになったのが魅力的でした。この流れはクラウドの登場により不可逆になります[3]。また、この頃より相乗りは辞めてアプリ毎に環境(VM)を作る流れが強くなったと思います。必要な必要なリソースに絞れるのでOSの上での相乗りは不要ですからね?
そして現在はコンテナが普及してきています。これは実行するプロセス毎にユーザランドや名前空間を分離し実行します。言うなれば OSの仮想化[4] ですね。
カーネルはホストと同一[5]になりますが、DebianでもFedoraでもAlpineでもゲストとして同時に実行する事が出来ます。VMでもリソースを柔軟に使う事は出来たのですがアプリケーションランタイムとしてみた場合にはアプリ毎にOSのリソースを消費して無駄が多く、起動時間等も相応にかかるのでテスト環境の自動払い出しやオートスケールでは実行のオーバーヘッドが大きかったのでそこにコンテナが上手くはまった形です。コンテナ毎にディストリやその中のディレクトリ構成やパッケージ構成を自由に作れたフレキシビリティも大きな魅力でした。
このようにアプリケーションのランタイム環境は徐々に抽象度を上げていき、管理単位がHWからOS、そしてアプリ(コンテナ)へと変化していったのが近年のトレンドです。
クラウドネイティブ - コンテナ活用の成熟と課題
Dockerの普及当初はVagrantのようなVMの単純な代替として考えられていたため、移植されるアプリも既存のアプリや設計をそのまま持ってきており、ローカルファイルとしてログや設定ファイル様のディレクトリや特別な権限を作成したり、JavaなんかだとWebLogicやGlassFishなどのM/Wのインストールなどを行っていました。Dockerは ユーザランドを自由に変更 出来たので、好きなディストリ、好きなパッケージシステムで任意の構成を作れました。
ImageMagicとか特定ライブラリを入れるのも自在です。この 「Linuxで出来る事なら大体なんでもできる」 というのはDokcerの大きな魅力で普及の糧になったのは間違いないありません。
一方で、コンテナを利用したアプリケーションのデザインはクラウドネイティブのキーワードと共に成熟してきました。The Twelve-Factor Appに従う形で、設定ファイルは環境変数になり、ログファイルは標準出力になり、ファイル入出力も様々なDBやオブジェクトストレージへと変化していきました。
こうした動きの中で、個々のアプリケーションに特化した特殊な構成のイメージは不要になり、OSや言語ランタイム標準の構成を使ったり、Alpineなどの軽量なディストリを使う傾向に変わっていきました。
不要なユーザランドとセキュリティ
先ほど、ユーザランドを自由に弄れるのがDockerのメリットだと書きましたが、クラウドネイティブによりコンテナの中身がシンプルになった結果、デメリットの方が目立つようになります。
成熟したクラウドネイティブアプリにとってはプログラムの実行以外は不要な部分です。しかしながらLinuxに由来する部分もコンテナにあるので、 それらの脆弱性の管理 も必要になります。
log4jなどアプリケーションに由来する脆弱性は当然デベロッパーの管轄ですが、コンテナではlibcやopensslをはじめとしたLinuxやインストールされているパッケージの脆弱性もアプリケーションデベロッパーの管轄になります。従来はインフラエンジニアの領分だった所が少しあいまいになるわけですね。
これらの負担を小さくするために コンテナのセキュリティ というキーワードではコンテナの脆弱性スキャンはトレンドの一つです。Docker for Desktopにも付いてますしjFrogの脆弱性スキャンやPrisma CloudやAqua SecurityのようなCSPM(Cloud Security Posture Management)もコンテナイメージの脆弱性スキャンに対応しています。
これは静的なセキュリティスキャンがDevSecOpsなどトレンドに乗ってコモディティになったのもありますが、放置されがちなイメージのアプリ以外の部分がセキュリティリスクとして高いと考えられたためでもあると思います。
Scratchやdistrolessの利用
アプリケーション以外の脆弱性が問題になるなら、まず最初に思いつくのは余計なものが入っていない軽量なイメージを作る事ですね? そんなわけでAlpine LinuxなんかがUbuntuやRocky Linuxなど比べても人気です。そして、更なる軽量化を求めてLinux From Scratch (LFS) で作った言語ランタイムやGoogleのdistrolessなどaptやyumの様なパッケージマネージャや場合によってはbashなども含まない非常に小さなイメージを作ることもあります。
単純にイメージの容量が小さくなるのも良いことですが、ここでのポイントはそれ以上にセキュリティです。
Dockerfileからの脱却
Dockerfileはとても良いもので、当時、冪等など気にせずにLinuxコマンドをそのまま書けば良いのでChefやAnsibleよりも楽だと感じたものです。非常に高いフレキシビリティと手に馴染んだシェルスクリプトのような記述性を持っています。
しかしながら、先ほど話したようにセキュリティやクラウドネイティブなアプリに限れば話は変わってきます。Dockerfileは非常に自由度が高いので何でも書けますが、それ故にイメージのサイズやセキュリティと言った品質は開発者次第になってしまいます。定番のaptやyumのキャッシュを消す事からマルチステージビルドなど様々なベストプラクティスがありますが、開発者はそれを学ぶ必要があり単にアプリを配置したいだけにしては過剰です。そこで登場してくるのがJibやBuildpacksです。これを使うとDockerfileが不要になるので開発者の責務が小さくなります。
例えばBuildpacksではソースコードから適切なビルダーを判定し、Scratchやdistrolessのようなセキュアでコンパクトなランタイムにアプリケーションを配置したイメージを作成します。これは単に手間を省くだけでは無く、ランタイムの責務がベンダーや(社内等の)カスタムビルダーの管理者になります。個々の開発者が実施するよりも品質が安定しやすくなりますよね。
その顕著な例がBuildpacksをベースにしたVMware(旧Pivotal)のTanzu Build Serviceで、ベースイメージに脆弱性があれば自動的に更新する機能もあるようです。
ref: https://blogs.vmware.com/vmware-japan/2021/08/tanzu-build-service-1-2-1-install.html
もちろんMySQLなどミドルウェア等の導入には継続してDockerfileを使えば良いのですが、クラウドネイティブなアプリの場合はこうした脱Dockerfileの動きも始まっています。
4番目の選択しとしてのWebAssembly
せっかく成熟したクラウドネイティブアプリを作ったのにこんな苦しい思いをするなら、いっそのことLinux部分を無かったことに出来ないか?と思うのは自然な流れですよね。そこで目を付けられたのがWASMです。
ここで改めてWASMの特徴をみてみましょう。
- Portable
- Language Free
- Lightweight
- Sandbox / Secure
これらはDockerの特徴にも似ており、かつユーザランドを含まないのでよりセキュアで軽量な運用を実現する事が可能そうです。Sandboxという点もコンテナにおけるgVisorやFirecrackerのような追加のレイヤーは不要でネイティブですし。そのため、コンテナに続く4番目の選択肢として注目されているわけです。
ref: https://wasmlabs.dev/articles/docker-without-containers/
たとえばユーザランドの管理に注目して共有責任モデルを描いてみると以下のようになります。
これは主にクラウドをイメージしたものですが、緑色がクラウドなどのベンダー、赤色が自分達です。物理サーバは全てを自分で管理する必要がり仮想サーバになるとOSより上が自分たちの責務になります。コンテナではユーザランドより上になり、WASM/WASI ではApplicationのみになります。もうlibcやopensslのバージョンを気にする必要は無いのです!
実はこれPaaS/FaaSと同じレベルの責任範囲ですよね。IaaS, CaaSを経て人類はPaaS/FaaSへの道を再度模索していると考えると中々面白いです。The Twelve-Factor Appを出した当時のHerokuやGAEを出した当時のGoogleに世の中が追い付いてきた、という話かもしれないですね。
とはいえ「もうコンテナはオワコン! これからはWASMしか勝たん」という分けではもちろんなく、用途毎に使い分ける必要があります。
DBやビッグデータ基盤などは性能の出しやすいベアメタルが選択肢になりますし、仮想環境は最もニュートラルな選択肢であり続けるでしょう。ミドルウェアや従来型のアプリは依然コンテナが多いと思います。クラウドネイティブなアプリケーションはWASMが良いケースも多いですが、ネイティブの実行速度には一歩ゆずるケースもあるのでピーク性能を重視するならコンテナ、というケースもあるかもしれません。ツールの成熟で変わってくる部分も多いでしょうが、いずれにしても何らかの使い分けが必要になってきます。
オーケストレーション - docker/k8sエコシステムとの統合
現在、本番環境で利用されてるのは基本的には単体のDockerではなくk8sです。これは当然でDockerコンテナエンジンだけではラインタイムに過ぎなくて、それを実際に本番運用するにはスケジューラやデプロイの仕組み、スケールの仕組み、監視の仕組み、その他もろもろのオーケストレーションを始めとしたエコシステムが必要になってきます。これを1から構築するのは非常に大変で、k8sもApache MesosやDocker Swarmとの競争を勝ち抜き、その上でエコシステムが確立されていきました。
WASMを本番活用する場合も同じですが、もっとも手っ取り早いのは既存のエコシステムに乗る事です。Docker社のアプローチはまさにそれでcontainerdのバックエンドでWASM対応をしています。
Dockerの内部は大きく分けると高レベルなAPIであるcontainerdとより低レベルなAPIであるruncに分かれます。今回Dockerは図のようにcontainedの裏にruncと並列にcontainerd-wasm-shimとwasmedgeを置くことでWASM対応をしています。これにより既存のコンテナのエコシステムはcontainedに対して操作を行っているのでシームレスに既存のエコシステムと統合する事が出来ます。
実際にdocker-composeを使って 「MySQLはコンテナで動かし、アプリはWASMで動かし、それを連携させる」 と言った構成も実現できます。これはk8s対応も同じで、マイクロソフトはAKSでのWASMサポートをKrustletからcontainerd-wasm-shimに切り換えています。
とはいえk8s以外のオーケストレーションも淡々とその地位を狙っていて、HashicorpはNomadのWASM対応で次の覇者を狙っています。
もう一つの方向性 - Cloudflare Nanoservices
コンテナ界隈とは似て非なる道でサーバサイドのWASM/WASIを進めているのがCDN Edge界隈です。その中でも面白いのがCloudflare Nanoservicesとなります。名前がBytecode AllianceのNanoprocessに似ていますが特に関係は無さそうです。
現在、マイクロサービスという形でモノリスなアプリケーションを分解し、個々の機能を独立したサービスとして捉え、API通信により協調させる分散アプリケーションがトレンドの一つになっています。マイクロサービス化により独立したデプロイやリソース管理、あるいはアーキテクチャ設計が出来るので保守性を上げ開発生産性を向上させると言われています。一方で分散アプリケーション特有の数々の課題を飲み込む必要があり、その一つがAPI通信のコストです。従来は関数呼び出しだったものがNW通信になるので性能は格段と堕ちます。CPU内やインメモリの呼出しがネットワーク越しになると数倍遅い程度では無い場合もあります。そのためHTML5のWebWorkerに着想を得てV8を改造したIsolationやそれをベースにしたWorkerdとNanoservicesの構想を発表しています。
これはマイクロサービスのように独立したデプロイを行いながら、同じローカルに配置し同一プロセスで動作させるため関数呼び出し並みのゼロコスト連携を実現するためのものです。可能な限り共有できるリソースは共有し、グローバル変数等のスコープはIsolationという形で分離するようです。CloudflareのようなCDN EdgeはAWSやGoogleのような巨大なデータセンタでは無く、世界中に分散した大小さまざまなインフラで実行する必要があるため、この高集約性が必要なようです。
FastlyもCompute@EdgeではJSを直接実行せずWASMでJSエンジンを作って実行するなど、CDN Edge界隈やNode.js/DenoといったJSの実行基盤の派生形はコンテナ系とはまた違う方向性でWASM/WASIを使おうとしているようにも見えて、同じサーバ向けWASM/WASIでも似て非なるアプローチなのがなかなか面白いです。ちなみにそっちはそっちで標準化団体作ってるみたいですね。WASM/WASI向けという分けでは無いですが。
まとめ
まとめですが大きな流れとしては以下のようになります。
- コンテナ界隈の成熟と共にLinux由来のユーザランドの管理が足枷に
- WASM/WASIを使う事でアプリケーションとインフラをより明確に分離
- WASMをDocker/k8sのエコシステムに統合したりCDN Edgeのより高集約な基盤として利用
特にコンテナ目線ではPaaSの再来という感じもしますが、コンテナでサーバレスに慣れ、クラウドネイティブで適切な設計が出来る今だからこそ、改めてPaaS的なアプローチに回帰したという側面もあると思っていて、10年前だとWASM/WASIがここまで注目されなかっただろうと思うと何事もタイミングは大事だなー、と思いますね。まだまだ本番運用に耐えうるとは言い切れない部分もあると思いますが、今後の動きがとても楽しみです。
また今回、本記事や動画を作成するにあたってWASM/WASIをいろいろ勉強したのですが、なかなか面白いですね。サーバサイド実行では無くアプリに組込む使い方も面白そうなので、引き続き調べたり使ったりして行きたいと思います。
それでは、Happy Hacking!
参考
Discussion