The CSI node plugin — Stage, Publish, Format, Mount
What you'll learn
- Describe the node plugin's responsibilities in detail
- Identify the gRPC operations: NodeStageVolume, NodePublishVolume, NodeUnstageVolume, NodeUnpublishVolume, NodeExpandVolume
- Trace the staging path and the bind-mount into the Pod
- Apply the production pattern for node plugin verification
Prerequisites
Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16
The CSI node plugin is the per-node operator that formats, mounts, and unmounts volumes. This lesson walks the operations, the staging path, the bind-mount into the Pod, and the production discipline.
Node plugin responsibilities
The node plugin implements the CSI Node Service:
| Operation | Purpose | When called |
|---|---|---|
| NodeStageVolume | Format (if needed) and mount to staging path | Before Pod start |
| NodeUnstageVolume | Unmount from staging path | After Pod termination |
| NodePublishVolume | Bind-mount staging path to Pod’s target path | Before Pod start |
| NodeUnpublishVolume | Unmount from Pod’s target path | After Pod termination |
| NodeExpandVolume | Resize the filesystem | After controller expansion |
| NodeGetCapabilities | Return node-level capabilities (e.g., stage, expand) | On kubelet startup |
| NodeGetInfo | Return node identity (nodeId) | On kubelet startup |
sequenceDiagram
participant K as kubelet
participant NP as CSI node plugin
participant BE as Block device
participant S as Staging path
participant P as Pod target path
Note over K,NP: Staging
K->>NP: NodeStageVolume (volumeHandle, stagingPath)
NP->>BE: format (if needed) and mount to stagingPath
BE-->>NP: mounted at stagingPath
NP-->>K: success
Note over K,NP: Publishing
K->>NP: NodePublishVolume (stagingPath, targetPath)
NP->>S: bind-mount to targetPath
NP-->>K: success
Note over K,NP: Unpublishing (reversed)
K->>NP: NodeUnpublishVolume (targetPath)
NP->>P: unmount
Note over K,NP: Unstaging (reversed)
K->>NP: NodeUnstageVolume (stagingPath)
NP->>S: unmount
The staging path
The staging path is a node-local directory where the CSI node plugin formats and mounts the volume:
/var/lib/kubelet/plugins/<csi-driver>.csi.k8s.io/
pvc-<pvc-uid>/
mount # the mounted filesystem
The staging path is shared between NodeStageVolume (which mounts the block device here) and NodePublishVolume (which bind-mounts from here to the Pod). The two-step process allows multiple Pods on the same node to share the same volume (one stage, multiple publish).
NodeStageVolume in detail
NodeStageVolume is the operation that formats and mounts the volume to the staging path:
message NodeStageVolumeRequest {
string volume_handle = 1; // from PV
string publish_context = 2; // from ControllerPublishVolume
string staging_target_path = 3; // the staging path
VolumeCapability volume_capability = 4;
map<string, string> secrets = 5;
}
The node plugin:
- Receives the request from the kubelet.
- Identifies the block device (from
publish_context, e.g.,/dev/xvdba). - Formats the device if it is not yet formatted (e.g.,
mkfs.ext4). - Mounts the device to the staging path.
- Returns success.
If the device is already formatted (idempotent), the plugin skips formatting and mounts.
NodePublishVolume in detail
NodePublishVolume is the operation that bind-mounts the staging path to the Pod’s target path:
message NodePublishVolumeRequest {
string volume_handle = 1;
string publish_context = 2;
string staging_target_path = 3;
string target_path = 4; // Pod's mountPath
VolumeCapability volume_capability = 5;
bool readonly = 6;
}
The node plugin:
- Receives the request from the kubelet.
- Bind-mounts the staging path to the target path
(e.g.,
/var/lib/kubelet/pods/<pod-uid>/volumes/...). - The kubelet then bind-mounts the target path into the container’s mount namespace.
The bind-mount chain:
block device
-> staging path (/var/lib/kubelet/plugins/<csi>/pvc-<uid>/mount)
-> kubelet target path (/var/lib/kubelet/pods/<pod-uid>/volumes/...)
-> container mount path (/var/lib/app)
NodeExpandVolume in detail
NodeExpandVolume resizes the filesystem after the controller has expanded the underlying volume:
message NodeExpandVolumeRequest {
string volume_handle = 1;
string volume_path = 2;
CapacityRange capacity_range = 3;
string staging_target_path = 4;
}
The node plugin:
- Receives the request from the kubelet.
- Resizes the filesystem (e.g.,
resize2fs,xfs_growfs). - Returns success.
For online expansion, the Pod is running during the resize. For offline expansion, the Pod is stopped.
The DaemonSet deployment
The node plugin is a DaemonSet:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: ebs-csi-node
namespace: kube-system
spec:
template:
spec:
hostNetwork: true # <-- required for node plugin
containers:
- name: ebs-csi-node
image: amazon/aws-ebs-csi-driver:latest
securityContext:
privileged: true # <-- required for mount operations
mountPropagation: Bidirectional
The node plugin needs:
- hostNetwork: true: to communicate with the kubelet on localhost.
- privileged: true: to perform mount operations that require CAP_SYS_ADMIN.
- mountPropagation: Bidirectional: to allow mount propagation into the Pod.
The kubelet-CSI communication
The kubelet communicates with the CSI node plugin via a Unix domain socket:
/var/lib/kubelet/plugins/<csi-driver>.csi.k8s.io/
csi.sock
The kubelet opens a connection to the socket, makes gRPC calls (NodeStageVolume, NodePublishVolume), and closes the connection. The CSI node plugin must be running and the socket must be present for the kubelet to mount volumes.
The node plugin verification
# Substitute your own driver's short name (ebs.csi.aws.com -> ebs):
CSI_DRIVER=ebs
# 1. The node plugin is running on every node
kubectl -n kube-system get pods -l app="$CSI_DRIVER",role=node -o wide
# READY STATUS NODE
# 1/1 Running node-1
# 1/1 Running node-2
# 1/1 Running node-3
# 2. The socket is present
ls /var/lib/kubelet/plugins/"$CSI_DRIVER".csi.k8s.io/csi.sock
# 3. The CSIDriver object is registered
kubectl get csidriver
# NAME ATTACHREQUIRED MAXSTORAGE_PERNODE
# ebs.csi.aws.com true 0
# 4. Test mount a PVC
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: csi-test
spec:
containers:
- name: test
image: busybox
command: ["sleep", "3600"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: test-pvc
EOF
Quiz
Knowledge check · 4 questions
Q1. Which CSI node plugin operation is responsible for formatting and mounting the volume to the staging path?
Q2. The CSI node plugin must run as a DaemonSet on every node where volumes will be mounted.
Q3. A Pod is stuck in ContainerCreating on node-2. The PVC is Bound. Walk through the CSI node plugin diagnostic.
Pod stuck for 5 minutes. PVC Bound. Events show "FailedMount" or "FailedAttach". The CSI node plugin Pod is running on node-2. Other nodes are working correctly.
Q4. Explain the staging path in the CSI node plugin and why it is separated from the Pod's target path.
Passing score: 75%. Answers are checked in this browser.
Production discipline
- The node plugin runs on every node. A DaemonSet ensures one Pod per node.
privileged: trueandhostNetwork: trueare required. The node plugin needs elevated privileges for mount operations.- The socket is the diagnostic goldmine. A missing socket blocks mounts; verify it on every node.
- Monitor NodeStageVolume and NodePublishVolume latency. Mount failures are user-visible; latency spikes are early signals.
- Test mount on every node. A new node may be missing the node plugin if the DaemonSet did not roll.