Kubernetes 解体新書Part 3 ランタイム

他人のコードを動かすときの隔て方

投稿されたプログラムや AI が生成したコードをどう分離して動かすか。通常のコンテナ、gVisor、VM などの方式を比べます。

  • L1 Web ターミナル
  • 更新 2026年10月6日

学習サイトに投稿されたプログラムや、AI エージェントが生成したコードを実行したい。 そのコードが意図せずファイルを壊したり、ホストの不具合を突いたりしても、ほかの利用者に影響させたくありません。

この章では、実行するコードとホストの間をどう分離するかを比べます。 通常のコンテナから gVisor や VM を使う方式まで、何を共有し、どこで処理を受け止めるかを図で見ていきます。

GOAL この章のゴール

  • 通常のコンテナで、ホストと共有している部分を説明できる
  • gVisor や VM を使う分離方式の違いと、追加の負担が分かる
  • Pod ごとにランタイムを選ぶ設定方法が分かる

runc の穴はホストの穴

アプリとホストの間に壁を挟んで隔てた実行環境を、一般に サンドボックスサンドボックスアプリをホストから隔てて動かす実行環境。第 4 章では CRI で Pod の土台を指す語として出たが、ここではホストから隔てる壁を持つ環境一般を指す。用語集で見る と呼びます。 壁の強さの段階を、この章では 分離レベルぶんりレベルアプリのシステムコールがホストカーネルに届くまでに、何がどれだけ挟まるかで表す、隔て方の強さ。この教材での呼び方。用語集で見る と呼び、runc のコンテナを一番弱い段として、挟むものを足すごとに強くなる、という順で比べます。

runc、gVisor、Kata Containers の違い。アプリとホストカーネルの間に何が挟まるかrunc: アプリの syscall がホストカーネルに直接届く。gVisor: Sentry がユーザー空間で syscall を受け、ホストへは絞った syscall だけ。Kata: ゲストカーネルを破っても VM の中で、KVM を越えてからホストに届く。runcホストカーネルnamespace / cgroup見せる一覧を分けるだけアプリsyscall が直接届くgVisor (runsc)ホストカーネル届く syscall は少数seccomp フィルタSentry (Go 製カーネル)systrap / KVM で横取りアプリSentry を Linux だと思うKata Containersホストカーネル + KVMVMM (QEMU / CH / FC)仮想デバイスゲストカーネルVM 1 つ = Pod 1 つアプリ (+ kata-agent)間に挟むもの: なしカーネルの穴 = ホストの穴挟むもの: SentrySentry を破ってもまだ seccomp挟むもの: VMゲストを破っても VM の中
図 1アプリとホストカーネルの間に何が挟まるか。runc では何も挟まらない。

runc のコンテナでは、アプリのシステムコール (syscall) はホストカーネルに直接届きます。 namespace が分けているのはカーネルが各プロセスに見せる一覧 (プロセス、NIC など) で、システムコールを受け取るのは同じホストカーネルです。 そのため、カーネル自体に脆弱性があれば namespace は守ってくれません。 もう 1 つの経路が、ランタイム自身の穴です。 runc はホストの root で動くので、runc のバグは root の権限で悪用できます。

挟むもの 1: ユーザー空間で動くカーネル (gVisor)

gVisor の構造: アプリの syscall は Sentry が受け、ファイルは Gofer 経由、ホストには絞られた syscall だけが届くアプリのプロセスが発行した syscall を platform (systrap / KVM) が横取りして Sentry に渡す。Sentry は Go で書かれたカーネル実装で、自分で処理するか、必要なら限られたホスト syscall を呼ぶ。ファイルアクセスは Gofer という別プロセスが 9P で代行する。gVisor の範囲 (runsc が作る)アプリ普通の Linux バイナリsyscallplatformsystrap (既定) / KVM横取りSentryGo 製のアプリケーションカーネル絞られた syscall (seccomp)ファイル: Gofer に頼む (9P)Gofer別プロセスホストの Linux カーネルSentry と Gofer からの、ごく少数の syscall しか受けない
図 2gVisor。アプリの syscall は Sentry が受けて処理し、ホストには seccomp で絞られた少数の syscall しか届かない。

gVisorジーバイザーGoogle 製の、アプリをホストから隔てて動かすランタイム。Sentry という Go 製のアプリケーションカーネルがアプリの syscall を受けて処理し、ホストカーネルに届く syscall を絞る。OCI ランタイムとしての名前は runsc。用語集で見る は、アプリとホストカーネルの間に「もう 1 つのカーネル」を置きます。 gVisor のドキュメントはこれを「Linux に似たインターフェースを実装するアプリケーションカーネル」と呼び、Sentry という Go 製のプログラムが、ホスト上では普通のユーザー空間プロセスとして動きます (VM は起動しません)。

アプリが syscall を発行すると、platform と呼ばれる部分が横取りして Sentry に渡します。 Sentry は Linux の syscall の大部分を Go で再実装しており、ドキュメントは「gVisor はアプリの syscall をホストに素通しすることは決してない」と述べています。 Sentry 自身がホストに出す syscall は seccomp で絞られていて、たとえば exec や connect は禁止されています。 ファイルアクセスは Gofer という別プロセスが 9P プロトコルで代行します。 アプリから見れば Linux そのものですが、カーネルの穴を突こうとしても相手は Sentry で、その先のホストカーネルには届きません。

代償は 2 つあります。 syscall のたびに横取りと Sentry での処理が挟まるので、gVisor のドキュメントによると、syscall が多いワークロード (高性能なデータストアや静的なネットワークサービス) が最も影響を受けます。 もう 1 つは互換性で、Sentry は Linux の syscall の一部だけを実装しているので、未実装の syscall を使うアプリは動きません。

ターミナルの実行環境 が gVisor なので、中から Sentry の痕跡を見てみます。

gVisor の中から見る
uname -r
dmesg | head -8

出力例

4.4.0
[   0.000000] Starting gVisor...
[   0.123456] Checking naughty and nice process list...
[   0.234567] Waiting for children...

uname -r が返す 4.4.0 は Sentry が名乗っているバージョンで、ホストのカーネルとは無関係です。 dmesg の中身も Sentry が自分で書いたメッセージで、ホストカーネルのログは一切見えません。

挟むもの 2: microVM (Kata Containers と Firecracker)

Kata Containers と Firecracker: Pod ごとに microVM を立て、ゲストの中で agent がコンテナを起動するcontainerd-shim-kata-v2 が VMM (QEMU / Cloud Hypervisor / Firecracker) を起動し、ゲストカーネルの中の kata-agent と vsock で話す。agent がゲスト内で namespace と cgroup を作ってコンテナを起動する。OCI bundle は virtio-fs でゲストに mount する。containerdttrpccontainerd-shim-kata-v2VM を 1 つ起動する起動VMMQEMU / Cloud Hypervisor / Firecrackervsock (ttrpc)microVM (Pod 1 つ分)kata-agentゲスト側でコンテナを起動する (Rust 製)pauseappゲストカーネルKata 用に小さく設定済みvirtio-fs で bundle を渡すホストカーネル + KVMFirecracker は Rust 製で、エミュレーションするデバイスは 5 つ。125 ms 未満で起動する
図 3Kata Containers。Pod ごとに microVM を立て、ゲストカーネルの中で kata-agent がコンテナを起動する。VMM は QEMU、Cloud Hypervisor、Firecracker などから選べる。

Kata ContainersカタコンテナーズPod ごとに軽量 VM を起動し、その中でコンテナを動かす OCI ランタイム。Intel の Clear Containers と Hyper の runV が合流して 2017 年 12 月に発表された、OpenInfra Foundation のプロジェクト。用語集で見る は、CPU の仮想化機能 (KVM) を使って、アプリとホストカーネルの間に VM を挟みます。 Kata のアーキテクチャ文書によると、VM は Pod (sandbox) ごとに 1 つで、中では Rust 製の kata-agent が常駐し、ホスト側の shim と vsock 越しの ttRPC で話してコンテナを起動します。 containerd から見ると containerd-shim-kata-v2 という普通の shim で、shim が VM の起動と agent との通信を隠しています。 OCI bundle は virtio-fs で VM の中に mount されます。

ゲストカーネルの脆弱性を突いても、そこは VM の中です。 ホストに届くには、さらに KVM と VMM の穴を突く必要があります。 これは、EC2 のインスタンス同士を隔てているのと同じ種類の隔て方です。 代償は VM 1 つ分の起動時間とメモリで、どの VMMブイエムエムVirtual Machine Monitor。KVM の上で VM のデバイスやメモリを用意するユーザー空間プログラム。QEMU、Cloud Hypervisor、Firecracker など。用語集で見る を選ぶかで大きく変わります。

VMM の中で最も小さい部類が FirecrackerファイアクラッカーAWS が Rust で書いた VMM。エミュレーションするデバイスを 5 つに絞り、125 ms 未満で起動する。AWS Lambda と Fargate の基盤で、Kata の VMM としても使える。用語集で見る です。 QEMU が持つ膨大なデバイスエミュレーションを捨て、virtio-net、virtio-block、virtio-vsock、シリアルコンソール、最小限のキーボードコントローラの 5 つしか持ちません。 その分、公称では VM が 125 ms 未満で起動し、1 台のホストで毎秒 150 個の microVM を作れ、VM 1 つあたりのメモリの上乗せは 5 MiB 未満です。 Firecracker の README によると、AWS Lambda と Fargate のために AWS で開発され、Kata Containers と組み合わせて使えます。

挟むもの 3: Unikernel

ゲストカーネルを小さくする方向をさらに進めると、Unikernelユニカーネルアプリと、それが使うカーネル機能だけをリンクして 1 つのイメージにしたもの。VM として起動するが、汎用カーネルを持たないので小さく速い。用語集で見る になります。 アプリに必要なカーネル機能だけをライブラリとしてリンクし、それだけを VM として起動します。 使わない機能は最初から存在しないので、攻撃に使える部分が少なく、イメージも小さくなります。

代償は互換性です。 普通のコンテナイメージはそのままでは動かず、アプリを Unikernel 向けにビルドし直す必要があります。 Kubernetes で動かすための OCI ランタイムとしては urunc があり、Unikraft、Rumprun、MirageOS などのフレームワークで作った Unikernel を、QEMU や Firecracker で起動します。 urunc は自らを「Unikernel のための runc」と位置付けていて、CRI 経由で containerd から使えます。

隔て方と起動コストを並べる

スライダで「他人のコードをどこまで警戒するか」を決め、点を押して説明を読んでください。 緑の輪は、その条件で最も起動が軽い選択です。

ホストカーネルとの隔て方と起動オーバーヘッドの散布図共有カーネルユーザー空間VM10 ms100 ms1000 ms起動オーバーヘッドの目安 (対数、環境で大きく変わる) →隔て方 →crunruncgVisor (runsc)Unikernel (urunc)FirecrackerKata (QEMU)

runc (ホストカーネルを共有。namespace + cgroup)

OCI Runtime Spec の参照実装。containerd の既定。

普通のイメージとの互換: そのまま動く。点の大きさは互換性の高さ

レベル 1 を満たす中で最も起動が軽いのは crun (緑の輪)。点を押すと説明が出ます。 数字は桁の目安で、VMM やカーネル、ディスクで変わります。

観点何も挟まない (runc / crun)間に挟む (gVisor / Kata / Firecracker)
向く用途自社のアプリ、信頼できるコード他人のコード、マルチテナント、FaaS
起動数十 ms100 ms 〜 1 秒
メモリの上乗せほぼ無しSentry 分 〜 VM 1 つ分
イメージの互換そのままgVisor: ほぼ / Kata: そのまま / Unikernel: 専用ビルド
カーネルの穴ホストの穴Sentry かゲストカーネルの中で止まる

AI エージェントのコマンドをどこで動かすか

AI エージェントがリポジトリを読み、生成したコードを実行し、ブラウザーを操作するときも、実際に動くのは OS 上のプロセスです。 利用者ごとのファイルを分け、実行するコードからホストや他の利用者へ届く範囲を制限するために、ここまで見たサンドボックス技術が使われています。

gVisor の公式ブログには、Tencent がエージェントの強化学習向け実行環境で gVisor を使う事例があります。 生成コードを繰り返し動かす用途では、Linux の実行環境との互換性、隔離、環境を大量に作るコストを同時に考える必要があります。 VM を前提にせず、システムコールをユーザー空間のカーネルで処理する方式にも用途があります。

Kubernetes SIGs の Agent Sandbox は、AI エージェント向けの実行環境を Kubernetes のリソースとして管理するプロジェクトです。 低レベルの隔離は RuntimeClass で選ぶ gVisor や Kata Containers に任せます。 公式リポジトリには Kata を使う構成例もあり、環境の払い出しを管理するコントローラーと、プロセスを隔離するランタイムが別の役割を担っています。 構成例が公開されていることと、ある企業が本番で採用したことは区別して読みます。

Kata と組み合わせる VMM の一つが Cloud Hypervisorクラウドハイパーバイザークラウド向けワークロードを対象にした Rust 製の VMM。Kata Containers のゲスト VM を動かす選択肢の一つ。用語集で見る です。 Kata の shim がコンテナの依頼を受け、Cloud Hypervisor が VM を動かし、ゲスト内の kata-agent がプロセスを起動します。 AI sandbox 向け OSS の CubeSandbox も、Cloud Hypervisor や Kata の部品を基に、独自の実行モデルに合わせた実装を公開しています。 これは実装例として確認できるものであり、この資料だけから市場全体での普及率や本番採用社数は判断できません。

Firecracker を使った商用サービスの例には、GMO Flatt Security の Takumi と Fly.io があります。 Takumi は脆弱性を検証するコードを実行するために Firecracker ベースの基盤を使い、Fly.io は Machines や AI エージェント向けの実行環境に Firecracker を使っています。 AWS の関数実行から、エージェントが継続して作業する環境へと用途が広がっています。

これらは同じ層の製品名ではありません。 gVisor はシステムコールを処理する隔離の仕組み、Kata は VM を使ってコンテナを動かすランタイム、Firecracker と Cloud Hypervisor はその VM を動かす VMM です。 どの方式でも、外向き通信の許可先、渡す資格情報、保存データの寿命はサービス側で別に制御します。 VM を挟んでも、渡した API キーで許可される操作まで制限されるわけではありません。

RuntimeClass: Pod ごとに選ぶ

RuntimeClass が Pod から containerd の runtime handler に届くまでPod の runtimeClassName が RuntimeClass オブジェクトの handler 名に解決され、kubelet が CRI の RunPodSandbox に runtime_handler として渡す。containerd は config.toml の runtimes.<handler> を引いて、対応する shim (runc / runsc / kata) を起動する。Podspec: runtimeClassName: gvisor containers: […]名前で参照RuntimeClass (cluster scope)metadata.name: gvisorhandler: runscoverhead / scheduling (任意)kubelet が RunPodSandbox に runtime_handler=runsc を付ける/etc/containerd/config.toml (version = 3)[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc] runtime_type = "io.containerd.runc.v2"[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runsc] runtime_type = "io.containerd.runsc.v1"handler 名 = このテーブルのキー。無ければ default_runtime_nameこの教材のクラスタRuntimeClass は 0 個config.toml にも runc だけ→ 全 Pod が runckubectl get runtimeclassNo resources found
図 4Pod の runtimeClassName → RuntimeClass の handler → containerd の runtimes.<handler>。この教材のクラスタには RuntimeClass が無い。

1 つのクラスタで複数のランタイムを使い分けるための仕組みが RuntimeClassランタイムクラスPod にどのコンテナランタイム設定を使うかを指定するためのクラスタスコープのリソース。handler 名が containerd / CRI-O の設定にある runtime の名前に対応する。1.20 で GA。用語集で見る です。 クラスタ管理者が RuntimeClass オブジェクトを作り、handler に containerd 側の runtime 名を書きます。 Pod は runtimeClassName でそれを参照するだけです。 kubelet は RunPodSandbox に runtime_handler を付けて渡し、containerd は config.toml の runtimes.<handler> を引いて、対応する shim を起動します。 handler は DNS ラベルとして有効な名前で、runtimes.<handler> には runtime_type (gVisor なら io.containerd.runsc.v1) を書きます。 この教材の containerd 2.x では、設定は次の形になります (Kubernetes のドキュメントは 1.x の io.containerd.grpc.v1.cri で書いています。違いは次の章で見ます)。

[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runsc]
  runtime_type = 'io.containerd.runsc.v1'

runtimeClassName を書かない Pod は、既定のランタイムで動きます。 Kubernetes のドキュメントによると、指定した RuntimeClass が存在しないか、CRI がその handler を動かせないときは、Pod は Failed フェーズになります。

overhead に VM 分のメモリと CPU を書いておくと、scheduler がそれを込みで空きを計算します。 scheduling.nodeSelector で「そのランタイムが入っているノード」に限定し、scheduling.tolerations でそのノードの taint を許すこともできます。

RuntimeClass を見る
kubectl get runtimeclass

出力例

No resources found

この教材のクラスタで同じコマンドを打つと、No resources found が返ります。 RuntimeClass オブジェクトが 1 つも無く、containerd の設定にも runc しか無いので、全 Pod が runc で動きます。 gVisor や Kata を足すには、ノードに runsc や kata のバイナリと shim を入れ、config.toml に handler を追加し、RuntimeClass を作る、の 3 手順が要ります。 作るとすれば次の形です (このクラスタでは handler が無いので、これを使う Pod は Failed になります)。

apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: gvisor
handler: runsc
overhead:
  podFixed:
    memory: "64Mi"
    cpu: "50m"

ふりかえり

QgVisor で、アプリのシステムコールに答えるのはどこ?

Sentry はただのユーザー空間プロセスで、アプリの syscall を横取りして自分で処理します。VM ではありません。

QKata Containers で VM が 1 つ作られる単位は?

Pod 1 つに microVM 1 つで、中の kata-agent が複数のコンテナを起動します。shim の単位と同じです。

QPod に runtimeClassName: gvisor と書いたが起動しない。最も疑うべきは?

RuntimeClass がするのは、Pod が書いた名前から containerd の handler 名を引くことだけです。ノード側に handler とバイナリが無ければ sandbox が作れず、Pod は Failed になります。

参考