Pod の設定に「メモリは 256 MiB まで」と書くと、ノードでその上限が設定されます。 その値は、kubelet からコンテナを動かすプログラムへ、さらに Linux の設定へと渡されていきます。
この受け渡しには、共通の書式や呼び出し方があります。 この章では、Kubernetes からの依頼を扱う CRI と、コンテナの実行やイメージの形式を定める OCI の仕様 を、設定の中身を見ながら整理します。
GOAL この章のゴール
- CRI と OCI の仕様が、それぞれ何を決めているか分かる
- コンテナを起動する設定ファイルを作って読める
- イメージの情報とファイルの層の関係を説明できる
2 つの取り決め
前の章の 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 に参加します。
サンドボックスという語は、この教材の後半では「ホストから隔てた実行環境」という広い意味でも使います。
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 なので、起動まではできません)。
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 するよう定めています。
jq '.process.capabilities.bounding, (.linux.seccomp // "seccomp: なし (全 syscall 許可)")' config.json
jq '.mounts[] | select(.destination | test("^/(proc|dev|sys)$")) | {destination, type}' config.jsoncapabilities は 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: イメージの形式
docker pull が層ごとにダウンロードする様子は最初の章で見ました。
その層の形式と、層をまとめる台帳の形式を決めているのが OCI Image Specオーシーアイイメージスペックコンテナイメージの形式。manifest (層と config の一覧)、config (環境変数や entrypoint)、層 (tar の blob) を、すべて sha256 digest で参照する。用語集で見る です。
イメージは 3 種類のオブジェクトでできています。
- manifest:「この config と、この層たち」という一覧。仕様では
layersの先頭がベースの層で、以降を順に重ねた結果が最終的なファイルシステムです。マルチアーキテクチャの場合、タグはまず index (manifest list) を指し、index が platform ごとの manifest を指します。 - config:
Env、Entrypoint、Cmd、WorkingDir、そして各層の展開後のハッシュ (diff_ids)。docker inspectで見える情報の大半はこれです。 - 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:… で指定します。
参考
- Kubernetes ドキュメント「Container Runtime Interface (CRI)」: https://kubernetes.io/docs/concepts/architecture/cri/
- Kubernetes ドキュメント「Container Runtimes」: https://kubernetes.io/docs/setup/production-environment/container-runtimes/
- Kubernetes ドキュメント「Images」: https://kubernetes.io/docs/concepts/containers/images/
- Kubernetes CRI API (api.proto): https://github.com/kubernetes/cri-api/blob/master/pkg/apis/runtime/v1/api.proto
- Kubernetes 設計文書「Container Runtime Interface v1」: https://github.com/kubernetes/design-proposals-archive/blob/main/node/container-runtime-interface-v1.md
- containerd ドキュメント「CRI Plugin Architecture」: https://github.com/containerd/containerd/blob/main/docs/cri/architecture.md
- OCI「About the Open Container Initiative」: https://opencontainers.org/about/overview/
- OCI Runtime Specification, Filesystem Bundle: https://github.com/opencontainers/runtime-spec/blob/main/bundle.md
- OCI Runtime Specification, Configuration: https://github.com/opencontainers/runtime-spec/blob/main/config.md
- OCI Runtime Specification, Runtime and Lifecycle: https://github.com/opencontainers/runtime-spec/blob/main/runtime.md
- OCI Image Format Specification, Image Manifest: https://github.com/opencontainers/image-spec/blob/main/manifest.md
- OCI Image Format Specification, Image Configuration: https://github.com/opencontainers/image-spec/blob/main/config.md
- OCI Image Format Specification, Image Layer Filesystem Changeset: https://github.com/opencontainers/image-spec/blob/main/layer.md
- OCI Distribution Specification: https://github.com/opencontainers/distribution-spec/blob/main/spec.md
- runc README: https://github.com/opencontainers/runc
- runc, specconv/example.go (runc spec の雛形): https://github.com/opencontainers/runc/blob/main/libcontainer/specconv/example.go