Kubernetes PVC Generator

Build a PersistentVolumeClaim, and find out before you apply it whether the storage behind it can actually serve what you asked for. Tell it which backend the class uses and it says plainly when the claim would sit in Pending forever.

Claim
Storage class

The class is where volumeBindingMode, allowVolumeExpansion and reclaimPolicy live. None of them can be set on the claim.

Bind to a volume that already exists

Leave both blank for normal dynamic provisioning. These only apply when you are matching a pre-created PersistentVolume.

Check it against your storage

Not part of the output. Used to tell you whether this claim can bind at all, and what happens when you delete it.

Pod snippet

Emitted as comments under the claim, so the output stays one document you can apply as it is.

pvc.yaml

updates as you type

    Common mistakes

    These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.

    1. Requesting ReadWriteMany on block storage

      EBS, GCE PD and Azure Disk are ReadWriteOnce. The claim stays Pending with an event most people do not look at.

      Instead:Check what the storage class supports. ReadWriteMany needs a file protocol such as EFS or NFS.

    2. Assuming a PVC can be shrunk

      Expansion is supported where the storage class allows it. Shrinking is not supported at all.

      Instead:Size with room, or migrate to a new claim.

    3. Leaving the default reclaim policy on production data

      Many dynamic provisioners default to Delete, so removing the PVC destroys the volume and its data.

      Instead:Set Retain for anything that matters.

    What a PersistentVolumeClaim actually controls, and what it does not

    A PVC asks for storage of a given size and access mode. Almost everything that decides whether that request can be met sits somewhere else: on the StorageClass, and in what the backing disk or filesystem is physically able to do. That split is why the common failures here are silent. A claim that cannot be satisfied does not fail, it stays Pending, and the reason lives in an event that is garbage collected after an hour.

    accessModes describe the storage, they do not ask it for anything

    An access mode is matched against what a volume can do. It is not a capability the cluster can add. ReadWriteMany on a class backed by AWS EBS, Google Persistent Disk or an Azure managed disk can never be satisfied, because those attach to one machine at a time. The claim is accepted, nothing is provisioned, and kubectl get pvc shows Pending with no cause next to it.

    accessModes:
      - ReadWriteMany
    
    on EBS / GCE PD / Azure Disk:
      kubectl get pvc        Pending, no reason shown
      kubectl describe pvc   ProvisioningFailed, until the
                             event is garbage collected
    
    ReadWriteMany needs a shared filesystem:
      EFS, FSx, Azure Files, CephFS, NFS

    ReadWriteOnce is one node. ReadWriteOncePod is one pod.

    ReadWriteOnce constrains attachment, not usage: several pods scheduled onto the same node can all mount the volume and all write to it. The pod that fails is the one on a second node, and it fails as a Multi-Attach error with the pod stuck in ContainerCreating. That is why this surfaces during a Deployment rolling update, where the replacement pod starts before the old one is gone. ReadWriteOncePod is the mode that genuinely means one pod, and the API server enforces it.

    ReadWriteOnce      one NODE attaches it
                       3 pods on that node all write to it
    ReadWriteOncePod   one POD, enforced by the API server
                       1.22 alpha, 1.27 default on, 1.29 GA
                       needs a CSI driver, no in-tree plugin
    ReadOnlyMany       many nodes, read only
    ReadWriteMany      many nodes, all writing
    
    Deployment + RWO + RollingUpdate:
      new pod on node-b   Multi-Attach error for volume
      old pod on node-a   still holding it
      fix: strategy.type: Recreate, or a StatefulSet

    Immediate binding can put the disk where the pod cannot go

    volumeBindingMode is on the StorageClass. Immediate provisions the volume the moment the claim exists, before anything knows where the pod will be scheduled. In a multi-zone cluster the volume can land in a zone with no room for the pod, and a zonal disk cannot be moved. The pod then stays Pending on a volume node affinity conflict while the claim reports Bound, which is why this gets investigated as a scheduling problem. WaitForFirstConsumer is the answer and should be the default choice.

    volumeBindingMode: Immediate
      1. PVC created       disk provisioned in eu-west-1a
      2. pod scheduled     only eu-west-1b has capacity
      3. pod Pending       1 node(s) had volume node
                           affinity conflict
      PVC says Bound throughout.
    
    volumeBindingMode: WaitForFirstConsumer
      1. PVC created       Pending, on purpose
      2. pod scheduled     eu-west-1b
      3. disk provisioned  eu-west-1b

    Omitted, empty and named storageClassName are three different things

    Omitting the field lets the DefaultStorageClass admission plugin write the cluster default into the object as it is created, and that value is then fixed: changing the cluster default later does nothing to claims that already exist. Setting the field to the empty string means no class at all, so nothing is provisioned and the claim binds only to a PersistentVolume that already exists. The two are frequently treated as the same "no opinion" and they are opposites.

    # omitted: the default class is written in at
    # creation time, and no default class means Pending
    spec:
      resources:
        requests:
          storage: 10Gi
    
    # empty string: no class, no provisioning. Binds only
    # to a pre-created PV, or waits forever.
    spec:
      storageClassName: ""
    
    # named: independent of whatever the default is today
    spec:
      storageClassName: gp3

    Growing works if the class allows it, shrinking never does

    Expansion requires allowVolumeExpansion: true on the StorageClass. With it, editing spec.resources.requests.storage upward grows the volume, and where the driver cannot resize a mounted filesystem the claim waits on a FileSystemResizePending condition until the pod restarts. Shrinking is rejected by the API server on every class. The requested size is also a minimum rather than an allocation: providers round up to their own increments and bill the rounded figure.

    allowVolumeExpansion: true
      10Gi -> 100Gi   allowed, edit requests.storage
      100Gi -> 10Gi   rejected, always
    
    reclaimPolicy is on the class, not the claim
      Delete   kubectl delete pvc data
               -> PV and disk destroyed, no undo
      Retain   kubectl delete pvc data
               -> PV goes Released and stays, still
                  billing, until spec.claimRef is
                  cleared by hand

    A claim stuck in Terminating is still referenced by a pod

    Deleting a PVC that is in use returns success and then leaves the object in Terminating. The kubernetes.io/pvc-protection finalizer holds it there while any pod still lists the claim in spec.volumes, whether or not that pod is running. The fix is to find and delete the pod. Removing the finalizer by hand deletes the claim out from under a pod that still has the volume mounted, and on a Delete class that destroys the disk.

    kubectl delete pvc data
      persistentvolumeclaim "data" deleted
    kubectl get pvc data
      data   Terminating
    
    metadata:
      finalizers:
        - kubernetes.io/pvc-protection
    
    # find what is holding it:
    kubectl get pods -o json | jq -r '
      .items[] | select(
        .spec.volumes[]?.persistentVolumeClaim.claimName
        == "data") | .metadata.name'