PersistentVolumes and PersistentVolumeClaims
The PV and PVC objects, the fields that decide whether a claim binds, access modes and reclaim policies, PV phases, and how to bring a Released volume back into service.
After this lesson
- 01You can write a hostPath PersistentVolume and a matching claim, mount the claim in a Pod and confirm the binding.
- 02You can read a Pending claim and create exactly the PersistentVolume that satisfies it.
- 03You can explain Retain versus Delete, recognize a Released volume and make it bindable again without losing its data.
This lesson covers two of the three storage competencies: Manage persistent volumes and persistent volume claims and the access-mode and reclaim-policy half of Configure volume types, access modes and reclaim policies. The exam asks for these in three shapes: create a PV, a PVC and a Pod that uses it; make a Pending claim bind; or clean up after a deleted claim. All three come down to a handful of fields that must match, so this lesson is mostly about those fields.
Two objects, two owners
A PersistentVolume (PV) is a cluster-scoped object that describes a piece of storage: how big it is, how it can be accessed, where it lives, and what happens to it when nobody claims it anymore. An administrator creates PVs by hand (static provisioning) or a StorageClass creates them on demand (dynamic provisioning, next lesson). A PersistentVolumeClaim (PVC) is namespaced and is what an application owner writes: a request for storage of a size and access mode. The control plane binds one claim to one volume, and a Pod mounts the claim by name, never the PV. That split is the point: the Pod spec stays the same whether the PV is a hostPath on a lab node or a disk in a cloud account.
apiVersion: v1
kind: PersistentVolume
metadata:
name: st-data-pv
spec:
capacity:
storage: 1Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: st-manual
hostPath:
path: /opt/st-data
type: DirectoryOrCreate
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: st-local-pv
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: st-local
local:
path: /mnt/disks/ssd1
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- cka-worker2capacity.storage is the size the controller compares against claims. volumeMode is Filesystem unless you say Block, in which case the Pod gets a raw device through volumeDevices instead of a mount. storageClassName is just a string on a static PV; it does not need a StorageClass object to exist, it only needs to equal the claim's. The source (hostPath, local, nfs, csi) is what the kubelet mounts. local is hostPath done properly: the API requires a nodeAffinity so the scheduler puts Pods on the node that has the disk, and the kubelet refuses to mount it elsewhere. hostPath PVs have no such guard, which is fine on a single node and silently wrong on a cluster.
Access modes
A PV lists every mode it supports; a claim asks for the modes it needs, and every requested mode must appear on the PV. Access modes are a matching rule, not an enforcement mechanism: a hostPath PV happily declares ReadWriteMany although nothing stops two nodes from seeing different directories, and once a volume is mounted the mode does not make it read-only. Only one mode is active per mount. In practice the exam says "one node" or "many nodes" and expects you to translate.
Reclaim policies
persistentVolumeReclaimPolicy decides what happens when the claim is deleted. Retain keeps the PV and its data; the PV moves to Released and an administrator cleans up or hands it to a new claim by hand. Delete removes the PV object and asks the provisioner to delete the storage behind it. Recycle is deprecated and needs a recycler nobody runs anymore. A statically created PV defaults to Retain; a dynamically provisioned PV inherits its StorageClass's reclaimPolicy, which defaults to Delete. The policy is mutable, so "make sure the data survives if the claim is deleted" is one command: kubectl patch pv <name> -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'.
The claim, the Pod, and how binding works
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-claim
namespace: st-pv
spec:
accessModes:
- ReadWriteOnce
volumeMode: Filesystem
storageClassName: st-manual
resources:
requests:
storage: 1Gi
selector:
matchLabels:
tier: data
---
apiVersion: v1
kind: Pod
metadata:
name: data-pod
namespace: st-pv
spec:
containers:
- name: web
image: nginx:1.27
volumeMounts:
- name: data
mountPath: /usr/share/nginx/html
volumes:
- name: data
persistentVolumeClaim:
claimName: data-claim- Capacity: the PV's
capacity.storagemust be at least the claim'srequests.storage. The controller picks the smallest PV that fits, so a 1Gi claim may end up bound to a 5Gi volume and will report 5Gi. - Access modes: every mode the claim lists must be on the PV.
- Class:
storageClassNamemust be the same string on both. A claim that says""binds only to PVs with no class at all. A claim that leaves the field out gets the cluster's default class filled in at admission (and, since v1.28, later if a default appears afterwards), which on this lab meansstandardand a dynamically provisioned volume, not your hand-made PV. - volumeMode:
FilesystemandBlocknever match each other. - Selector: if the claim has one, the PV's labels must satisfy it; a claim with a selector is never dynamically provisioned.
- State: the PV must be
Available. A PV whosespec.claimRefnames a specific claim is reserved for it; a claim can reserve a PV the same way withspec.volumeName.
Binding is one-to-one and sticky: once bound, the pair stays bound until the claim is deleted, whatever else appears. A claim that cannot be satisfied stays Pending and kubectl describe pvc tells you why in its events; the fix is almost always a PV with the right class, enough capacity and the requested modes. One more rule that bites during cleanup: a claim that a Pod still uses cannot be deleted. The kubernetes.io/pvc-protection finalizer keeps it in Terminating until the last Pod is gone, so delete Pods before claims.
PV phases and reusing a Released volume
kubectl get pv st-keep-pv # STATUS Released, CLAIM still shows the old claim
kubectl get pv st-keep-pv -o jsonpath='{.spec.claimRef}'
# Drop the reference to the deleted claim. The data on the volume is untouched.
kubectl patch pv st-keep-pv -p '{"spec":{"claimRef":null}}'
# Interactive alternative: kubectl edit pv st-keep-pv and delete the whole claimRef: block.
kubectl get pv st-keep-pv # Available
# A Pending claim that matches is picked up on the controller's next pass (within ~15s).
kubectl -n st-pv wait --for=jsonpath='{.status.phase}'=Bound pvc/keep-claim-v2 --timeout=90sIn the docs · the tab you get on exam day
- Persistent Volumes ↗search: “persistent volumes”
- Volumes: hostPath and local ↗search: “hostpath volume”
- Configure a Pod to Use a Volume for Storage ↗search: “configure volume storage”
context · kubectl config use-context kind-cka
A hostPath PV, a claim and a Pod that mounts it
st-data-pv with 1Gi of capacity, access mode ReadWriteOnce, reclaim policy Retain and storage class name st-manual, backed by the host directory /opt/st-data (create it if it does not exist). In namespace st-pv create a PersistentVolumeClaim named data-claim that requests 1Gi from that volume, and a Pod named data-pod using image nginx:1.27 that mounts the claim at /usr/share/nginx/html. The claim must be Bound to st-data-pv and the Pod must be running.- 01
Set up the scenario
curl -fsSL https://codewithmuh.com/labs/cka/t/st-pv-1/setup | bash - 02
Solve it in your terminal
Work the way the exam makes you: kubectl, the docs, nothing else. Put any answer files under
/tmp/cka/. - 03
Check your work
curl -fsSL https://codewithmuh.com/labs/cka/t/st-pv-1/verify | bashCounts toward your readiness score. - 04
Clean up
curl -fsSL https://codewithmuh.com/labs/cka/t/st-pv-1/cleanup | bash
context · kubectl config use-context kind-cka
Make a Pending claim bind
logs-claim in namespace st-pv has been Pending since it was created. Create a PersistentVolume named st-logs-pv that satisfies it, backed by the host directory /opt/st-logs-pv and with reclaim policy Retain. Do not modify or recreate the claim. When you are done the claim must be Bound to st-logs-pv.- 01
Set up the scenario
curl -fsSL https://codewithmuh.com/labs/cka/t/st-pv-2/setup | bash - 02
Solve it in your terminal
Work the way the exam makes you: kubectl, the docs, nothing else. Put any answer files under
/tmp/cka/. - 03
Check your work
curl -fsSL https://codewithmuh.com/labs/cka/t/st-pv-2/verify | bashCounts toward your readiness score. - 04
Clean up
curl -fsSL https://codewithmuh.com/labs/cka/t/st-pv-2/cleanup | bash
context · kubectl config use-context kind-cka
Reuse a Released Retain volume
st-keep-pv belonged to a claim that has since been deleted. It still holds data that must be kept, and it is now in phase Released. A new claim keep-claim-v2 in namespace st-pv is waiting for it. Make st-keep-pv bindable again so that keep-claim-v2 binds to it. Do not delete or recreate the PersistentVolume, and keep its reclaim policy.- 01
Set up the scenario
curl -fsSL https://codewithmuh.com/labs/cka/t/st-pv-3/setup | bash - 02
Solve it in your terminal
Work the way the exam makes you: kubectl, the docs, nothing else. Put any answer files under
/tmp/cka/. - 03
Check your work
curl -fsSL https://codewithmuh.com/labs/cka/t/st-pv-3/verify | bashCounts toward your readiness score. - 04
Clean up
curl -fsSL https://codewithmuh.com/labs/cka/t/st-pv-3/cleanup | bash
Finished reading?
0 of 3 tasks passed. Readiness counts tasks, so run them before you move on.