1 つのコンテナを、Kubernetes、ランタイム、Linux の順に見てみましょう。 表示される名前や ID は違っていても、同じアプリを指しています。
この章では、動いている CoreDNS のコンテナを選び、その ID を手がかりに設定ファイル、プロセス、メモリの上限を探します。 ここまで学んだ仕組みが、ノード上のどこにあるかを確かめる実習です。
GOAL この章のゴール
- コンテナの ID から、ノード上のプロセスを見つけられる
- 同じコンテナの実行設定とメモリ上限を確認できる
- イメージの層がどこで重ねられているか確認できる
ノードの中の地図
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 で確認して消してください。
バージョンと設定
まず、ノードに入っている containerd、runc、crictl のバージョンを確かめます。
containerd --version
runc --version | head -1
export CONTAINER_RUNTIME_ENDPOINT=unix:///run/containerd/containerd.sock
crictl version出力例
containerd github.com/containerd/containerd/v2 v2.4.1 …
runc version 1.5.2
Version: 0.1.0
RuntimeName: containerd
RuntimeVersion: v2.4.1
RuntimeApiVersion: v1RuntimeApiVersion: v1 が CRI のバージョンです。
次に、kubelet がどのソケットに繋いでいるかを kubelet の設定で見ます。
grep -E 'containerRuntimeEndpoint|cgroupDriver|cgroupRoot' /var/lib/kubelet/config.yaml出力例
cgroupDriver: systemd
containerRuntimeEndpoint: unix:///run/containerd/containerd.sockcgroupDriver: systemd は「cgroup の作成を systemd に頼む」という意味で、この設定を cgroup driver と呼びます。
Kubernetes のドキュメントによると、driver は cgroupfs (kubelet と containerd が /sys/fs/cgroup に直接書く。kubelet の既定) と systemd の 2 つで、kubelet と containerd は同じ driver に揃っていなければなりません。
同じドキュメントは、systemd を init に使うホストでは systemd driver を使い、cgroup v2 では cgroupfs の代わりに systemd driver を使うよう求めています。
理由は、systemd がシステムに cgroup の管理者が 1 つだけいることを前提にしていて、管理者が 2 つあると資源の見え方が 2 通りになり、負荷がかかったときにノードが不安定になるからです。
containerd のドキュメントは同じことを、cgroup の「single-writer」の規則と呼んでいます。
containerd 側の対応する設定が、runc の options の SystemdCgroup = true です。
grep -vE '^\s*(#|$)' /etc/containerd/config.toml | head -40出力例
version = 3
[plugins.'io.containerd.cri.v1.runtime'.containerd]
default_runtime_name = 'runc'
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc]
runtime_type = 'io.containerd.runc.v2'
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc.options]
SystemdCgroup = trueファイルが短ければ、書いてない項目は既定値です。
全部の既定値は containerd config default が TOML で出してくれます。
version = 3 が containerd 2.x の形式で、1.x の version = 2 では plugin 名が io.containerd.grpc.v1.cri という 1 つにまとまっていました。
コンテナを 1 つ選ぶ
ここからは CoreDNS のコンテナを 1 つ追いかけます。
CID=$(crictl ps --name coredns -q | head -1); echo $CID
crictl inspect $CID | jq '{pid: .info.pid, runtime: .info.runtimeType, sandbox: .info.sandboxID, cgroup: .info.runtimeSpec.linux.cgroupsPath, log: .status.logPath}'
PID=$(crictl inspect $CID | jq -r .info.pid); echo $PID出力例
7d6c5b4a3f2e…
{
"pid": 1301,
"runtime": "io.containerd.runc.v2",
"sandbox": "9f8e7d6c5b4a…",
"cgroup": "kubepods-burstable-pod1a2b3c4d_….slice:cri-containerd:7d6c5b4a3f2e…",
"log": "/var/log/pods/kube-system_coredns-…/coredns/0.log"
}
1301crictl inspect の .info は containerd が kubelet に返す付加情報で、runc に渡した OCI spec がまるごと入っています。
cgroupsPath が 親.slice:プレフィックス:ID の形なのは systemd driver の記法です。
containerd の目線: bundle と config.json
containerd のドキュメントによると、/var/lib/containerd は再起動後も残るデータ (イメージの content と snapshot) の置き場で、/run/containerd はソケット、PID、実行中の状態など再起動で消えてよいデータの置き場です。
shim が runc に渡した bundle は後者の下にあります。
B=/run/containerd/io.containerd.runtime.v2.task/k8s.io/$CID
ls $B
jq '{args: .process.args, root: .root.path, ns: [.linux.namespaces[] | .type + (.path // "")], cgroup: .linux.cgroupsPath}' $B/config.json出力例
address config.json init.pid log log.json options.json rootfs runtime shim-binary-path work
{
"args": ["/coredns", "-conf", "/etc/coredns/Corefile"],
"root": "rootfs",
"ns": ["pid", "ipc/proc/1260/ns/ipc", "uts/proc/1260/ns/uts", "mount", "network/proc/1260/ns/net"],
"cgroup": "kubepods-burstable-pod1a2b3c4d_….slice:cri-containerd:7d6c5b4a3f2e…"
}namespaces の配列を見ると、pid と mount は path が空で、ipc、uts、network は /proc/1260/ns/… という path 付きです。
OCI Runtime Spec は、path があればその既存の namespace に参加し、無ければ新しく作る、と定めています。
1260 は pause の PID です。
Pod 内のコンテナが同じ IP を持つのは、config.json のこの 3 行で pause の namespace を指しているからで、CRI の章で見た「sandbox が先」という順序がここで JSON になっています。
PAUSE=$(jq -r '.linux.namespaces[] | select(.type=="network") | .path' $B/config.json | cut -d/ -f3)
for ns in net ipc uts pid mnt; do printf "%-4s pause=%s coredns=%s\n" $ns $(readlink /proc/$PAUSE/ns/$ns) $(readlink /proc/$PID/ns/$ns); done出力例
net pause=net:[4026532716] coredns=net:[4026532716]
ipc pause=ipc:[4026532714] coredns=ipc:[4026532714]
uts pause=uts:[4026532713] coredns=uts:[4026532713]
pid pause=pid:[4026532717] coredns=pid:[4026532801]
mnt pause=mnt:[4026532712] coredns=mnt:[4026532799]net / ipc / uts は同じ番号、pid / mnt は別の番号です。 手作りコンテナの章で見た「番号が同じなら同じ namespace」を、本物の Pod で確かめました。
runc の目線: state
runc --root /run/containerd/runc/k8s.io list | head -5
runc --root /run/containerd/runc/k8s.io state $CID | jq '{status, pid, bundle}'出力例
ID PID STATUS BUNDLE CREATED
7d6c5b4a3f2e… 1301 running /run/containerd/io.containerd.runtime.v2.task/k8s.io/7d6c… 2026-…
{ "status": "running", "pid": 1301, "bundle": "/run/containerd/io.containerd.runtime.v2.task/k8s.io/7d6c…" }--root は runc が状態を置く場所で、containerd は /run/containerd/runc/<containerd の namespace> を使います。
runc のプロセス自体は起動後に終了していますが、state.json にコンテナの PID と bundle を残していて、runc kill や runc exec はこれを読んで動きます。
state の出力 (status、pid、bundle) の形式は OCI Runtime Spec で決まっています。
カーネルの目線: cgroup と mount
CG=/sys/fs/cgroup$(cut -d: -f3 /proc/$PID/cgroup); echo $CG
cat $CG/memory.max $CG/cpu.max $CG/pids.max
cat $CG/cgroup.procs
cat $CG/memory.current出力例
/sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod1a2b3c4d_….slice/cri-containerd-7d6c5b4a3f2e….scope
178257920
max 100000
max
1301
24117248kubeadm が作る CoreDNS の Deployment は limits.memory: 170Mi なので、memory.max が 178257920 (= 170 × 1024 × 1024) です。
cpu.max が max 100000 なのは CPU の limit が無いからで、requests は親の cpu.weight に効いています。
memory.current が今の使用量で、kubectl top の数字の出どころはここです。
mount | grep "$CID/rootfs" | tr ',' '\n'出力例
overlay on /run/containerd/io.containerd.runtime.v2.task/k8s.io/7d6c…/rootfs type overlay (rw
relatime
lowerdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/41/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/12/fs
upperdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/58/fs
workdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/58/work)lowerdir の数はイメージの層の数です。
upperdir の snapshot 番号はこのコンテナ専用で、コンテナを消すと消えます。
中からファイルを 1 つ書くと、upperdir に現れます。
UP=$(mount | grep "$CID/rootfs" | sed 's/.*upperdir=\([^,]*\).*/\1/')
nsenter -t $PID -m -p sh -c 'echo hello > /tmp/from-inside' 2>/dev/null || nsenter -t $PID -m sh -c 'echo hello > /tmp/from-inside'
ls -la $UP/tmp/出力例
-rw-r--r-- 1 root root 6 … from-insidensenterエヌエスエンター指定したプロセスの namespace に入ってコマンドを実行する util-linux のコマンド。-t <pid> で対象、-m -n -p などで入る namespace を選ぶ。用語集で見る でコンテナの mount namespace に入って書いたファイルが、ホスト側の upperdir に現れました。
イメージ (lowerdir) は変わっていません。
kubectl logs が読むファイル
tail -2 $(crictl inspect $CID | jq -r .status.logPath)出力例
2026-10-06T10:00:00.123456789Z stderr F [INFO] 10.244.0.5:51234 - 1234 "A IN kubernetes.default.svc.cluster.local. udp 54 false 512" NOERROR qr,aa,rd 106 0.000123skubectl logs は、kubelet がこのファイルを読んで返しているだけです。
行の形式 (RFC 3339 の時刻、stdout / stderr の別、行が完結しているかの F / P、本文) は Kubernetes の CRI ログ設計で決まっていて、書くのはランタイム側 (shim と containerd の CRI plugin)、読むのは kubelet です。
これで 1 つのコンテナを、kubelet (crictl)、containerd (bundle)、runc (state)、カーネル (cgroup、mount、/proc) の 4 つの目線で見ました。 どの層でも同じ ID と PID で繋がっています。
rm -f $UP/tmp/from-inside
exitふりかえり
Qkubelet の cgroupDriver: systemd に対応する containerd 側の設定は?
両方が systemd を指していないと、cgroup の作成で食い違って Pod が起動しません。
Qconfig.json の namespaces で path が付いているものの意味は?
pause の /proc/<pid>/ns/net などを指して「ここに入れ」と言っています。Pod 内で IP が共有される仕組みです。
Qkubectl top pod のメモリ使用量の元になるファイルは?
kubelet が cgroup の統計を読み、metrics-server 経由で kubectl top に出ています。
参考
- Kubernetes ドキュメント「Container Runtimes」: https://kubernetes.io/docs/setup/production-environment/container-runtimes/
- Kubernetes ドキュメント「About cgroup v2」: https://kubernetes.io/docs/concepts/architecture/cgroups/
- Kubernetes 設計文書「Kubelet CRI logging」: https://github.com/kubernetes/design-proposals-archive/blob/main/node/kubelet-cri-logging.md
- Kubernetes, kubeadm の CoreDNS マニフェスト (limits の出典): https://github.com/kubernetes/kubernetes/blob/master/cmd/kubeadm/app/phases/addons/dns/manifests.go
- containerd ドキュメント「CRI plugin config」: https://github.com/containerd/containerd/blob/main/docs/cri/config.md
- containerd ドキュメント「containerd 2.0」: https://github.com/containerd/containerd/blob/main/docs/containerd-2.0.md
- containerd ドキュメント「containerd-config(8)」: https://github.com/containerd/containerd/blob/main/docs/man/containerd-config.8.md
- containerd ドキュメント「Operations」(root と state のディレクトリ): https://github.com/containerd/containerd/blob/main/docs/ops.md
- containerd ドキュメント「Runtime v2」: https://github.com/containerd/containerd/blob/main/docs/runtime-v2.md
- containerd ドキュメント「Using crictl」: https://github.com/containerd/containerd/blob/main/docs/cri/crictl.md
- cri-tools ドキュメント「crictl」: https://github.com/kubernetes-sigs/cri-tools/blob/master/docs/crictl.md
- Linux kernel documentation, Control Group v2: https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html
- Linux kernel documentation, Overlay Filesystem: https://www.kernel.org/doc/html/latest/filesystems/overlayfs.html
- nsenter(1), Linux manual page: https://man7.org/linux/man-pages/man1/nsenter.1.html
- OCI Runtime Specification, Configuration: https://github.com/opencontainers/runtime-spec/blob/main/config.md
- OCI Runtime Specification, Runtime and Lifecycle (state の形式): https://github.com/opencontainers/runtime-spec/blob/main/runtime.md
- runc README: https://github.com/opencontainers/runc
読み切ると称号を獲得
カーネルの代理人
この Part を読み切りました。
称号一覧