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

Pod の YAML が runc に届くまで

Pod の設定は、コンテナを動かすプログラムへどう渡るのでしょうか。CRI と OCI の仕様を、実際の設定から読みます。

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

Pod の設定に「メモリは 256 MiB まで」と書くと、ノードでその上限が設定されます。 その値は、kubelet からコンテナを動かすプログラムへ、さらに Linux の設定へと渡されていきます。

この受け渡しには、共通の書式や呼び出し方があります。 この章では、Kubernetes からの依頼を扱う CRI と、コンテナの実行やイメージの形式を定める OCI の仕様 を、設定の中身を見ながら整理します。

GOAL この章のゴール

  • CRI と OCI の仕様が、それぞれ何を決めているか分かる
  • コンテナを起動する設定ファイルを作って読める
  • イメージの情報とファイルの層の関係を説明できる

2 つの取り決め

CRI と OCI。kubelet と containerd の間が CRI (gRPC)、containerd と runc の間が OCI Runtime Spec (bundle)kubelet は RunPodSandbox / PullImage / CreateContainer / StartContainer を CRI で呼ぶ。containerd は OCI Image Spec のイメージを展開して rootfs を作り、OCI Runtime Spec の config.json を生成し、runc を実行する。kubeletPod spec を持つcontainerdCRI plugin + snapshotterruncOCI RuntimeCRIgRPC / UNIX socketOCIbundle (ディレクトリ)CRI の主な RPCRunPodSandboxPullImageCreateContainerStartContainerListContainersExec / Attach / PortForwardStopPodSandboxcontainerd がやることイメージを pull して層を OverlayFS で展開netns を作り CNI を呼び、pause を起動 (sandbox)config.json を生成してshim 経由で runc を実行runc が受け取るものbundle/├─ config.json└─ rootfs/process.argslinux.namespaceslinux.resources (cgroup)mounts / hooks
図 1左が CRI、右が OCI。containerd はその両方を話す翻訳者。

前の章の crictl は、kubelet が containerd に出すのと同じ呼び出しを手元から送るコマンドでした。 その呼び出しの取り決めが CRIシーアールアイContainer Runtime Interface。kubelet がコンテナランタイムに命令するための gRPC API。RuntimeService と ImageService の 2 つからなり、UNIX ソケット越しに話す。用語集で見る です。 Kubernetes のドキュメントは CRI を「kubelet とコンテナランタイムの間の通信を定める gRPC プロトコル」と説明しています。 「Pod の sandbox を作れ」「このイメージを pull しろ」「コンテナを起動しろ」という命令を protobuf で定義したもので、containerd や CRI-O は、この API を実装するサーバーです。

containerd が runc に命令するときの取り決めは、OCIオーシーアイOpen Container Initiative。2015 年 6 月に Docker や CoreOS などが Linux Foundation の下に作った、コンテナの仕様を決める団体。Runtime Spec / Image Spec / Distribution Spec の 3 つを管理する。用語集で見る という団体が決めています。 OCI の仕様は 3 つあります。 Runtime Spec はコンテナを 1 つ起動するための入力形式、Image Spec はイメージの形式、Distribution Spec はレジストリの HTTP API です。 runc は、OCI の設立時に Docker が寄贈した Runtime Spec の実装で、レジストリと containerd は Image / Distribution Spec の実装です。

CRI OCI Runtime Spec
決めた人 Kubernetes (SIG Node) OCI (Linux Foundation 傘下)
形 gRPC の .proto ディレクトリ + config.json
話す相手 kubelet ↔ 高レベルランタイム 高レベル ↔ 低レベルランタイム
単位 Pod sandbox とコンテナ コンテナ 1 つ
知っていること Pod、イメージ、ログ、exec namespace、cgroup、mount、seccomp

CRI の中身

CRI は 2 つの gRPC サービスからなります。

  • RuntimeService:RunPodSandbox / StopPodSandbox / CreateContainer / StartContainer / StopContainer / ListContainers / Exec / Attach / PortForward / ContainerStats など。
  • ImageService:PullImage / ListImages / ImageStatus / RemoveImage / ImageFsInfo。

前の章の crictl pods に出た Pod は、CRI の中では サンドボックスサンドボックスCRI で Pod の土台を表す単位 (Pod sandbox)。Pod の net namespace や ipc namespace (場合によって pid namespace) を最初に作って持ち続ける pause コンテナのこと。用語集で見る と呼ばれます。 CRI の定義ファイルは RunPodSandbox を「Pod 単位の sandbox を作って起動する」操作と説明しています。 CRI の設計文書によると、sandbox を独立した概念にしたのは、namespace で作るランタイムでは namespace の組が、VM で作るランタイムでは VM が、それぞれ自然に sandbox にあたるからです。 Pod の他のコンテナは、起動時にその sandbox の namespace に参加します。 サンドボックスという語は、この教材の後半では「ホストから隔てた実行環境」という広い意味でも使います。

Pod が起動するまでの CRI 呼び出しの順序。sandbox が先、コンテナは後kubelet はまず RunPodSandbox を呼ぶ。containerd は Pod の netns を作って CNI で veth と IP を付け、pause コンテナをその netns で起動する。そのあと各コンテナについて PullImage、CreateContainer、StartContainer を呼ぶ。containerd はそれぞれを shim と runc への操作に変換する。kubeletcontainerd (CRI)CNI pluginshim + runcRunPodSandbox(config)netns を作り CNI ADD (veth + IP)pause を起動して netns に入れるPullImage(nginx:1.29)CreateContainer(sandbox, config)bundle を作り runc createStartContainer(id)runc startPod sandbox (1 回)コンテナごとに繰り返す (init container → 通常コンテナ)kubelet は CRI しか知らない。CNI と runc を呼ぶのは containerd。
図 2Pod 起動時の CRI 呼び出し。sandbox が 1 回、そのあとコンテナごとに PullImage → CreateContainer → StartContainer を繰り返す。

kubelet は、まず RunPodSandbox を呼びます。 containerd のドキュメントによると、containerd は Pod の net namespace を作って CNI プラグインでそこに veth と IP を付け、その namespace で pause を起動します (Part 2 で見た CNI の仕事はここで起きています)。 IP が決まってから、kubelet は各コンテナの CreateContainer で「この sandbox に入れ」と指定します。 全コンテナが pause の net namespace に入るので、Pod 内のコンテナは同じ IP を持ちます。

CreateContainer の引数 ContainerConfig には、イメージ名、コマンド、環境変数、マウント、linux.security_context (capabilities、seccomp、user namespace など)、リソース上限が全部入っています。 kubelet が Pod の YAML から組み立てたものです。

OCI Runtime Spec の bundle と config.json

containerd は CreateContainer を受けると、bundle を 1 つ作ります。 Runtime Spec の言葉では、bundle はディレクトリで、中身は直下の config.json と、root.path が指す rootfs のディレクトリです。 runc は、そのディレクトリを指定した runc create <id> で呼ばれます。

runc の README は、この bundle を使う方法を 2 つ挙げています。 runc run <id> は作成、起動、後始末を一度に行い、runc create / runc start / runc delete は段階ごとに分けて呼びます。 後者は create と start の間に別の作業を挟みたいときのための分け方で、containerd の shim はこちらを使います。

config.json の雛形は runc spec で作れます。 Web ターミナル に runc が入っているので、ファイルを作るところまでは試せます (gVisor 上の非 root なので、起動まではできません)。

OCI bundle の雛形を作る
mkdir -p /tmp/bundle && cd /tmp/bundle && runc spec --rootless
jq '{args: .process.args, root: .root, namespaces: [.linux.namespaces[].type]}' config.json

出力例

{
  "args": ["sh"],
  "root": { "path": "rootfs", "readonly": true },
  "namespaces": ["pid", "ipc", "uts", "mount", "cgroup", "user"]
}

--rootless を付けたので user namespace が入り、network が抜けています (runc のソースでは、rootless 用の雛形を作るときに network namespace を外して user namespace を足しています)。 --rootless を外した通常の雛形では network が入り user が抜けます。 手作りコンテナの章でトグルを切り替えたあの組み合わせが、JSON の配列として書いてあります。

config.json には、ここで見た項目のほかに、仕様のバージョンを示す ociVersion、コンテナの作成前後に実行するコマンドを指定する hooks、追加のマウントを列挙する mounts があります。 仕様は、mounts を列挙した順に mount するよう定めています。

config.json の主な項目と、それが対応する Linux の機能OCI Runtime Spec の config.json の各セクションは、runc が呼ぶシステムコールか cgroup ファイルに 1 対 1 で対応する。config.json の項目runc がやることprocess.args / env / cwdexecve()何を動かすかroot.pathpivot_root()rootfs の場所 (新しい root mount)linux.namespaces[]clone() / setns()path 無し: 作る、path 有り: 参加linux.resources/sys/fs/cgroup/…cpu.max / memory.maxmounts[]mount()/proc /dev /sys、volumeprocess.capabilitiescapset()CAP_NET_BIND_SERVICE …linux.seccompseccomp()許可する syscall の一覧hooks(ユーザー空間)createRuntime / poststop などkubelet の Pod spec → containerd がこの JSON に翻訳 → runc がカーネルに命令する
図 3config.json の項目は、runc が呼ぶシステムコールか cgroup のファイルに対応する。
seccomp と capabilities を見る
jq '.process.capabilities.bounding, (.linux.seccomp // "seccomp: なし (全 syscall 許可)")' config.json
jq '.mounts[] | select(.destination | test("^/(proc|dev|sys)$")) | {destination, type}' config.json

capabilities は root の権限を細かく分けた Linux の権限で、たとえば CAP_NET_BIND_SERVICE は 1024 未満のポートを開く権限です。 seccomp は、プロセスが呼べるシステムコールを絞る仕組みです。 runc spec の雛形では capabilities が CAP_AUDIT_WRITE / CAP_KILL / CAP_NET_BIND_SERVICE の 3 つだけで、seccomp プロファイルは付いていません。 containerd は、kubelet から受けた securityContext を元にここを埋めてから runc に渡します。 Pod で privileged: true を書くと、CRI の security_context に privileged が立ち、containerd はその指定を受けて capabilities の制限を外した config.json を作ります。 特権コンテナも、同じ runc に違う config.json を渡して作ります。

OCI Image Spec: イメージの形式

OCI Image Spec の構造。index → manifest → config と layers。全部 digest (内容の sha256) で参照し合うタグは index (複数アーキテクチャ) を指し、index が platform ごとの manifest を指し、manifest が config (環境変数や entrypoint) と層 (tar.gz の blob) を sha256 で指す。nginx:1.29タグ (付け替え可能)解決index (manifest list)platform ごとの manifest 一覧linux/arm64manifestsha256:3f2a…configEnv / Entrypoint / Cmd+ diff_ids (展開後の層の digest)layers[]layer 3 sha256:9c1d… (gzip)layer 2 sha256:77be… (gzip)layer 1 sha256:a1b2… (gzip)pull → 展開ノード上の snapshotter (OverlayFS)/var/lib/containerd/ io.containerd.snapshotter.v1.overlayfs/ snapshots/1/fs ← layer 1 を tar 展開 snapshots/2/fs ← layer 2 snapshots/3/fs ← layer 3同じ digest の層は 1 回しか置かない(他のイメージと共有)コンテナ起動時に lowerdir に並べるタグは動くが digest は動かない。再現性が要るならimage@sha256:…で指す。
図 4イメージを構成する manifest、config、層は、互いに内容の sha256 (digest) で指し合う。付け替えられるのはタグだけ。

docker pull が層ごとにダウンロードする様子は最初の章で見ました。 その層の形式と、層をまとめる台帳の形式を決めているのが OCI Image Specオーシーアイイメージスペックコンテナイメージの形式。manifest (層と config の一覧)、config (環境変数や entrypoint)、層 (tar の blob) を、すべて sha256 digest で参照する。用語集で見る です。 イメージは 3 種類のオブジェクトでできています。

  1. manifest:「この config と、この層たち」という一覧。仕様では layers の先頭がベースの層で、以降を順に重ねた結果が最終的なファイルシステムです。マルチアーキテクチャの場合、タグはまず index (manifest list) を指し、index が platform ごとの manifest を指します。
  2. config:Env、Entrypoint、Cmd、WorkingDir、そして各層の展開後のハッシュ (diff_ids)。docker inspect で見える情報の大半はこれです。
  3. layers:層ごとの tar (gzip か zstd で圧縮)。前の層からの差分だけが入っています。

これらは全部、中身から計算した sha256 のハッシュ値 (digest) で参照されます。 そのため同じ digest の層は 1 回しか保存されず、改ざんすれば digest が変わって検出されます。 タグは、後から別の digest に付け替えられる「名前 → digest」の対応です。 Kubernetes のドキュメントによると、imagePullPolicy を省略すると、タグが :latest か省略のときは Always、それ以外は IfNotPresent になります。 つまり image: nginx:1.29 は、ノードにすでに同名のイメージがあれば、レジストリで解決し直されません。 同じドキュメントは、毎回同じ内容を動かしたいなら image@sha256:… の digest で指すよう勧めています。

Distribution Spec: レジストリの API

レジストリの HTTP API を決めているのが Distribution Spec です。 GET /v2/<name>/manifests/<reference> (reference はタグか digest) で manifest を、GET /v2/<name>/blobs/<digest> で層や config を取ります。 docker pull も containerd の PullImage も、やっていることはこの 2 種類の GET の繰り返しです。 manifest を取るときは、受け取れる形式を Accept ヘッダで伝えます。

レジストリに直接聞いてみる
curl -s "https://registry.k8s.io/v2/pause/manifests/3.10.1" \
  -H "Accept: application/vnd.oci.image.index.v1+json" | jq '.manifests[] | {platform: .platform.architecture, digest}'

出力例

{ "platform": "amd64", "digest": "sha256:…" }
{ "platform": "arm64", "digest": "sha256:…" }

ふりかえり

Qkubelet が Pod を起動するとき最初に呼ぶ CRI の RPC は?

sandbox (pause と net namespace) が先です。CNI で IP を付けてから、各コンテナをその sandbox に入れます。

QOCI bundle の中身として正しいのは?

Runtime Spec の bundle は rootfs ディレクトリと config.json だけです。manifest と層は Image Spec の話で、containerd が展開して rootfs にします。

Q同じ image:tag を指定しても別の内容が動くことがある理由は?

タグ以外は全部内容のハッシュです。固定したいなら image@sha256:… で指定します。

参考