k8wiz
All graphs

Kubernetes graph

PVC, StorageClass and CSI: how a cloud disk ends up in your Pod

Dynamic provisioning with the AWS EBS CSI driver, object by object: StatefulSet, PVC, StorageClass, PV, CSIDriver, VolumeAttachment and the controller and node plugins that create, attach and mount the disk.

A PersistentVolumeClaim is a request for storage. Nothing in it says where the disk comes from. The StorageClass names a CSI driver, and that driver's controller creates a real disk in the cloud and a PersistentVolume that points at it. The kubelet and the driver's node plugin then attach and mount the disk on the node that runs the Pod.

The example uses the AWS EBS CSI driver. Swap ebs.csi.aws.com for pd.csi.storage.gke.io or disk.csi.azure.com and the objects stay the same. The volume binding mode matters more than anything else here, because a disk lives in one availability zone and the Pod has to run in that zone too.

Interactive graph

Click a resource to see what it does, or follow the setup step by step.

You write itCreated by a controllerComes with an add-onOutside the clustercreatesreferencestraffic

How it works

All 11 resources and what they do

StatefulSet postgresapps/v1
Runs one Pod per replica with a stable name, postgres-0, and creates one PVC per Pod from volumeClaimTemplates. If the Pod is recreated, it gets the same PVC back.
Pod postgres-0v1
Created by the StatefulSet. Its volumes section points at the claim data-postgres-0. It stays in ContainerCreating until the disk is attached and mounted.
PersistentVolumeClaim data-postgres-0v1
Created by the StatefulSet from its template, named <template>-<pod>. It asks for 50Gi of ReadWriteOnce storage of class gp3. It shows Pending until a PV is bound to it.
StorageClass gp3storage.k8s.io/v1
Cluster-scoped. Names the CSI driver in provisioner and passes it parameters. WaitForFirstConsumer delays the disk until the Pod is scheduled, so the disk is created in the Pod's zone.
CSIDriver ebs.csi.aws.comstorage.k8s.io/v1
Registers the driver with Kubernetes and says how to treat it, for example attachRequired: true, which makes Kubernetes create a VolumeAttachment before mounting.
Deployment ebs-csi-controllerapps/v1
The driver's controller plugin, with sidecars from the Kubernetes CSI project. external-provisioner watches PVCs and calls CreateVolume. external-attacher watches VolumeAttachments and calls ControllerPublishVolume. It needs AWS permissions to create and attach EBS volumes.
PersistentVolume pvc-3f1c9a7e-…v1
Created by external-provisioner once the disk exists. csi.volumeHandle is the EBS volume ID, and nodeAffinity pins it to the disk's zone, so the scheduler never puts a Pod using it elsewhere.
EBS volume vol-0a1b2c3d4e5f67890
The actual disk, a 50 GiB encrypted gp3 volume in eu-west-1a. It is billed whether a Pod uses it or not.
Node ip-10-0-1-23v1
The EC2 instance the scheduler picked for postgres-0, in zone eu-west-1a. The EBS volume gets attached to this instance as a block device.
VolumeAttachment csi-8e2f…storage.k8s.io/v1
Created by the attach/detach controller in kube-controller-manager. It asks for this PV to be attached to this node. external-attacher does the attach and sets status.attached: true.
DaemonSet ebs-csi-nodeapps/v1
The driver's node plugin, one Pod on every node. The kubelet calls it to format the device the first time (NodeStageVolume) and to mount it into the Pod's directory (NodePublishVolume).

Common mistakes

  • volumeBindingMode: Immediate creates the disk as soon as the claim exists, in whatever zone the driver picks. If the Pod then can't be scheduled in that zone, it stays Pending with a volume node affinity conflict event.
  • ReadWriteOnce means one node at a time. When a Pod moves to another node, the old attachment must be removed first, and if the old node is unreachable you'll see Multi-Attach error until the attach/detach controller force-detaches it, by default after about six minutes.
  • Resizing works only with allowVolumeExpansion: true. Then you raise spec.resources.requests.storage on the PVC. Editing volumeClaimTemplates in a StatefulSet is not allowed, so resize each PVC directly.
  • A claim stuck in Pending says why in its events. kubectl describe pvc data-postgres-0 -n db shows messages from external-provisioner, such as missing IAM permissions, and the controller logs have the details.
  • Since Kubernetes 1.32 a StatefulSet can delete its claims for you with persistentVolumeClaimRetentionPolicy. Leave it at the default Retain for databases unless you want data gone when you scale down.

The manifests you write

Everything above that is marked "You write it", in one file. The rest comes from add-ons and controllers.

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  namespace: db
spec:
  serviceName: postgres
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:17
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: gp3
        resources:
          requests:
            storage: 50Gi
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: gp3
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  encrypted: "true"
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
reclaimPolicy: Delete

Keep going

More graphs