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
| Volume | Data lives as long as | Use it for | Watch out |
|---|---|---|---|
emptyDir | The Pod (survives container restarts) | Scratch space, sharing files between containers in one Pod | Gone on reschedule. medium: Memory counts against the container's memory limit |
configMap, secret, projected, downwardAPI | The source object | Injecting config, credentials, Pod metadata as files | Read-only. A subPath mount never sees updates |
hostPath | The node | Node agents that need /var/log or a runtime socket | Ties the Pod to one node's disk and is a security risk. Avoid for app data |
Generic ephemeral | The Pod, but provisioned through a StorageClass | Per-Pod scratch that needs a real disk or a specific size | The PVC is created and deleted with the Pod |
persistentVolumeClaim | The claim, then the PV's reclaim policy decides | Databases, uploads, anything that must survive the Pod | Namespaced: 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.volumeNameon the PVC (and optionallyspec.claimRefon the PV to reserve it). - A
selector.matchLabelson 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 pvorkubectl create pvc. Copy a skeleton from the docs page and edit it. hostPathPVs only work if the Pod lands on the node that has the directory. For real node-local disks use alocalPV, which requiresspec.nodeAffinityso 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
| Mode | Short | Means | Typical backend |
|---|---|---|---|
ReadWriteOnce | RWO | Read-write, mounted by one node. Several Pods on that node can share it | Block disks, hostPath, local |
ReadOnlyMany | ROX | Read-only from many nodes | NFS, file shares, cloned disks |
ReadWriteMany | RWX | Read-write from many nodes | NFS, CephFS, cloud file services |
ReadWriteOncePod | RWOP | Read-write by one Pod in the whole cluster | CSI 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: trueon thevolumeMountsentry or on thepersistentVolumeClaimvolume 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 atmountPaththroughvolumeMounts.Block: the Pod gets a raw device. The container usesvolumeDeviceswith adevicePathsuch as/dev/xvdainstead ofvolumeMounts. Used by databases that manage their own on-disk format.volumeModeis part of binding: aBlockclaim won't bind aFilesystemPV.kubectl get pv -o wideshows theVOLUMEMODEcolumn.
What happens when the claim is deleted
| Policy | PV object | Backing storage | Default for |
|---|---|---|---|
| Retain | Kept, phase Released | Kept with its data | PVs you create by hand |
| Delete | Removed | Removed by the plugin or CSI driver | Dynamically provisioned PVs (inherited from the class, which defaults to Delete) |
- A
ReleasedPV still hasspec.claimRefpointing 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 toAvailable. - 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.
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
A claim without storageClassName gets the default class, and only PVs of that class (or new provisioned ones)
match it. Setting storageClassName: "" on the claim restricts it to classless PVs such as press-pv. A larger PV
binds fine, hostPath PVs bind like any other, and RWO is the most common static mode.
A Released PV still references the old claim's UID in spec.claimRef, so no other claim can bind it. Clearing it
returns the PV to Available with the data intact. Recycle would wipe the data and is deprecated. Recreating the
PV works only if you redo it carefully and isn't needed. The default-class annotation belongs on StorageClasses.
RWO limits the volume to one node, and both Pods were on that node. RWOP limits it to one Pod cluster-wide and is
enforced at mount time, so the second Pod waits. Pair it with the Recreate strategy so the rollout doesn't stall.
Making the new Pods read-only breaks the service, ROX doesn't allow writes, and the reclaim policy is unrelated.
Drill
kubectl config use-context cka-storage
- Create a PersistentVolume
reef-pv: 1Gi,ReadWriteOnce, reclaim policyRetain, storage classslow-lab, backed byhostPath/srv/reef. - In namespace
coral, create a PVCreef-claimrequesting 512Mi that binds toreef-pv. - Run a Pod
reef-writer(imagebusybox:1.37) incoralthat mounts the claim at/dataand writes the date into/data/stamp.txt. - Delete the Pod and the PVC, then make
reef-pvavailable for a new claim without losing the file.
# reef.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: reef-pv
spec:
capacity:
storage: 1Gi
accessModes: ["ReadWriteOnce"]
persistentVolumeReclaimPolicy: Retain
storageClassName: slow-lab
hostPath:
path: /srv/reef
type: DirectoryOrCreate
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: reef-claim
namespace: coral
spec:
storageClassName: slow-lab
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 512Mi
---
apiVersion: v1
kind: Pod
metadata:
name: reef-writer
namespace: coral
spec:
containers:
- name: w
image: busybox:1.37
command: ["sh", "-c", "date > /data/stamp.txt && sleep 3600"]
volumeMounts:
- name: d
mountPath: /data
volumes:
- name: d
persistentVolumeClaim:
claimName: reef-claimk create ns coral # skip if it exists
k apply -f reef.yaml
k get pv reef-pv; k get pvc -n coral reef-claim # both Bound
k exec -n coral reef-writer -- cat /data/stamp.txt
k delete pod -n coral reef-writer
k delete pvc -n coral reef-claim
k get pv reef-pv # STATUS Released
k patch pv reef-pv --type=merge -p '{"spec":{"claimRef":null}}'
k get pv reef-pv # STATUS AvailableThe class name slow-lab doesn't need a StorageClass object for static binding: it only has to be identical on
both sides. The file stays in /srv/reef on the node because the policy is Retain.
Further reading
Domain 4 · Storage
10% of the exam. Creating PersistentVolumes, PersistentVolumeClaims and StorageClasses, getting a claim to bind, and mounting it into a Pod.
StorageClasses and dynamic provisioning
How a StorageClass and its CSI driver create PVs on demand, setting the default class, Immediate vs WaitForFirstConsumer binding, allowVolumeExpansion and growing a claim, and which fields you can't change later.