Kubernetes StorageClasses, PersistentVolumes, and PVCs for CKA
Introduction
Kubernetes StorageClasses, PersistentVolumes (PVs) and PersistentVolumeClaims (PVCs) connect application storage requests to real storage. They form the Storage domain (10% of the CKA), covering dynamic provisioning, volume types, access modes, reclaim policies and claim management. Original exercises below bind a static volume, provision a zonal disk and recover retained storage.
The official exam page currently lists Kubernetes v1.37. Confirm your sitting's version and the lab's storage driver capabilities. Examples use Linux and namespaces selected explicitly with your kubectl context; a Pod and its PVC must share a namespace. Fixed images are lab examples rather than production version recommendations.
After this lesson you will be able to choose a volume type for a requirement, create PersistentVolumes, PersistentVolumeClaims and StorageClasses, explain access modes and reclaim policies, enable dynamic provisioning, and diagnose a claim that will not bind.
Learning objectives
- Distinguish ephemeral volumes (
emptyDir,configMap,secret,projected) from persistent storage. - Create a PersistentVolume and a PersistentVolumeClaim and predict whether they bind.
- Explain
ReadWriteOnce,ReadOnlyMany,ReadWriteManyandReadWriteOncePod. - Explain the reclaim policies
Retain,Deleteand the legacyRecycle. - Create a StorageClass, set a default class, and explain
volumeBindingMode: WaitForFirstConsumer. - Expand a claim and take a snapshot conceptually, and troubleshoot a Pending PVC.
Volumes in Pods
A container's writable layer is ephemeral and a replacement container starts from its image; do not rely on that layer to retain application data. A volume is declared in spec.volumes of the Pod and mounted into containers through volumeMounts. Common types:
| Type | Lifetime | Typical use |
|---|---|---|
emptyDir |
Pod lifetime (survives container restarts, lost when the Pod is deleted) | Scratch space, cache, sharing files between containers of a Pod |
hostPath |
Node filesystem path | Node agents, lab setups; avoid for applications (ties the Pod to a node and is a security risk) |
configMap, secret |
Mirrors the object | Configuration and credentials as files |
projected |
Combines several sources in one directory | Service account tokens, configuration bundles |
persistentVolumeClaim |
Independent of the Pod | Databases and any data that must survive |
nfs, CSI-backed volumes |
External | Shared or cloud-backed storage |
A small example with a memory-backed emptyDir shared by two containers:
apiVersion: v1
kind: Pod
metadata:
name: shared
spec:
containers:
- name: writer
image: busybox:1.36
command: ["sh", "-c", "while true; do date >> /data/log; sleep 5; done"]
volumeMounts:
- {name: scratch, mountPath: /data}
- name: reader
image: busybox:1.36
command: ["sh", "-c", "tail -F /data/log"]
volumeMounts:
- {name: scratch, mountPath: /data, readOnly: true}
volumes:
- name: scratch
emptyDir:
medium: Memory
sizeLimit: 64MiMemory-backed emptyDir uses tmpfs: bytes written count against the writing container's memory budget. Its sizeLimit caps the volume but does not reserve RAM or replace appropriate container memory requests/limits.
Entries for in-tree cloud volume plugins (such as
awsElasticBlockStore) have been removed or migrated to CSI drivers. In current versions you consume cloud disks through a CSI driver and a StorageClass, rather than relying on removed in-tree sources. CSI also has inline ephemeral-volume support for drivers that implement it; that is distinct from persistent PVC-backed disks.
PersistentVolumes and claims
Kubernetes separates two roles:
- A PersistentVolume (PV) is a cluster-wide resource that represents a piece of storage. It is created by an administrator or dynamically by a provisioner.
- A PersistentVolumeClaim (PVC) is a namespaced request for storage by a user: a size, an access mode, optionally a class. For this PV/PVC workflow, Pods name a PVC in their own namespace rather than a PV name. Direct inline volume sources such as nfs are a separate mechanism.
The control plane binds a claim to a PV that satisfies it. Binding is one-to-one: a PV bound to a claim cannot serve another, even if capacity remains.
Matching rules
A PVC binds to a PV when all of these hold:
- The PV capacity is greater than or equal to the request.
- The PV offers every requested access mode.
- The storageClassName is identical. An empty string
""means "no class" (static binding only). An omitted field means "use the default class", if one exists. - The PV satisfies
volumeMode(FilesystemorBlock), and any label selector orvolumeNamein the claim.
A PV that is Available has not been claimed; Bound has; Released was bound to a claim that was deleted but has not been reclaimed; Failed means automatic reclamation failed.
Worked example: static provisioning
Scenario. The administrator prepared the directory /mnt/data on node worker-1. Create a 2Gi PV, a claim for 1Gi, and a Pod that mounts it at /usr/share/nginx/html.
Steps.
- Use hostPath only for this controlled lab. In a multi-node lab, constrain this PV to worker-1 and verify that node's
kubernetes.io/hostnamelabel equals worker-1. For production node-local storage, consider a local PV with node affinity or an appropriate CSI solution. Assume no other claim competes for this PV and/mnt/datacontains a readable index.html. The declared capacity is used for binding; a hostPath directory does not itself enforce a 2Gi filesystem quota.
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-data
spec:
capacity:
storage: 2Gi
accessModes: ["ReadWriteOnce"]
persistentVolumeReclaimPolicy: Retain
storageClassName: manual
hostPath:
path: /mnt/data
type: Directory
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values: [worker-1]- Create the claim. The class must match, and the request may be smaller than the PV:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: manual
resources:
requests:
storage: 1Gi- Check:
kubectl get pv,pvcshows bothBound, and the claim reports a capacity of2Gi(the whole PV is reserved). - Mount it in the Pod:
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
nodeSelector:
kubernetes.io/hostname: worker-1
containers:
- name: nginx
image: nginx:1.27
volumeMounts:
- {name: html, mountPath: /usr/share/nginx/html}
volumes:
- name: html
persistentVolumeClaim:
claimName: pvc-dataWhy not the alternatives? The Pod's persistentVolumeClaim source requires claimName, not a PV name. If the default class differs from manual, omitting storageClassName will not target pv-data; another suitable PV or dynamic provisioning may satisfy the claim. A request larger than 2Gi would never bind.
Access modes
Access modes advertise supported mounting capabilities and participate in claim matching. They are not filesystem permissions or a general read-only security control; enforce read-only access with mount and backend permissions where needed. ReadWriteOncePod additionally constrains use to a single Pod and requires a compatible CSI driver/sidecars.
| Mode | Short | Meaning |
|---|---|---|
ReadWriteOnce |
RWO | Read-write by a single node (several Pods on that node can share it) |
ReadOnlyMany |
ROX | Read-only by many nodes |
ReadWriteMany |
RWX | Read-write by many nodes |
ReadWriteOncePod |
RWOP | Read-write by a single Pod in the whole cluster |
Block-based cloud disks are typically RWO. For shared filesystem workloads, RWX commonly uses NFS, a managed file service or another multi-node-capable CSI backend. Asking for RWX from a driver that cannot provide it leaves the claim Pending.
Reclaim policies
When a bound claim is deleted, the PV's persistentVolumeReclaimPolicy decides what happens:
| Policy | Result | Typical use |
|---|---|---|
Retain |
The PV becomes Released and data stays; an administrator controls sanitisation or intentional data-preserving rebinding | Valuable data |
Delete |
The PV and the backing storage asset are removed | Dynamic provisioning default for most classes |
Recycle |
Deprecated; basic scrub (rm -rf) |
Do not use |
Change a policy with kubectl patch pv pv-data -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'. Dynamically provisioned volumes inherit the reclaimPolicy of their StorageClass (default Delete).
To reuse a Released PV, first identify its former owner, preserve any required data and ensure old workloads cannot still write to it. Prevent competing claims, then remove or replace the stale claimRef as part of an administrator-controlled rebinding procedure. A reservation naming the replacement claim can avoid exposing the PV to unrelated claimants. Removing claimRef alone can expose retained data to another matching claim. Retain is a recovery aid, not a backup.
StorageClasses and dynamic provisioning
Creating PVs by hand does not scale. A StorageClass describes a "kind" of storage and the provisioner (often a CSI driver or an external provisioning controller) that creates volumes on demand. When provisioning is needed, a running compatible provisioner and backend create the PV according to the class and binding mode; a StorageClass object alone does not install a driver. A suitable pre-existing PV can also satisfy a claim.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: ebs.csi.aws.com
parameters:
type: gp3
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
mountOptions:
- discardKey fields:
provisioner: the CSI driver name (here the AWS EBS driver is only an example; each driver documents its ownparameters).reclaimPolicy:Delete(default) orRetainfor volumes created by this class.allowVolumeExpansion: lets you increase a claim later by editingspec.resources.requests.storage. Shrinking is never supported.volumeBindingMode: Immediate starts binding/provisioning without the consumer's scheduling requirements; WaitForFirstConsumer coordinates binding/provisioning with the scheduler after a consuming Pod exists, so eligible topology and placement constraints are considered. Do not set nodeName to bypass the scheduler in this workflow; use nodeSelector or affinity.- The annotation
storageclass.kubernetes.io/is-default-class: "true"marks the default class, used by claims that omitstorageClassName. Prefer one default; with several, an omitted class uses the most recently created default StorageClass. Claims with storageClassName explicitly set to an empty string opt out. Unset class fields can be assigned retroactively when a default becomes available.
kubectl get storageclass shows (default) next to the default one. To change defaults: kubectl patch storageclass old -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}', then set "true" on the new one.
Worked example: a database with dynamic storage
Scenario. A PostgreSQL Pod needs a 5Gi volume that survives Pod deletion and lives in the same zone as the Pod. The cluster has a class fast with WaitForFirstConsumer.
Reasoning.
- Create only the claim, naming the class: no PV needed.
- Before a consuming Pod exists, the claim normally stays
Pending. This is normal withWaitForFirstConsumerand is not an error.kubectl describe pvcsayswaiting for first consumer to be created before binding. - Create the Pod. The scheduler and provisioner coordinate eligible placement, the driver creates a suitable disk and the claim becomes Bound. Check Pod events, node affinity and available backend capacity if it remains Pending.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pgdata
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast
resources:
requests:
storage: 5GiWhy not Immediate? With zonal disks the volume could be created in a zone where the Pod cannot be scheduled (for example because of node affinity or effective resource requests), leaving the Pod Pending until the topology or workload constraints are corrected. Review Pod scheduling, affinity, and resource requests when diagnosing this interaction.
Expanding a claim
If the class has allowVolumeExpansion: true and the driver/backend support expansion, request growth through the PVC rather than editing PV capacity:
kubectl patch pvc pgdata -p '{"spec":{"resources":{"requests":{"storage":"10Gi"}}}}'
kubectl get pvc pgdata -wExpansion support, filesystem type and online versus remount requirements are driver-dependent. FileSystemResizePending means filesystem work remains; inspect conditions and driver guidance, and mount/restart only when required. Do not delete the claim to force normal expansion. A failed oversized expansion request can be retried with a lower request still above the current status.capacity; this does not shrink the actual volume.
Comparison: static versus dynamic provisioning
| Aspect | Static | Dynamic |
|---|---|---|
| Who creates the PV | Administrator | Provisioner, on demand |
Claim storageClassName |
Must match the PV (often "" or a label class) |
Names a StorageClass |
| Default reclaim policy | Whatever the PV says | Class reclaimPolicy (Delete by default) |
| Best for | Pre-existing disks, NFS shares, labs | Everyday workloads |
Using volumes with StatefulSets
A StatefulSet can declare volumeClaimTemplates; each replica gets its own claim named <template>-<statefulset>-<ordinal>. With the default Retain retention settings, scaling down does not delete claims. Explicit whenScaled: Delete or whenDeleted: Delete policies change the corresponding behaviour. PVC deletion is distinct from backing-volume deletion: the PV reclaim policy controls the latter. See Deployments, StatefulSets, and application configuration when choosing a controller and storage identity.
CSI drivers, snapshots, and raw block volumes
Storage drivers plug into Kubernetes through the Container Storage Interface (CSI). A driver usually runs as a controller Deployment (creating, attaching and snapshotting volumes) plus a node DaemonSet (mounting them). When provisioning fails, these Pods in kube-system or the driver namespace are the first place to look, and kubectl get csidrivers and kubectl get csinodes show what is registered.
Two optional features appear in practice:
- Volume snapshots. With snapshot CRDs, snapshot controller, compatible CSI sidecars and driver snapshot support installed, a
VolumeSnapshotClassandVolumeSnapshotcapture a claim at a point in time, and a new PVC with adataSourceof kindVolumeSnapshotrestores it. Example: before a risky schema migration, Contoso snapshotspgdata, and if the migration fails, createspgdata-restorefrom that snapshot and points the Deployment at it. The API is provided by CRDs;kubectl api-resources | grep snapshotchecks API availability, not driver functionality. Verify readyToUse and restoration, and coordinate database quiescing or native backups for application consistency. An etcd snapshot preserves Kubernetes objects, not PV payloads; etcd recovery and cluster backups covers that separate layer. - Raw block volumes. Setting
volumeMode: Blockon both PV and claim exposes the device without a file system, referenced in the container byvolumeDeviceswith adevicePathinstead ofvolumeMounts. Some databases use this for performance. AFilesystemclaim never binds to aBlockPV.
A method for storage problems
kubectl get pvc: is itBoundorPending?kubectl describe pvc <name>: the events explain the reason (no matching PV, no default class, provisioner failure, waiting for consumer).- Compare the claim with
kubectl get pv: size, access mode, class,volumeMode. - For dynamic claims, check the provisioner Pods:
kubectl get pods -n kube-system | grep -i csi. - If the Pod itself is
ContainerCreating,kubectl describe podshows mount errors (FailedAttachVolume,FailedMount): a RWO volume still attached to another node, a wrongclaimName, or a missing Secret/ConfigMap volume.
Common pitfalls and exam tips
- Storage class mismatch. A claim with
storageClassName: standardnever binds to a PV of classmanual.""is not the same as omitted. - Capacity. A PV smaller than the request is ignored; a larger one is accepted and fully reserved.
- Pending is not always a bug. With
WaitForFirstConsumerthe claim waits for a Pod. - RWO is per node, not per Pod. Pods on the same node may share it; single-attach block drivers can reject a second-node attachment. Use RWOP with a compatible CSI driver when one-Pod access is required; access modes do not replace application locking.
hostPathdata is per node. If the Pod moves to another node, it sees different (or no) data.ReleasedPVs do not rebind automatically; clearclaimRefafter dealing with the data.- Deleting a PVC that a Pod uses leaves the claim in
Terminatinguntil the Pod is gone (storage object in use protection). - No shrinking. Expansion only grows.
- Exam tip: copy the PV and PVC YAML from the docs, change the values, and check
kubectl get pv,pvcimmediately. Usekubectl explain pv.specfor fields. If asked to set a default class, change annotations withkubectl patchand verify withkubectl get sc.
Mini case study: Contoso's reporting database
Contoso needs the following in namespace reports:
- A StorageClass
retainedusing the cluster's CSI provisioner, reclaim policyRetain, expansion allowed, bindingWaitForFirstConsumer. - A 20Gi RWO claim
reportsdbusing it, mounted at/var/lib/postgresql/databy a Deployment with one replica. - A week later the claim is accidentally marked for deletion. The team needs to preserve its data, restore use of the claim, then grow it to 40Gi.
Solution.
- Create the StorageClass with the fields above (copy a class from the docs, then change
reclaimPolicy: Retain). Verify withkubectl get sc retained -o yaml. - Create the PVC naming
retained; it stays Pending until the Deployment is created, then Bound. Because Postgres expects an empty directory, mount at/var/lib/postgresql/dataand, if the volume has alost+founddirectory, set thePGDATAenvironment variable to a subdirectory. - If Pods still reference the claim, PVC protection can keep it Terminating; it has not yet released the PV. Pause the workload controller and stop consumers through the approved procedure. After deletion completes, inspect Released state, the actual disk and backups. With Retain, data should remain unless an external process deleted it. Rebind under administrator control: prevent competing claims, replace the stale claimRef with a reservation naming the intended namespace/claim (without the old UID), and create a compatible PVC with volumeName naming that PV. Preserve class, access/volume modes and requested capacity. Verify data before resuming the workload. With Delete, provisioner cleanup can remove the backing disk, so recovery would depend on an independent backup or snapshot.
- After recovery, confirm driver/backend expansion support, patch the PVC request to 40Gi and watch events and conditions. Confirm the size inside the application filesystem as well as PVC status.
Commentary. The decisive choice was the reclaim policy; it turned a destructive mistake into a recoverable one. The same task in an exam checks that you know Released is not Available and that claimRef must be cleared.
Key terms
- Volume: directory accessible to containers in a Pod.
- PersistentVolume (PV): cluster-level storage resource.
- PersistentVolumeClaim (PVC): namespaced request that binds to a PV.
- StorageClass: template and provisioner for dynamic volumes.
- Access mode: RWO, ROX, RWX or RWOP.
- Reclaim policy:
Retain,Delete(and deprecatedRecycle). WaitForFirstConsumer: coordinate volume binding/provisioning with a consuming Pod's scheduler constraints.- CSI: Container Storage Interface, the plugin API for storage drivers.
Summary checklist
-
emptyDirlives with the Pod; PVCs outlive it. - A claim binds to a PV with at least the requested size, matching modes and the same class.
-
storageClassName: ""means no class; omitted means the default class. - RWO is per node; RWOP needs compatible CSI support; RWX requires a backend supporting multi-node writes.
-
Retainkeeps data and leaves the PVReleased;Deleteremoves it. -
WaitForFirstConsumerleaves claims Pending until a Pod uses them. - Expansion needs class permission plus driver/backend support; reducing a failed request above current capacity is not volume shrink.
-
kubectl describe pvcexplains why a claim is Pending.
Official sources
- Volumes: https://kubernetes.io/docs/concepts/storage/volumes/ (opens in a new tab)
- Persistent Volumes: https://kubernetes.io/docs/concepts/storage/persistent-volumes/ (opens in a new tab)
- Storage Classes: https://kubernetes.io/docs/concepts/storage/storage-classes/ (opens in a new tab)
- Dynamic volume provisioning: https://kubernetes.io/docs/concepts/storage/dynamic-provisioning/ (opens in a new tab)
- Configure a Pod to use a PersistentVolume: https://kubernetes.io/docs/tasks/configure-pod-container/configure-persistent-volume-storage/ (opens in a new tab)
- Volume snapshots: https://kubernetes.io/docs/concepts/storage/volume-snapshots/ (opens in a new tab)
- StatefulSet claim retention: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/ (opens in a new tab)
- CKA domains and current version: https://training.linuxfoundation.org/certification/certified-kubernetes-administrator-cka/ (opens in a new tab)
- CKA curriculum: https://github.com/cncf/curriculum (opens in a new tab)
