containerd CRI実装:Kubernetesランタイム統合
containerdはKubernetes CRI(Container Runtime Interface)を内蔵プラグインとして実装しています。この記事ではCRI gRPCサービスの実装詳細、Pod Sandbox管理、コンテナスペック変換、ストリーミングAPI、RuntimeClass、NRIを分析します。
1. CRI gRPCサービス
1.1 サービス構造
CRIは2つのgRPCサービスで構成されます:
CRI gRPCサービス:
RuntimeService:
+-- PodSandbox管理
| RunPodSandbox
| StopPodSandbox
| RemovePodSandbox
| PodSandboxStatus
| ListPodSandbox
|
+-- Container管理
| CreateContainer
| StartContainer
| StopContainer
| RemoveContainer
| ListContainers
| ContainerStatus
| UpdateContainerResources
|
+-- ストリーミング
| ExecSync
| Exec
| Attach
| PortForward
|
+-- ランタイム情報
Status
Version
ImageService:
+-- PullImage
+-- ListImages
+-- ImageStatus
+-- RemoveImage
+-- ImageFsInfo
1.2 ソケット構成
CRIソケット:
containerdは同一のgRPCソケットでCRIを提供:
/run/containerd/containerd.sock
kubelet設定:
--container-runtime-endpoint=unix:///run/containerd/containerd.sock
CRIプラグインがcontainerdサーバーにCRIサービスを登録:
プラグインID:io.containerd.grpc.v1.cri
2. Pod Sandbox
2.1 Pod Sandbox概念
Pod SandboxはPodの分離環境を表します:
Pod Sandbox構成:
Pod Sandbox = Pauseコンテナ + 共有ネームスペース
共有リソース:
- ネットワークネームスペース(同じIP、ポート空間)
- IPCネームスペース(プロセス間通信)
- UTSネームスペース(ホスト名)
- PIDネームスペース(オプション)
分離リソース:
- マウントネームスペース(コンテナ別)
- cgroup(コンテナ別リソース制限)
2.2 RunPodSandboxフロー
RunPodSandbox処理:
1. Sandboxメタデータ作成
- ID生成
- ログディレクトリ作成
|
v
2. Pauseイメージプル
- sandbox_image設定からイメージ決定
- デフォルト値:バージョンにより異なる(ドキュメント基準 registry.k8s.io/pause:3.10.2)
|
v
3. Pauseコンテナスナップショット準備
|
v
4. OCIスペック生成
- Pauseコンテナ用最小スペック
- ホスト名、DNS設定を含む
|
v
5. ネットワークネームスペース作成
- /var/run/netns/にネームスペースファイル作成
|
v
6. CNIプラグイン呼び出し
- ネットワークインターフェース作成
- IP割り当て
|
v
7. PauseコンテナTask作成と起動
|
v
8. Sandbox状態をSANDBOX_READYに設定
2.3 Pauseコンテナ
Pauseコンテナの役割:
1. ネームスペース保持者:
- ネットワークネームスペースの最初のプロセス
- Appコンテナが終了してもネームスペースを維持
- ネームスペースのライフサイクルをPodにバインド
2. PID 1の役割:
- Pod PIDネームスペースのinitプロセス
- ゾンビプロセス回収(reap)
- 最小リソース使用(約1MB)
3. 動作:
- pause()システムコールで無限待機
- SIGTERM受信で終了
3. コンテナスペック変換
3.1 CRIリクエストからOCIスペックへ
スペック変換過程:
CRI ContainerConfig:
- Image
- Command、Args
- Envs
- Mounts
- Devices
- SecurityContext
- Resources
|
v
containerd CRIプラグインが変換
|
v
OCI Runtime Spec:
- root(イメージスナップショットパス)
- process(コマンド、env、capabilities)
- mounts(ボリューム、特殊ファイルシステム)
- linux.resources(cgroup設定)
- linux.namespaces(Sandboxと共有)
- hooks(OCIフック)
3.2 リソース変換
Kubernetesリソース -> OCIリソース変換:
CPU:
requests.cpu: 250m
-> linux.resources.cpu.shares = 256
(1000m = 1024 shares基準)
limits.cpu: 500m
-> linux.resources.cpu.quota = 50000
linux.resources.cpu.period = 100000
(500m/1000m * 100000us)
Memory:
limits.memory: 512Mi
-> linux.resources.memory.limit = 536870912
(バイト単位)
requests.memory:
-> スケジューリングにのみ使用、OCIスペックには反映しない
Hugepages:
limits.hugepages-2Mi: 100Mi
-> linux.resources.hugepageLimits:
pageSize: "2MB"
limit: 104857600
3.3 セキュリティコンテキスト変換
SecurityContext -> OCIスペック変換:
runAsUser: 1000
-> process.user.uid = 1000
runAsGroup: 1000
-> process.user.gid = 1000
readOnlyRootFilesystem: true
-> root.readonly = true
privileged: true
-> すべてのcapabilitiesを付与
-> すべてのデバイスアクセスを許可
-> AppArmor/SELinux/Seccompを無効化
capabilities:
add: ["NET_ADMIN"]
drop: ["ALL"]
-> process.capabilities設定
seccompProfile:
type: RuntimeDefault
-> linux.seccompプロファイル適用
4. ストリーミングAPI
4.1 ExecSync
ExecSync動作:
同期的にコンテナでコマンド実行:
1. kubeletがExecSync(containerID, cmd, timeout)を呼び出し
|
v
2. containerdがshimにExecリクエスト
|
v
3. shimがrunc execを実行
- コンテナネームスペースに新しいプロセス作成
|
v
4. stdout/stderrをキャプチャ
|
v
5. プロセス終了待機
|
v
6. exit code + stdout + stderrを返す
使用事例:liveness/readiness probe、kubectl exec(同期)
4.2 Exec(非同期ストリーミング)
Execストリーミング動作:
1. kubeletがExec(containerID, cmd, stdin, stdout, stderr)を呼び出し
|
v
2. containerdがストリーミングURLを返す
- ストリーミングサーバーアドレス:https://node:10250/exec/...
|
v
3. kubeletがクライアントにURLを転送
|
v
4. クライアントがWebSocket/SPDYでストリーミングサーバーに接続
|
v
5. ストリーミングサーバーがcontainerdに実際のExecを実行
|
v
6. stdin/stdout/stderr双方向ストリーミング
ストリーミングプロトコル:
- SPDY(レガシー)
- WebSocket(最新)
4.3 Attach
Attach動作:
実行中のコンテナのメインプロセスに接続:
1. ストリーミングURL生成(Execと類似)
|
v
2. コンテナのstdin/stdout/stderrに接続
- 新しいプロセスを作成しない
- 既存プロセスのI/Oに直接接続
|
v
3. 双方向ストリーミング
使用事例:kubectl attach
4.4 PortForward
PortForward動作:
Podのポートにローカルトラフィックを転送:
1. ストリーミングURL生成
|
v
2. Podのネットワークネームスペースでsocat/nsenterを実行
- 指定ポートにTCP接続
|
v
3. ローカルポートとPodポート間の双方向データ転送
実装:
containerdがPodのネットワークネームスペースに参入し
対象ポートにTCP接続を作成します。
使用事例:kubectl port-forward
5. RuntimeClass
5.1 RuntimeClassマッピング
RuntimeClass処理:
1. Kubernetes RuntimeClassリソース:
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: kata
handler: kata
2. kubeletがCRI RunPodSandbox呼び出し時
runtime_handler = "kata"を渡す
3. containerdがhandlerをランタイム設定にマッピング:
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata]
runtime_type = "io.containerd.kata.v2"
4. 対応するshimバイナリでTask作成:
containerd-shim-kata-v2
5.2 デフォルトランタイム
デフォルトランタイム設定:
[plugins."io.containerd.grpc.v1.cri".containerd]
default_runtime_name = "runc"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true
PodにruntimeClassNameがなければデフォルトランタイム(runc)を使用
5.3 RuntimeClassオーバーヘッド
RuntimeClassリソースオーバーヘッド:
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: kata
handler: kata
overhead:
podFixed:
memory: "160Mi"
cpu: "250m"
オーバーヘッド処理:
- kubeletがPodリソースにオーバーヘッドを追加
- スケジューラーがオーバーヘッドを含めてノード選択
- VMベースランタイムの固定コストを反映
6. NRI(Node Resource Interface)
6.1 NRI概要
NRIはcontainerdのプラグイン拡張メカニズムで、コンテナライフサイクルイベントにフックを登録できます:
NRIアーキテクチャ:
kubelet -> containerd
|
+-- NRI Plugin 1(リソース割り当て)
+-- NRI Plugin 2(トポロジー認識)
+-- NRI Plugin 3(モニタリング)
NRIプラグインはコンテナライフサイクルイベントを受信し
OCIスペックを修正できます。
6.2 NRIフックポイント
NRIフックポイント:
1. RunPodSandbox:
- Pod作成時に呼び出し
- Podレベルリソース割り当て
2. CreateContainer:
- コンテナ作成時に呼び出し
- OCIスペック修正可能
- CPUピンニング、メモリNUMA割り当てなど
3. StartContainer:
- コンテナ起動時に呼び出し
4. UpdateContainer:
- リソース更新時に呼び出し
5. StopContainer:
- コンテナ停止時に呼び出し
- リソース解放
6. RemoveContainer:
- コンテナ削除時に呼び出し
6.3 NRI使用事例
NRI活用事例:
1. CPU/メモリトポロジー認識割り当て:
- NUMA認識CPUピンニング
- メモリを特定NUMAノードに割り当て
- トポロジーマネージャーとの連携
2. デバイスリソース管理:
- GPU割り当て最適化
- RDMAリソース管理
- デバイスプラグイン補完
3. セキュリティポリシー適用:
- 動的Seccompプロファイル
- ランタイムセキュリティルール注入
4. モニタリング/監査:
- コンテナ起動/停止イベントロギング
- リソース使用追跡
7. イメージサービス
7.1 イメージPull
CRI PullImage処理:
1. kubeletがPullImage(imageSpec, authConfig)を呼び出し
|
v
2. containerdがイメージ参照を解決
- タグまたはダイジェスト
- レジストリ認証情報を適用
|
v
3. イメージダウンロード
- Manifest、Config、Layers
- k8s.ioネームスペースに保存
|
v
4. レイヤーアンパッキング
- Snapshotterでスナップショットチェーン作成
|
v
5. イメージ参照(imageRef)を返す
7.2 イメージキャッシング
イメージキャッシング:
containerdのイメージキャッシング:
- Content Storeにレイヤーが既にある場合はダウンロードスキップ
- Snapshotterにスナップショットが既にある場合はアンパッキングスキップ
- ダイジェストベースの正確な重複排除
kubeletのイメージポリシー:
imagePullPolicy: Always
-> 常にレジストリマニフェストを確認(レイヤーはキャッシュ活用)
imagePullPolicy: IfNotPresent
-> ローカルにない場合のみPull
imagePullPolicy: Never
-> ローカルイメージのみ使用
8. モニタリングとデバッグ
8.1 CRIメトリクス
containerd CRI関連メトリクス:
container_runtime_cri_operations_total: CRI操作数
container_runtime_cri_operations_errors_total:CRI操作エラー数
container_runtime_cri_operations_latency_seconds:CRI操作レイテンシ
containerd内部メトリクス:
containerd_task_count: 実行中Task数
containerd_container_count: コンテナ数
containerd_image_pull_duration_seconds: イメージPull所要時間
8.2 デバッグツール
デバッグツール:
1. crictl(CRI CLI):
crictl ps # コンテナ一覧
crictl pods # Pod一覧
crictl images # イメージ一覧
crictl inspect CONTAINER_ID # コンテナ詳細
crictl logs CONTAINER_ID # コンテナログ
crictl exec -it CONTAINER_ID /bin/sh # exec
2. ctr(containerd CLI):
ctr -n k8s.io containers list
ctr -n k8s.io tasks list
ctr -n k8s.io images list
3. containerdログ:
journalctl -u containerd -f
9. crictlで実際に行う作業
ここまでの8章がCRIとは何かを説明したものだとすれば、ノードでCRIを実際に触る通り道はcrictl一つです。ところがcrictlは最初の実行から人を混乱させるところがあります。cri-toolsのドキュメントは、既定のエンドポイントはもはや非推奨であり、ランタイムエンドポイントを常に明示すべきだと断言しています。明示しないとcrictlは既知のソケット候補を順に試し、失敗した候補ごとに接続がタイムアウトするまで数秒ずつ消費します。何もしていないcrictlのコマンド1つが十数秒かかるなら、原因はたいていこの探索です。設定ファイルは /etc/crictl.yaml で、ドキュメントが示すキーは次のとおりです。
# /etc/crictl.yaml
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
timeout: 2
debug: true
pull-image-on-create: false
max-retries: 3
同じ値はフラグでその都度渡すこともできます。-r, --runtime-endpoint がランタイムサービス、-i, --image-endpoint がイメージサービスで、後者の既定値はruntime-endpointの設定をそのまま引き継ぎます。-t, --timeout の既定値は2秒、--max-retries の既定値は3で、明示的に設定されたエンドポイントに対して指数バックオフで再試行します。応答がおかしいときは -D, --debug を付けてリクエストとレスポンスをそのまま見るほうが、ログを漁るより速いです。
crictl info # ランタイム情報
crictl pods # ID / Created / State / Name / Namespace / Attempt / Runtime
crictl ps -a # CONTAINER / IMAGE / CREATED / STATE / NAME / ATTEMPT / POD ID / POD / NAMESPACE
crictl ps -p POD_ID # 特定Podのコンテナのみ
crictl inspectp POD_ID # Pod Sandboxの状態
crictl inspect CONTAINER_ID # コンテナの状態
crictl logs -f --tail 100 CONTAINER_ID
crictl logs -p CONTAINER_ID # 直前のインスタンスのログ
crictl stats # コンテナのリソース使用量
crictl statsp # Pod単位の統計
crictl imagefsinfo # イメージファイルシステムの使用量
出力から何を読むべきかが実力の差になります。crictl pods のRuntime列はそのPodがどのランタイムハンドラへ降りたかを示すので、RuntimeClassが意図どおり適用されたかをマニフェストではなく結果で確認できます。crictl ps は -a がなければ実行中のものしか出さないため、たったいま落ちて再起動を待っているコンテナを丸ごと取り逃がします。CrashLoopを追うときは -a が事実上必須です。ATTEMPT列はkubeletが同じコンテナを何度目に作り直しているかを教えてくれ、POD ID列はコンテナとサンドボックスを結ぶ唯一の手がかりです。crictl inspect が返す構造には3章で説明した変換の結果、つまり最終的に適用されたスペックが入っているので、要求したSecurityContextが本当に反映されたかを目で確認できます。フィールド名はバージョンによって変わるので、正確なフィールドは使用中のバージョンのドキュメントで確認してください。
10. 挙動を左右する設定キー
CRI側の設定もイメージ側と同様にバージョン分岐から見る必要があります。containerd 1.xのversion 2ではすべてが [plugins."io.containerd.grpc.v1.cri"] の下にありましたが、containerd 2.xのversion 3はこれをランタイム側の [plugins.'io.containerd.cri.v1.runtime'] とイメージ側の [plugins.'io.containerd.cri.v1.images'] に分けます。セクション名を間違えてもcontainerdはエラーを出さず、そのブロックを無視したままデフォルト値で起動します。設定を変えたのに何の変化もないときに、値ではなくセクション名をまず疑うべき理由がここにあります。
# containerd 1.x
version = 2
[plugins."io.containerd.grpc.v1.cri"]
sandbox_image = "registry.k8s.io/pause:3.10.2"
max_container_log_line_size = 16384
enable_unprivileged_ports = true
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = false
# containerd 2.x
version = 3
[plugins.'io.containerd.cri.v1.images'.pinned_images]
sandbox = 'registry.k8s.io/pause:3.10.2'
[plugins.'io.containerd.cri.v1.runtime']
max_container_log_line_size = 16384
enable_unprivileged_ports = true
device_ownership_from_security_context = false
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc.options]
SystemdCgroup = true
pauseイメージの位置がバージョン間で変わったことが、実務で最もよく引っかかる点です。1.xではCRIセクションの sandbox_image 1行でしたが、2.xではイメージプラグインの pinned_images 配下の sandbox キーへ移りました。閉域網でpauseイメージを社内レジストリに向けていたクラスタがcontainerd 2.xへ上がると、古いキーは無視されて既定値である公開レジストリのアドレスに戻り、その結果はノード全体でPodが1つも起動しないことです。
SystemdCgroup の既定値は両バージョンともfalseです。この値はkubeletのcgroupドライバ設定と必ず一致していなければなりません。kubeletがsystemdドライバで動いているのにランタイムがcgroupfsで動くと、同じPodのcgroupパスを互いに違う形で計算するため、コンテナは起動するもののリソース会計がずれて状態が不安定になります。症状は特定Podの問題には見えずノード全体の再起動として現れるため、原因を突き止めるまでに時間がかかります。kubeadmで構築したクラスタの既定はsystemdなので、設定ファイルを手で書いたノードでこの値をtrueにし忘れるのが典型的なミスです。
max_container_log_line_size の既定値は16384バイトで、ドキュメントの説明は制限を超えるログ行が複数行に分割されるというものです。アプリケーションが1行のJSONログを吐き、その行が16KBを超えると、ログ収集器は壊れたJSONの断片を2つ受け取ることになります。パース失敗が断続的にしか起きず特定のリクエストでのみ再現するなら、この値をまず疑うのがよいです。ペイロードを丸ごとログに書くサービスで特によく起きます。
enable_unprivileged_ports の既定値はtrueで、ドキュメントの説明はホストネットワークを使わないすべてのコンテナに対して net.ipv4.ip_unprivileged_port_start=0 を設定するというものです。つまりコンテナ内でrootでなくても1024未満のポートを開けます。device_ownership_from_security_context はversion 3のサンプルで既定値falseとして現れます。
11. NRIを運用の観点で見る
6章はNRIが何であるかを説明しました。ここでは実際に有効化するために必要なものだけを見ます。containerdのドキュメントが示す設定ブロックは次のとおりです。
[plugins."io.containerd.nri.v1.nri"]
disable = true
disable_connections = false
plugin_config_path = "/etc/nri/conf.d"
plugin_path = "/opt/nri/plugins"
plugin_registration_timeout = "5s"
plugin_request_timeout = "2s"
socket_path = "/var/run/nri/nri.sock"
まず目に入れるべきは disable = true、つまり既定で無効だという事実です。NRIプラグインを配ったのに何も起きないなら、プラグインではなくこの1行を確認すべきです。socket_path の既定値は /var/run/nri/nri.sock で、外部で動くプラグインはこのソケットに接続します。プラグインをDaemonSetで動かすならこのパスをhostPathでマウントする必要があり、パスが違えば静かに接続されないだけでエラーは出ません。plugin_request_timeout の既定値が2秒であることも覚えておく価値があります。NRIフックはコンテナ生成の経路上にあるため、プラグインが遅くなればその分Podの起動が遅くなり、タイムアウト時の扱いはプラグインの実装と設定に左右されます。本番クラスタにNRIプラグインを入れる前に、この遅延を予算に入れておくべきです。
一つ正直に書いておくことがあります。containerdのNRIドキュメントは設定ブロックと動作の概要を扱うだけで、イベントの一覧を列挙していません。どの時点でどのフックが呼ばれ、各フックで何を変えられるのかはNRIリポジトリ自体を見る必要があります: https://github.com/containerd/nri
12. ワークドエグザンプル: 起動しないPodを追う
PodがPendingやContainerCreatingで止まったとき、kubeletがCRIへ降ろす呼び出しの順序は決まっています。RunPodSandboxでサンドボックスを作り、PullImageでイメージを取得し、CreateContainerでコンテナを作り、StartContainerで起動します。どの段階で止まったかが分かれば見る場所が一つに絞れるので、この順に降りていくのが最短の診断経路です。
# 1) サンドボックスは作られたか
crictl pods --namespace my-ns
# 2) イメージはノードにあるか
crictl images | grep my-app
# 3) コンテナレコードはできたか (死んだものも含む)
crictl ps -a -p POD_ID
# 4) 起動失敗の理由
crictl inspect CONTAINER_ID
crictl logs -p CONTAINER_ID
# 5) ランタイム自身の視点
journalctl -u containerd -f
crictl pods にそのPodがそもそも無ければ、RunPodSandboxの段階で失敗しています。この段階の失敗はたいていpauseイメージのpull失敗かCNIプラグインの失敗のどちらかで、どちらかはcontainerdのログで分かれます。サンドボックスはあるのに crictl ps -a にコンテナが無ければPullImageで止まったのであり、crictl images にイメージが無いことがそれを裏づけます。コンテナレコードはあるのに状態がCreatedから進まないなら、CreateContainerは成功したがStartContainerが失敗したということで、このときが crictl inspect の結果を読むべき瞬間です。マウントパスが無い、要求したユーザーがイメージに存在しない、といった理由がここで表に出ます。状態がExitedなら、コンテナは起動して死んだのですから、ここから先はランタイムの問題ではなくアプリケーションの問題であり、crictl logs -p で直前のインスタンスの出力を見ることになります。
13. 失敗事例と診断の順序
閉域網クラスタでpauseイメージを取得できないことが、最も破壊的な失敗です。アプリケーションのイメージをどれだけ丁寧に社内レジストリへ載せても、サンドボックスが作られなければそのノードではPodが1つも起動しません。症状が特定のワークロードではなくノード全体に及ぶため、ネットワーク障害と誤認しやすいです。確認の順序は、crictl pods に新しいサンドボックスが現れないこと、containerdログのイメージpullエラー、そして設定のpauseイメージのアドレスです。前述のとおりこのキーは1.xと2.xで位置が異なります。
cgroupドライバの不一致は症状が曖昧で長引きがちです。kubeletとcontainerdのどちらか一方だけがsystemdを使うと、Podは起動するものの不安定に再起動し、ノードを入れ替えても再現します。crictl info でランタイム側の設定を見てkubeletの設定と突き合わせるのが最短です。
RuntimeClassのhandler名とcontainerd設定のruntimes項目の名前がずれるケースもよくあります。Kubernetes側のRuntimeClassは検証を通りますが、kubeletがそのhandlerをCRIへ渡した瞬間にcontainerdが対応するランタイムを見つけられず、サンドボックスの生成が失敗します。RuntimeClassのhandlerと containerd.runtimes 配下のキー名は文字単位で一致していなければならず、対応するshimバイナリがPATHにある必要があります。確認は crictl pods のRuntime列が期待したハンドラを示すかどうかで行います。
crictlが見当違いのエンドポイントを見ている場合は、診断そのものが無意味になります。ノードに別のランタイムのソケットが残っているとcrictlがそちらに繋がって空の一覧を見せ、その結果コンテナが1つも無いという誤った結論に至ります。crictl info でいま繋がっている相手をまず確認し、/etc/crictl.yaml のruntime-endpointがkubeletの --container-runtime-endpoint と同じかを突き合わせるのが最初の一手です。
最後はexecとattachだけが失敗するケースです。4章で見たとおり、この2つはkubeletが返したURLへAPIサーバーがストリーミングサーバーへ接続し直すことで成立します。つまり通常のPodの動作が健全でも、APIサーバーからノードのストリーミングアドレスへの経路が塞がっていればexecだけがタイムアウトします。ファイアウォール、ノードの広告アドレス、プロキシ設定が候補で、ノード上で直接 crictl exec が通るかを先に確認すれば、問題区間がランタイムかネットワーク経路かが即座に分かれます。
14. crictlではなくkubectlを使うべき境界
crictlはAPIサーバーを迂回します。だから診断には強力ですが、状態を変える用途に使うと危険です。kubeletは自分が作ったコンテナとサンドボックスを監視し続け、その状態をAPIサーバーへ報告しています。人が crictl rm や crictl rmp でkubeletの所有するオブジェクトを消すと、kubeletの認識と実際の状態がずれ、その結果として作り直されるか奇妙な中間状態に留まります。コンテナを立て直したいなら、Podを消すかワークロードをロールアウトすること、つまりkubectlで行うのが正しい手です。
境界を単純に定めるとこうなります。Kubernetesが何をしようとしているかを知りたければkubectlを、ランタイムが実際に何をしたかを知りたければcrictlを使います。イベント、スケジューリングの決定、Podスペック、コントローラの状態はkubectl側にしかありません。逆に、サンドボックスが作られたか、イメージが本当にノードにあるか、コンテナが何度リトライされたかはcrictl側のほうが正確です。二つの視点が食い違う地点こそが問題の位置です。そしてcrictlで何かを消す行為は、kubeletが既に死んでいてkubectlが届かない状況に限定するのが安全です。
15. 参考資料
- containerd CRIプラグイン設定: https://github.com/containerd/containerd/blob/main/docs/cri/config.md (2026-08-16 確認)
- containerd NRIサポート: https://github.com/containerd/containerd/blob/main/docs/NRI.md (2026-08-16 確認)
- NRIリポジトリ: https://github.com/containerd/nri (2026-08-16 確認)
- crictl 利用ガイド: https://github.com/kubernetes-sigs/cri-tools/blob/master/docs/crictl.md (2026-08-16 確認)
- CRI API定義: https://github.com/kubernetes/cri-api/blob/master/pkg/apis/runtime/v1/api.proto (2026-08-16 確認)
16. まとめ
containerdのCRI実装はKubernetesとコンテナランタイム間の核心インターフェースです。Pod Sandboxを通じたPodレベルの分離、CRIリクエストからOCIスペックへの正確な変換、WebSocket/SPDYベースのストリーミング、RuntimeClassによるマルチランタイムサポート、NRIによる柔軟な拡張が主要な特徴です。この階層化された設計によりcontainerdはKubernetesの安定したコンテナランタイムとしての地位を確立しています。