1 | sequenceDiagram |
command
1 | # 1. Get the full container ID for a pod's container |
Here’s the end-to-end reference flow of how a Pod’s volume target path is created in Kubernetes with a CSI driver (using your LVM-on-host + containerd scenario), from PVC all the way to the containerd rootfs view.
The Big Picture
There are two distinct “target” concepts people often conflate: the kubelet target path (/var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~csi/<vol-name>/mount) created by the CSI node plugin, and the containerd task rootfs (/run/containerd/io.containerd.runtime.v2.task/k8s.io/<container-id>/rootfs) created later by the runtime when the container starts. The CSI driver never touches the containerd path directly — the kubelet hands the CSI target path to the CRI, and containerd/runc bind-mounts it into the container’s mount namespace.
Stage 1 — Provision and Bind
- You create a PVC referencing a StorageClass with
provisioner: <your-csi-driver>. - The external-provisioner sidecar watches PVCs and calls
CreateVolumeon the CSI controller plugin. For an LVM driver this typically creates the LV on the target host (or records intent for topology-aware late binding withvolumeBindingMode: WaitForFirstConsumer). - The provisioner creates the PV object; the PersistentVolume controller binds PV↔PVC.
- Pod is created referencing the PVC; scheduler picks a node (with
WaitForFirstConsumer, provisioning happens after scheduling so the LV lands on the right node).
Stage 2 — Attach (ControllerPublishVolume)
If the driver reports attach_required: true, the Attach/Detach controller creates a VolumeAttachment object, and the external-attacher calls ControllerPublishVolume to “attach” the volume to the node. For pure local LVM drivers this is usually a no-op or skipped entirely (CSIDriver.spec.attachRequired: false), since the LV is already local.
Stage 3 — NodeStageVolume (Staging Path)
Once the pod lands on the node, the kubelet’s Volume Manager (reconciler loop comparing desired vs. actual state of world) drives the mount:[2]
- Kubelet’s in-tree CSI mounter calls
NodeStageVolumeover the driver’s UNIX socket. - Staging path:
/var/lib/kubelet/plugins/kubernetes.io/csi/pv/<pv-name>/globalmount(for block volumes:/var/lib/kubelet/plugins/kubernetes.io/csi/volumeDevices/staging/<pv-name>). - Your LVM driver here:
lvchange -ay vg/lv, format if the LV has no filesystem, thenmount /dev/vg/lv <staging-path>. - Staging happens once per PV per node, even if multiple pods use it.
Stage 4 — NodePublishVolume (Target Path)
This is where the per-pod target path is born:
- Kubelet calls
NodePublishVolumewithtarget_path = /var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~csi/<pvc-name>/mount. - The driver performs a bind mount:
mount --bind <staging-path> <target-path>. - The path is stable for the pod UID; kubelet’s reconciler will re-issue
NodePublishVolumeif the mount is lost, and drivers withRequiresRepublishget repeated calls.
So on the host you now have:
1 | /dev/mapper/vg-lv → /var/lib/kubelet/plugins/kubernetes.io/csi/pv/<pv>/globalmount |
Stage 5 — CRI → containerd → the rootfs Path
Now the runtime enters:
- Kubelet calls CRI
CreateContainer, passing Mounts:host_path = /var/lib/kubelet/pods/<uid>/volumes/kubernetes.io~csi/<pvc>/mount,container_path = <volumeMounts.mountPath>. - Containerd’s CRI plugin builds the OCI spec (rootfs from the snapshotter + the mount list) and starts a runtime v2 shim task.
- runc creates the container’s mount namespace and bind-mounts the kubelet target path into the rootfs at
mountPathbefore pivot_root. - Containerd exposes the running container’s rootfs on the host at
/run/containerd/io.containerd.runtime.v2.task/k8s.io/<container-id>/rootfs— this is the path you see inconfig.json/runc specoutput.
Because that rootfs bind shares the same underlying directory the mounts were applied to, you can see the LV’s content from the host at:
1 | /run/containerd/io.containerd.runtime.v2.task/k8s.io/ca47433f.../rootfs/<mountPath> |
This rootfs path is also what vulnerability scanners and debug tooling walk when inspecting container filesystems.
Path Ownership Map
| Path | Created by | Lifetime |
|---|---|---|
/var/lib/kubelet/plugins/kubernetes.io/csi/pv/<pv>/globalmount |
CSI node plugin (NodeStageVolume) |
Per PV per node |
/var/lib/kubelet/pods/<uid>/volumes/kubernetes.io~csi/<pvc>/mount |
CSI node plugin (NodePublishVolume, bind mount) |
Per pod UID |
/run/containerd/io.containerd.runtime.v2.task/k8s.io/<cid>/rootfs |
containerd shim / runc | Per container task |
/var/log/pods/<ns>_<pod>_<uid>/<container>/*.log → symlinks into containerd log dir |
kubelet | Per pod |
Verify on the Host
1 | # find the CSI staging + target mounts for a pod |
Teardown is the exact reverse: NodeUnpublishVolume (unmount target) → container task destroyed (rootfs path disappears) → NodeUnstageVolume (unmount staging, optionally lvchange -an) → ControllerUnpublishVolume → DeleteVolume when the PV is released.[11][1]