Kubernetes 解体新書Part 4 インターフェース

CRI: kubelet がランタイムに頼む方法

Pod が起動するまで、kubelet は何をどの順に頼むのでしょうか。ランタイムの窓口につないで状態を確かめます。

  • L2 ノードの中
  • 更新 2026年10月6日

Pod が ContainerCreating のままで、アプリが動き始めない。 イメージの取得を待っているのか、ネットワークの準備に失敗したのか、コンテナを起動できないのか。 同じ表示でも、止まっている場所は違います。

この章では、kubelet がランタイムに頼む仕事を、起動の順に追います。 その窓口である CRI に、crictl というコマンドで接続し、準備のどこまで進んだかを確かめます。

GOAL この章のゴール

  • Pod の準備からコンテナの起動まで、依頼の順序を説明できる
  • イベントから起動処理のどこで失敗したかを調べられる
  • crictl でランタイムが持つコンテナの状態を確認できる

CRI と OCI の位置関係

kubelet は、コンテナの起動や状態確認を containerd などのランタイムに頼みます。 この呼び出し方を定めるのが CRI です。 同じノード上のプログラム同士をつなぐ UNIX socket を使い、gRPC という方式でやり取りします。

一方、containerd が runc に渡す実行設定や、コンテナの起動・停止の決まりは OCI Runtime Spec が定めます。 CRI が Kubernetes からの依頼を扱うのに対して、こちらはコンテナを実際に作るための設定と操作を扱います。 図で、2 つがどの間にあるかを見てください。

CRI と OCI の位置: kubelet から runc までのスタックkubelet と containerd の間が CRI (Kubernetes の層)、containerd と runc の間が OCI Runtime Spec (Linux カーネルの層)。RuntimeClass は CRI の runtime handler でどの低レベルランタイムを使うか選ぶ。Linux kernelnamespace / cgrouprunc / runsc / kata低レベル (OCI) ランタイムcontainerd-shim-runc-v2コンテナごとの shimcontainerd高レベル (CRI) ランタイムkubeletCRI (gRPC)kubelet → containerd。Kubernetes が決めた仕様OCI Runtime Spec (config.json)containerd → runc。OCI が決めた仕様RuntimeClass → CRI の runtime_handler →containerd がどの OCI ランタイムを exec するか選ぶ
図 1上の矢印が CRI、下の矢印が OCI。RuntimeClass は、Pod をどの runtime handler (runc や gVisor など) で起動するかを選ぶ。

2 つの仕様は、頼む内容が違います。 CRI は「Pod 単位でコンテナ群を作り、イメージを管理し、ログや exec を中継する」という、Pod やイメージといった Kubernetes の概念で書かれています。 OCI Runtime Spec は「この config.json の通りに namespace と cgroup を作ってプロセスを起動しろ」という、Linux の概念で書かれています。 containerd の仕事は、前者を後者に翻訳することです。

図の RuntimeClass は、Part 3 で見た gVisor などを Pod ごとに選ぶためのリソースです。 RuntimeClass の handler の名前が CRI の RunPodSandbox に runtime_handler として渡され、containerd は自分の設定 (/etc/containerd/config.toml の runtimes.<handler>) からどの低レベルランタイムを使うかを引きます。

2 つの gRPC サービス

イメージを用意する仕事と、コンテナを動かす仕事は、別々の窓口になっています。 CRI では、この 2 つの窓口をサービスと呼びます。 RuntimeService は Pod sandbox とコンテナの操作、ImageService はイメージの取得と削除です。 どちらも同じ UNIX socket で提供され、kubelet の設定 containerRuntimeEndpoint にそのパスが書いてあります。

CRI の 2 つの gRPC サービス: RuntimeService と ImageServicekubelet と crictl が同じ UNIX socket に接続し、RuntimeService (Pod とコンテナの操作) と ImageService (イメージの操作) を呼ぶ。kubelet本番の clientcrictlデバッグ用 clientcontainerd.sock/run/containerd/gRPCRuntimeServiceRunPodSandbox / StopPodSandboxCreateContainer / StartContainerExec / Attach / PortForwardListPodSandbox / ContainerStatusStatus / Version / RuntimeConfigImageServicePullImage / ListImagesImageStatus / RemoveImageImageFsInfo同じ socket、同じ API。crictl は kubelet のふりをしているだけ
図 2kubelet と crictl は同じ socket に繋ぎ、同じメソッドを呼ぶ。crictl は kubelet のふりをするデバッグ用 client。

RuntimeService には Exec / Attach / PortForward もあります。 CRI の Exec の応答は「コマンドを実行するストリーミングサーバーの URL」で、kubelet はその URL に接続して apiserver との間を中継します。 kubectl exec でコンテナの中に入る操作も、apiserver → kubelet → CRI という同じ道を通っています。

Pod が起動するまでの 4 呼び出し

kubelet が Pod を 1 つ起動するとき、CRI の呼び出しは大きく 4 段です。 containerd のドキュメントが、コンテナ 1 つの Pod を例に、この流れを説明しています。

CRI のシーケンス: RunPodSandbox から StartContainer までkubelet が containerd に RunPodSandbox、PullImage、CreateContainer、StartContainer を順に gRPC で呼ぶ。RunPodSandbox の中で containerd は network namespace を作り、CNI ADD で設定し、pause コンテナを runc で起動する。CreateContainer では OCI の config.json を作って runc でコンテナを作り、StartContainer で起動する。kubeletcontainerd (CRI)CNI / runc1. RunPodSandbox(PodSandboxConfig)network namespace を作るCNI ADD (netns に veth と IP)pause コンテナを runc で起動pod_sandbox_id2. PullImage(ImageSpec)image_ref3. CreateContainer(sandbox_id, ContainerConfig)OCI config.json を生成、runc でコンテナを作るcontainer_id4. StartContainer(container_id)runc でプロセスを起動
図 3RunPodSandbox の中で containerd が network namespace を作り、CNI で設定し、pause コンテナを起動する。アプリのコンテナは CreateContainer でその sandbox に足されていく。
  1. RunPodSandbox

    Pod sandboxポッドサンドボックスPod 内のコンテナが共有する namespace (net / ipc / uts) を保持するもの。containerd では pause コンテナがこの役を担う。用語集で見る を作ります。containerd はまず Pod の network namespace を作り、それを CNI で設定し (Pod の IP が決まるのはこの時点です)、次に pause コンテナを起動して Pod の cgroup と namespace に入れます。
  2. PullImage

    各コンテナのイメージを ImageService で取得します。containerd は、イメージがノードに無いときだけ pull します。
  3. CreateContainer

    sandbox の ID と ContainerConfig (イメージ、コマンド、環境変数、マウント、デバイス) を渡します。containerd はここで OCI の config.json を生成し、runc でコンテナを作ります。
  4. StartContainer

    コンテナのプロセスを起動します。init コンテナがあれば、それが終わるのを待ってからアプリのコンテナで 3 と 4 を繰り返します。

Pod が ContainerCreating で止まるとき、kubelet のイベントにはこのどれで止まったかが出ます。 FailedCreatePodSandBox なら 1 (多くは CNI 側)、ErrImagePull なら 2、CreateContainerError なら 3 です。 どの段で止まったかが分かれば、疑う場所が決まります。

年表

CRI の年表: dockertools から CRI v1 まで2015 年 7 月の 1.0 で kubelet が dockertools と rkt を内蔵、2016 年 12 月の 1.5 で CRI alpha と dockershim、2020 年 12 月の 1.20 で dockershim 非推奨、2022 年 5 月の 1.24 で dockershim 削除、2022 年 12 月の 1.26 で CRI v1 必須。2015.71.0dockertools + rkt2016.121.5CRI alpha + dockershim2020.121.20dockershim 非推奨2022.51.24dockershim 削除2022.121.26CRI v1 必須
図 4dockertools から CRI v1 まで。1.5 で CRI と同時に dockershim が入り、1.24 で消えた。

実機: kubelet と同じ socket に繋ぐ

ノードの中に入るノードの中で実行
kubectl debug node/$(kubectl get node -o jsonpath='{.items[0].metadata.name}') -it \
  --profile=sysadmin --image=busybox:1.37 -- chroot /host nsenter -t 1 -m -u -i -n -p -- bash

抜けるときは exit。デバッグ用 Pod は自動では消えないので、終わったら kubectl delete pod -l app.kubernetes.io/managed-by=kubectl-debug かkubectl get pods で確認して消してください。

まず kubelet がどこに繋いでいるかを設定ファイルで確かめます。

kubelet の設定からランタイムの socket を読むノードの中で実行
grep -E 'containerRuntimeEndpoint|cgroupDriver' /var/lib/kubelet/config.yaml

出力例

cgroupDriver: systemd
containerRuntimeEndpoint: unix:///run/containerd/containerd.sock

次に crictl (CRI 互換のランタイムを検査、デバッグするための CLI) で同じ socket に繋ぎます。 info は RuntimeService の Status を呼び、ランタイムの状態と、containerd の CRI プラグインが持っている設定を返します。

crictl でランタイムの状態を読むノードの中で実行
crictl --runtime-endpoint unix:///run/containerd/containerd.sock info | jq '.status.conditions, .config.cni, (.config.containerd.runtimes | keys)'

出力例

[
  { "type": "RuntimeReady", "status": true, ... },
  { "type": "NetworkReady", "status": true, ... }
]
{ "binDir": "/opt/cni/bin", "confDir": "/etc/cni/net.d", ... }
[ "runc" ]

CRI の仕様は RuntimeReady と NetworkReady を「kubelet が正しく動くために必須の条件」と定め、どちらかが満たされないとき Node は NotReady になる、と書いています。 NetworkReady は「コンテナネットワークを必要とするコンテナを受け付けられる状態か」を表し、containerd では CNI の設定が読めているかに対応します。 CRI の Status に CNI の状態が混ざっているのは、containerd が RunPodSandbox の中で CNI を呼ぶ側だからです。 次の章で、この confDir の中身を読みます。

Pod sandbox とコンテナを列挙するノードの中で実行
crictl pods
crictl ps

出力例

POD ID         CREATED      STATE   NAME                         NAMESPACE     ATTEMPT
3f1c2a9b8d7e   2 hours ago  Ready   cilium-abcde                 kube-system   0
...
CONTAINER      IMAGE          CREATED      STATE     NAME            ATTEMPT  POD ID
9a8b7c6d5e4f   quay.io/...    2 hours ago  Running   cilium-agent    0        3f1c2a9b8d7e

crictl pods が返すのは RunPodSandbox で作られた sandbox、crictl ps は CreateContainer で作られたコンテナです。 kubectl get pods と同じものが見えますが、こちらは apiserver を経由していません。 ノードの上の事実をそのまま読んでいます。

ふりかえり

QCNI の ADD が呼ばれるのは CRI のどのメソッドの処理中?

Pod の network namespace は sandbox が持ちます。containerd は RunPodSandbox の中で network namespace を作って CNI で設定し、pause コンテナをそこに入れます。

QCRI と OCI Runtime Spec の関係として正しいのは?

CRI は Kubernetes の語彙 (Pod、イメージ、exec) で書かれ、OCI Runtime Spec は Linux の語彙 (namespace、cgroup、プロセス) で書かれています。containerd が前者を後者に翻訳します。

Qcrictl info の NetworkReady が false のとき、何が起きる?

CRI の仕様は RuntimeReady と NetworkReady を必須の条件とし、どちらかが満たされないと Node は NotReady になります。NotReady の Node には新しい Pod がスケジュールされません。

参考