Storage interviews probe whether you can name who does what: five sidecars, two plugin halves, and three control loops — the PV controller, the attach-detach controller, and the scheduler's volume-binding plugin — turn a PVC into a mounted filesystem. The mental model: CSI is a gRPC contract split into cluster-scoped operations (create, attach) and node-scoped operations (stage, publish), with sidecars translating API objects into gRPC so the driver never needs to know Kubernetes. This chapter zooms into the volume segment of Flow 8 (Chapter 4).
Early volume drivers were "in-tree": vendor code compiled into Kubernetes itself. A driver bug meant patching Kubernetes; a new feature waited for a Kubernetes release. CSI (Container Storage Interface) moved drivers out-of-tree behind a versioned gRPC contract, shared with other orchestrators. In-tree cloud drivers have been migrated to CSI equivalents.
A CSI driver implements three gRPC services:
- Identity —
GetPluginInfo,Probe, capabilities. Served by both halves. - Controller — cluster-scoped calls against the storage backend:
CreateVolume/DeleteVolume,ControllerPublishVolume/ControllerUnpublishVolume(attach/detach),ControllerExpandVolume,CreateSnapshot. - Node — node-local calls:
NodeStageVolume(format and mount the device once per node, to a staging path),NodePublishVolume(bind-mount the staged volume into a specific pod's directory), and their inverses.
The controller plugin (a Deployment/StatefulSet, runs anywhere) serves Identity+Controller. The node plugin (a DaemonSet on every node) serves Identity+Node and talks to kubelet over a Unix socket.
The driver never watches the Kubernetes API. Sidecar containers, maintained by the community and deployed next to the driver, do that translation:
| Sidecar | Watches | Calls on the driver |
|---|---|---|
| external-provisioner | PVCs needing a volume | CreateVolume / DeleteVolume |
| external-attacher | VolumeAttachment objects | ControllerPublishVolume / Unpublish |
| external-resizer | PVC size increases | ControllerExpandVolume |
| external-snapshotter | VolumeSnapshot objects | CreateSnapshot / DeleteSnapshot |
| node-driver-registrar | — | registers the node plugin's socket with the kubelet |
flowchart TD
API[kube-apiserver]
Prov[external provisioner]
Att[external attacher]
CP[controller service]
Reg[node driver registrar]
NP[node service]
Kubelet[kubelet]
SB[storage backend]
Prov -->|watch PVCs| API
Att -->|watch attachments| API
Prov -->|CreateVolume| CP
Att -->|ControllerPublishVolume| CP
CP -->|cloud calls| SB
Reg -->|register socket| Kubelet
Kubelet -->|stage, publish| NP
classDef resource fill:#fb7185,stroke:#be123c,color:#fff
classDef process fill:#38bdf8,stroke:#0369a1,color:#fff
classDef external fill:#64748b,stroke:#334155,color:#fff
classDef leader fill:#10b981,stroke:#047857,color:#fff
class API resource
class Prov,Att,Reg process
class CP,NP,SB external
class Kubelet leader
Figure 8.1 — sidecars watch the API and translate to gRPC; kubelet calls the node plugin directly.
- PV/PVC binding: the PV controller (in kube-controller-manager) pairs claims with volumes; each references the other, phase
Bound. Binding is exclusive and permanent. - StorageClass names a provisioner plus parameters;
volumeBindingModeis its key field. Immediate provisions/binds as soon as the PVC exists — possibly in the wrong zone. WaitForFirstConsumer (WFFC) delays until a pod uses the claim, so the scheduler picks the node first. - Access modes: RWO (one node), ROX (many nodes, read-only), RWX (many nodes), RWOP (single pod, enforced at scheduling). RWO is per-node: two pods on one node can share it.
- Topology:
CreateVolumereceives the selected node's topology; the PV carriesnodeAffinityso consumers are never scheduled where the volume can't attach. - Expansion: resizer calls
ControllerExpandVolume, then kubelet finishes withNodeExpandVolume(filesystem grow). - Snapshots:
VolumeSnapshot/VolumeSnapshotClass; a new PVC restores viadataSource.
You create a PVC with a WFFC StorageClass, then a pod that mounts it.
- User creates the PVC referencing StorageClass
fast(volumeBindingMode: WaitForFirstConsumer). - KCM (PV controller) finds no matching PV and, because of WFFC, waits — the PVC stays
Pendingwith event "waiting for first consumer". - User creates a pod mounting the PVC.
- Sched runs its volume-binding plugin: filters nodes by StorageClass topology, picks one, and annotates the PVC with the selected node.
- Prov (external-provisioner, watching PVCs) sees the annotation and calls
CreateVolumeon the controller plugin, passing capacity, parameters, and the node's topology. - CSI controller service creates the disk in the backend; the provisioner then creates a PV object carrying
nodeAffinityfor that topology. - KCM (PV controller) binds PVC and PV; both go
Bound, and the scheduler completes the pod binding to the chosen node. - KCM (attach-detach controller) sees a scheduled pod using the volume and creates a VolumeAttachment object for (volume, node).
- Att (external-attacher) sees it and calls
ControllerPublishVolume; the backend attaches the disk to the node; the attacher setsattached: true. - Kubelet (volume manager) has been waiting for that status; it now calls
NodeStageVolumeon the node plugin: format if empty, mount the device at a per-volume staging path. - Kubelet calls
NodePublishVolume: bind-mount the staging path into the pod's volumes directory. - CRI starts containers with that directory bind-mounted at the declared
mountPath. - Kubelet begins teardown in exact reverse once containers stop:
NodeUnpublishVolume, thenNodeUnstageVolume. The attach-detach controller then deletes the VolumeAttachment (ControllerUnpublishVolumedetaches), and PVC deletion applies the reclaim policy (DeletetriggersDeleteVolume;Retainkeeps the PV asReleased).
sequenceDiagram
autonumber
participant User
participant API
participant Sched
participant Prov
participant Att
participant Kubelet
participant CSI
User->>API: create PVC, pod
Sched->>API: annotate PVC
Prov->>CSI: CreateVolume
Prov->>API: create PV
Sched->>API: bind pod
Att->>CSI: ControllerPublishVolume
Kubelet->>CSI: NodeStageVolume
Kubelet->>CSI: NodePublishVolume
Kubelet->>API: pod Running
Note over CSI: attach, then mount
Figure 8.2 — provision, attach, stage, publish: four distinct actors, and teardown runs the exact reverse.
Where this can fail
- Symptom: PVC
Pendingforever, no events. Cause: WFFC and no pod uses the claim yet — working as designed. Where to look:kubectl describe pvc; create the consumer. - Symptom: PVC
Pending, "no volume plugin matched" or no provisioner activity. Cause: StorageClass names a provisioner that isn't running. Where to look: controller plugin pod, StorageClassprovisionerfield. - Symptom: pod stuck
ContainerCreating, "failed to attach". Cause:ControllerPublishVolumefailing — backend limits (max disks per node), permissions, or wrong zone. Where to look: VolumeAttachment status, attacher logs. - Symptom: attach succeeded but mount fails, "waiting for device". Cause:
NodeStageVolumefailing — device not visible, filesystem corrupt, fsck errors. Where to look: node plugin logs, kubelet log on that node.
A node fails; a StatefulSet pod using an RWO volume must start on node B.
- KCM (node lifecycle controller) marks node A
NotReady; after the eviction timeout its pods get deletion timestamps — but the dead kubelet cannot confirm termination. - KCM (StatefulSet controller) will not create a replacement until the old pod object is fully gone — RWO safety by refusing to run two instances.
- User (or a fencing mechanism) force-deletes the pod; the replacement is created and Sched places it on node B.
- Kubelet on B waits for the volume, but the attach-detach controller still records it attached to A and refuses to detach while it might be in use.
- KCM (attach-detach controller) emits the classic event: "Multi-Attach error for volume".
- KCM proceeds once safe: the old pod is gone, and unmount confirmation is skipped after the force-detach timeout (
maxWaitForUnmountDuration, 6 minutes), since node A cannot answer. - Att calls
ControllerUnpublishVolume(detach from A), then a new VolumeAttachment for B triggersControllerPublishVolume. - Kubelet on B runs
NodeStageVolumeandNodePublishVolume; the pod starts. Total delay: commonly 6–8+ minutes without intervention.
sequenceDiagram
autonumber
participant API
participant KCM
participant Att
participant CSI
participant Kubelet
Note over API: node A dead
Kubelet->>API: waiting for volume
KCM->>API: Multi-Attach error
Note over KCM: waits, force detaches
KCM->>API: delete VolumeAttachment
Att->>CSI: ControllerUnpublishVolume
Att->>CSI: ControllerPublishVolume
Kubelet->>CSI: NodeStage, NodePublish
Kubelet->>API: pod Running
Figure 8.3 — detach must be proven safe before attach; the 6-minute force-detach timeout dominates failover time.
Where this can fail
- Symptom: replacement pod
Pending/ContainerCreatingwith Multi-Attach errors for 6+ minutes. Cause: normal force-detach wait for an unreachable node. Where to look: VolumeAttachment objects; consider node fencing orNonGracefulNodeShutdowntaint (out-of-service) to skip the wait safely. - Symptom: StatefulSet never creates the replacement. Cause: old pod stuck
Terminatingon the dead node; the controller deliberately waits. Where to look: pod deletion timestamp; force-delete only when the node is confirmed dead. - Symptom: filesystem corruption after failover. Cause: someone force-detached while node A was actually alive and writing. Where to look: fencing discipline — this is why the safeguards exist.
- Symptom: attach to B fails after successful detach. Cause: backend attach limits on B, or topology mismatch. Where to look: attacher logs, node's allocatable attachments.
Q 8.1 — Why does CSI exist, and what did it replace?
Answer. It decouples storage drivers from the Kubernetes release cycle. In-tree drivers were vendor code compiled into core Kubernetes: bugs required Kubernetes patches, features waited on releases, and the core repo carried every vendor's dependencies. CSI defines a versioned gRPC contract (Identity/Controller/Node) that drivers implement out-of-tree; sidecars bridge the Kubernetes API to the contract. In-tree cloud drivers were migrated to CSI equivalents.
Strong answers also mention: the same decoupling pattern as CNI and CRI — Kubernetes standardizes interfaces, not implementations; CSI is orchestrator-neutral.
Q 8.2 — What is the difference between NodeStageVolume and NodePublishVolume?
Answer. Stage happens once per volume per node: the node plugin formats (if needed) and mounts the device at a global staging path. Publish happens once per pod: a bind-mount from the staging path into that pod's volume directory. The split lets multiple pods on one node share a single device mount, and makes teardown ordering explicit — unpublish per pod, unstage when the last pod leaves.
Strong answers also mention: kubelet's volume manager drives both calls and reconciles actual vs desired mounts; block-mode volumes skip the filesystem and publish the raw device.
Q 8.3 — Walk through the sidecars and what each one does.
Answer. external-provisioner: watches PVCs, calls CreateVolume/DeleteVolume. external-attacher: watches VolumeAttachments, calls ControllerPublishVolume/Unpublish. external-resizer: PVC size changes → ControllerExpandVolume. external-snapshotter: VolumeSnapshots → CreateSnapshot. node-driver-registrar: registers the node plugin's socket with the kubelet. The point: the driver stays Kubernetes-ignorant; sidecars own all API-server interaction.
Strong answers also mention: VolumeAttachment objects are created by the attach-detach controller in kube-controller-manager — the attacher only consumes them.
Q 8.4 — Why does WaitForFirstConsumer exist? What breaks with Immediate binding?
Answer. Immediate provisions the moment the PVC appears — before any pod exists, so the provisioner must guess placement. In a multi-zone cluster it can create the disk in zone A while the pod's other constraints force zone B: the pod is then unschedulable, since the PV's nodeAffinity pins it to zone A. WFFC inverts the order: the scheduler picks the node first, and CreateVolume receives that node's topology. It also avoids paying for volumes no pod ever uses.
Strong answers also mention: WFFC makes the scheduler and provisioner cooperate through the selected-node annotation on the PVC — scheduling and provisioning are otherwise independent loops.
Q 8.5 — Why are attach and mount separate steps with separate components?
Answer. They act on different scopes. Attach (ControllerPublishVolume) is a backend control-plane operation — "connect disk X to VM Y" — done centrally by the controller plugin, which can serialize safely and needs no node access. Mount (NodeStage/NodePublish) manipulates the node's devices, which only that node's plugin can do. The VolumeAttachment object is the handoff: the attacher records success, kubelet waits for it before mounting. Central attach also enables multi-attach prevention.
Strong answers also mention: some storage (NFS-style) needs no attach at all — the driver simply doesn't declare the PUBLISH_UNPUBLISH capability and kubelet skips the wait.
Q 8.6 — A colleague says "ReadWriteOnce means only one pod can use the volume." Correct them.
Answer. RWO is per-node, not per-pod: any number of pods on the same node can mount an RWO volume — stage once, publish per pod. The per-pod guarantee is RWOP, enforced at scheduling time. The distinction bites in incidents: two replicas that landed on one node "work", then break when one reschedules elsewhere — a latent misconfiguration becomes a Multi-Attach outage.
Strong answers also mention: access modes are declared capabilities checked at bind/attach time, not I/O enforcement — the filesystem doesn't know about them.
Q 8.7 — Symptom: a node died; its StatefulSet pod is stuck ContainerCreating on a new node with "Multi-Attach error". Explain and fix.
Answer. The volume is RWO and still recorded as attached to the dead node. The attach-detach controller refuses to detach until it's safe: the old pod gone, unmount confirmed — impossible with a dead kubelet, so it waits the 6-minute force-detach timeout. Fix: confirm the node is truly dead; force-delete the stale pod; if the node won't return, delete the Node object or apply the out-of-service taint (non-graceful shutdown handling), which lets the controller skip the wait safely. Then detach → attach → stage → publish proceeds (Flow 25). Prevention: automated fencing — and never force-detach a possibly-alive writer; that's how filesystems get corrupted.
Strong answers also mention: the delay is a correctness feature — Kubernetes chooses potential unavailability over split-brain writes to one disk.
Q 8.8 — Design: your PVC is Pending. Enumerate the distinct causes and how you'd distinguish them in minutes.
Answer. kubectl describe pvc events distinguish nearly all: (1) WFFC waiting for a consumer — create/schedule the pod. (2) No provisioner — the StorageClass names a driver that isn't running; check the controller plugin Deployment. (3) Provisioning failing — CreateVolume errors in events/provisioner logs: quota, credentials, bad parameters. (4) No StorageClass and no default class — the claim waits for static PVs. (5) Static binding mismatch — a PV exists but capacity/access mode don't match. (6) Topology unsatisfiable under WFFC — no schedulable node can host the volume; the pod's scheduler events show it.
Strong answers also mention: the pod's events matter as much as the PVC's under WFFC, because the scheduler drives the first step.
- "The kubelet provisions and attaches volumes." Kubelet only stages and publishes on its node; the external-provisioner provisions, the attach-detach controller plus external-attacher attach.
- "The CSI driver watches PVCs." Drivers implement gRPC only; sidecars watch the API. That separation is the design.
- "RWO = one pod." Per-node, not per-pod; RWOP is the per-pod mode.
- "Attach and mount are the same step." Different scope, component, and failure modes — VolumeAttachment status is the boundary to check.
- "Kubernetes is just slow to fail over StatefulSets." The 6-minute force-detach wait is deliberate split-brain protection; the fix is fencing, not blind force-deletes.
- "Expansion is instant once the PVC is edited." Controller expand first, then the node filesystem grow — often only when a pod (re)mounts.