Asterrr's Handbook

PersistentVolumes and claims

Ephemeral vs persistent volume types, how a PVC binds to a PV, access modes including ReadWriteOncePod, Filesystem vs Block volume mode, Retain vs Delete reclaim policies and reusing a Released volume.

Exam tasks: 4.2 (volume types, access modes, reclaim policies), 4.3 (PersistentVolumes and PersistentVolumeClaims)

The decision: how long must this data live, who has to share it, and which PV and PVC fields have to agree before the claim binds?

Pick the volume by lifetime

VolumeData lives as long asUse it forWatch out
emptyDirThe Pod (survives container restarts)Scratch space, sharing files between containers in one PodGone on reschedule. medium: Memory counts against the container's memory limit
configMap, secret, projected, downwardAPIThe source objectInjecting config, credentials, Pod metadata as filesRead-only. A subPath mount never sees updates
hostPathThe nodeNode agents that need /var/log or a runtime socketTies the Pod to one node's disk and is a security risk. Avoid for app data
Generic ephemeralThe Pod, but provisioned through a StorageClassPer-Pod scratch that needs a real disk or a specific sizeThe PVC is created and deleted with the Pod
persistentVolumeClaimThe claim, then the PV's reclaim policy decidesDatabases, uploads, anything that must survive the PodNamespaced: the Pod and the PVC must be in the same namespace

Exam signal

"Two containers in the same Pod exchange files" means emptyDir. "Data must survive the Pod being deleted" means a PVC. If the task says "create a PersistentVolume", it wants the PV object itself, not just a claim.

How a claim binds

  • A PV is cluster-scoped, a PVC is namespaced. Binding is exclusive: one PV, one PVC, even if the PV is far bigger than the request. The claim reports the PV's full capacity.
  • The control loop picks the smallest matching PV. To force a specific one, set spec.volumeName on the PVC (and optionally spec.claimRef on the PV to reserve it).
  • A selector.matchLabels on the PVC limits candidates to labelled PVs. A claim with a selector is never dynamically provisioned.

Static PV, claim and Pod

apiVersion: v1
kind: PersistentVolume
metadata:
  name: tidepool-pv
  labels:
    tier: archive
spec:
  capacity:
    storage: 2Gi
  accessModes: ["ReadWriteOnce"]
  persistentVolumeReclaimPolicy: Retain   # the default for hand-made PVs
  storageClassName: manual
  volumeMode: Filesystem
  hostPath:                               # fine for a lab, not for production
    path: /srv/tidepool
    type: DirectoryOrCreate
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: tidepool-data
  namespace: harbor
spec:
  storageClassName: manual
  accessModes: ["ReadWriteOnce"]
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: tidepool
  namespace: harbor
spec:
  containers:
  - name: app
    image: busybox:1.37
    command: ["sh", "-c", "sleep 1d"]
    volumeMounts:
    - name: data
      mountPath: /var/tidepool
  volumes:
  - name: data
    persistentVolumeClaim:
      claimName: tidepool-data
  • There is no kubectl create pv or kubectl create pvc. Copy a skeleton from the docs page and edit it.
  • hostPath PVs only work if the Pod lands on the node that has the directory. For real node-local disks use a local PV, which requires spec.nodeAffinity so the scheduler knows where the disk is.

storageClassName left out

On a cluster with a default StorageClass, a claim with no storageClassName is assigned the default class. It then ignores your hand-made PV with no class, and a provisioner creates a fresh volume instead. To bind to a classless PV, set storageClassName: "" on the claim explicitly, or give both objects the same class name.

Access modes

ModeShortMeansTypical backend
ReadWriteOnceRWORead-write, mounted by one node. Several Pods on that node can share itBlock disks, hostPath, local
ReadOnlyManyROXRead-only from many nodesNFS, file shares, cloned disks
ReadWriteManyRWXRead-write from many nodesNFS, CephFS, cloud file services
ReadWriteOncePodRWOPRead-write by one Pod in the whole clusterCSI drivers only (stable since v1.29)
  • Access modes are a matching rule, not write protection. Only RWOP is actually enforced at mount time: a second Pod using an RWOP claim stays Pending.
  • A volume is mounted with one mode at a time, even if the PV lists several.
  • To mount read-only in a Pod, set readOnly: true on the volumeMounts entry or on the persistentVolumeClaim volume source.

Exam signal

"Make sure only one Pod can ever write to this volume, even on the same node" means ReadWriteOncePod. RWO doesn't stop a second Pod scheduled to the same node from mounting it.

Volume mode

  • Filesystem (default): the volume is formatted and mounted at mountPath through volumeMounts.
  • Block: the Pod gets a raw device. The container uses volumeDevices with a devicePath such as /dev/xvda instead of volumeMounts. Used by databases that manage their own on-disk format.
  • volumeMode is part of binding: a Block claim won't bind a Filesystem PV. kubectl get pv -o wide shows the VOLUMEMODE column.

What happens when the claim is deleted

PolicyPV objectBacking storageDefault for
RetainKept, phase ReleasedKept with its dataPVs you create by hand
DeleteRemovedRemoved by the plugin or CSI driverDynamically provisioned PVs (inherited from the class, which defaults to Delete)
  • A Released PV still has spec.claimRef pointing at the old claim's UID, so no new claim can bind. After checking or wiping the data, clear the reference: kubectl patch pv tidepool-pv --type=merge -p '{"spec":{"claimRef":null}}'. The PV goes back to Available.
  • Change a policy on a live PV with kubectl patch pv <name> -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'. Do this before deleting a dynamically provisioned claim whose data you need.

Legacy: use Retain plus manual cleanup, or dynamic provisioning instead

The Recycle policy ran rm -rf on the volume and made it Available again. It is deprecated, only nfs and hostPath ever supported it, and you shouldn't choose it on the exam unless a task names it explicitly.

Deleting a PVC that a Pod still uses

kubectl delete pvc on a mounted claim doesn't fail: the claim sits in Terminating because of the kubernetes.io/pvc-protection finalizer until every Pod using it is gone. A bound PV has the matching kubernetes.io/pv-protection finalizer. Delete the Pod (or scale the workload to zero) first; don't strip the finalizers.

1 : 1
A PV binds to exactly one PVC, regardless of spare capacity.
Retain
Default reclaim policy of a PV you create by hand.
Delete
Default reclaim policy of a StorageClass, and so of dynamically provisioned PVs.
v1.29
ReadWriteOncePod became stable. CSI volumes only.
4 phases
PV phases: Available, Bound, Released, Failed. Pending is a PVC status.

Legacy: use CSI drivers instead

In-tree cloud volume types such as awsElasticBlockStore, azureDisk and gcePersistentDisk are deprecated and redirected to their CSI drivers by CSI migration; others like glusterfs, cephfs and rbd are gone entirely. Write new PVs and classes against a CSI driver.

Scenarios

Scenario
In namespace orchard, PVC 'press-logs' requests 500Mi with accessModes ReadWriteOnce and no storageClassName. An admin created PV 'press-pv' (1Gi, ReadWriteOnce, no storageClassName, hostPath). The cluster has a default StorageClass 'local-path'. The claim is Bound, but not to press-pv. What explains this, and what's the fix?
Scenario
A team deleted PVC 'ledger-db' to rebuild it. Its PV 'pv-ledger' has reclaim policy Retain and now shows status Released. They recreate an identical PVC, but it stays Pending. What should you do so the new claim binds to the existing data?
Scenario
A licensing service must never run two writers against its data volume. The Deployment uses a PVC with ReadWriteOnce on a CSI-backed class. During a rolling update both the old and new Pods landed on the same node and both wrote to the volume. Which change enforces a single writer?

Drill

kubectl config use-context cka-storage

  1. Create a PersistentVolume reef-pv: 1Gi, ReadWriteOnce, reclaim policy Retain, storage class slow-lab, backed by hostPath /srv/reef.
  2. In namespace coral, create a PVC reef-claim requesting 512Mi that binds to reef-pv.
  3. Run a Pod reef-writer (image busybox:1.37) in coral that mounts the claim at /data and writes the date into /data/stamp.txt.
  4. Delete the Pod and the PVC, then make reef-pv available for a new claim without losing the file.

Further reading

On this page