Storage (10%)
Notes on persistent storage primitives: volumes, persistent volumes/claims, storage classes, and dynamic provisioning.
Topics
- Understand storage classes, persistent volumes
- Understand volume mode, access modes and reclaim policies for volumes
- Understand persistent volume claims primitive
- Know how to configure applications with persistent storage
Volume basics
Container filesystems are ephemeral. A volume is a directory made available to containers in a Pod, with a lifetime tied either to the Pod or to external storage.
Common ephemeral/simple types:
emptyDir: created empty when the Pod is scheduled, deleted with the Pod. Survives container restarts.medium: Memoryuses tmpfs. Good for scratch space and sharing between containers.hostPath: mounts a node path. Ties the Pod to a node and is a security risk; fine for single-node labs and system agents.configMap,secret,projected,downwardAPI: inject configuration (see Workloads notes).persistentVolumeClaim: attaches durable storage via a PVC.
spec:
containers:
- name: app
image: nginx
volumeMounts:
- { name: cache, mountPath: /cache }
volumes:
- name: cache
emptyDir: { sizeLimit: 500Mi }
PersistentVolumes and PersistentVolumeClaims
- PersistentVolume (PV): a cluster-scoped piece of storage, provisioned by an admin or dynamically by a StorageClass.
- PersistentVolumeClaim (PVC): a namespaced request for storage (size, access mode, class). The control plane binds a PVC to a suitable PV one-to-one.
- Pods reference the PVC, never the PV.
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-data
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes: [ReadWriteOnce]
persistentVolumeReclaimPolicy: Retain
storageClassName: manual
hostPath:
path: /mnt/data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-claim
spec:
accessModes: [ReadWriteOnce]
storageClassName: manual
resources:
requests:
storage: 2Gi
---
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: nginx
volumeMounts:
- { name: data, mountPath: /usr/share/nginx/html }
volumes:
- name: data
persistentVolumeClaim:
claimName: data-claim
Binding rules: the PV must have capacity at least the requested size, include the requested access modes, match storageClassName exactly (an empty string "" means “no class”, which is different from omitted), and match volumeMode. A PVC can also select with selector labels or pin with volumeName. A PVC with no match stays Pending.
PV phases: Available, Bound, Released (claim deleted, not yet reclaimed), Failed.
Volume mode, access modes, reclaim policies
Volume mode
Filesystem(default): mounted as a directory; formatted automatically if empty.Block: raw block device exposed to the Pod viavolumeDeviceswithdevicePath, notvolumeMounts.
Access modes
| Mode | Short | Meaning |
|---|---|---|
| ReadWriteOnce | RWO | Read-write by a single node (many Pods on that node allowed) |
| ReadOnlyMany | ROX | Read-only by many nodes |
| ReadWriteMany | RWX | Read-write by many nodes (NFS, CephFS, Azure Files) |
| ReadWriteOncePod | RWOP | Read-write by a single Pod |
Access modes describe what the volume can support; they are not enforcement of permissions. Support depends on the backend (block disks are usually RWO only).
Reclaim policy (what happens to the PV when its PVC is deleted)
Retain: PV becomesReleasedand keeps its data. An admin must clean it and remove theclaimRefbefore it can be reused.Delete: the PV and the backing storage are deleted. Default for dynamically provisioned volumes.Recycle: deprecated (basicrm -rf).
kubectl patch pv pv-data -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
kubectl patch pv pv-data -p '{"spec":{"claimRef":null}}' # make a Released PV Available again
StorageClasses and dynamic provisioning
A StorageClass describes a “class” of storage and the provisioner that creates volumes on demand. A PVC naming a class triggers automatic PV creation, so admins do not pre-create PVs.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: ebs.csi.aws.com # or rancher.io/local-path, disk.csi.azure.com, etc.
parameters:
type: gp3
reclaimPolicy: Delete # Delete (default) or Retain
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
mountOptions: [debug]
Important fields:
volumeBindingMode: Immediate(default) provisions when the PVC is created.WaitForFirstConsumerdelays until a Pod using it is scheduled, so the volume lands in the right zone/node. Use it for topology-constrained storage and local volumes.allowVolumeExpansion: trueallows growing a PVC by editingspec.resources.requests.storage(growing only, never shrinking).- Only one class should be default. A PVC with no
storageClassNameuses the default class.storageClassName: ""explicitly disables dynamic provisioning. no-provisioner(for local volumes) means PVs are created manually.
Storage is provided by CSI (Container Storage Interface) drivers, which replaced the in-tree plugins.
Configuring applications with persistent storage
- Mount PVCs in Pod/Deployment templates. With a Deployment that has multiple replicas, all share the one PVC (needs RWX or all Pods on one node). For per-replica storage use a StatefulSet with
volumeClaimTemplates, which creates one PVC per Pod (data-db-0,data-db-1) that persists across rescheduling. subPathmounts a sub-directory of a volume (for example to avoidlost+foundclobbering a database directory).- Set
securityContext.fsGroupso mounted volumes are writable by the container’s group.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
spec:
serviceName: db
replicas: 2
selector:
matchLabels: { app: db }
template:
metadata:
labels: { app: db }
spec:
securityContext: { fsGroup: 999 }
containers:
- name: db
image: postgres:16
volumeMounts:
- { name: data, mountPath: /var/lib/postgresql/data }
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ReadWriteOnce]
storageClassName: fast
resources:
requests:
storage: 10Gi
Block volume example:
containers:
- name: app
image: busybox
volumeDevices:
- { name: raw, devicePath: /dev/xvda }
volumes:
- name: raw
persistentVolumeClaim: { claimName: block-claim }
Deleting: a PVC in use by a Pod is protected (kubernetes.io/pvc-protection finalizer) and stays Terminating until the Pod is gone. Same for PVs bound to claims.
Troubleshooting:
| Symptom | Likely cause |
|---|---|
PVC Pending | No matching PV (size, access mode, class, mode) or no provisioner; or WaitForFirstConsumer and no Pod yet |
Pod ContainerCreating, FailedMount or FailedAttachVolume | Volume attached to another node (RWO), backend/CSI problem, wrong node path |
Pod Pending with “volume node affinity conflict” | PV is tied to a zone or node the Pod cannot schedule to |
| Permission denied writing | Missing fsGroup or wrong ownership |
Cheat sheet
kubectl get pv,pvc -A
kubectl get sc
kubectl describe pvc data-claim # events show why it is Pending
kubectl describe pv pv-data
kubectl patch sc fast -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
kubectl patch pvc data-claim -p '{"spec":{"resources":{"requests":{"storage":"5Gi"}}}}' # expand
kubectl get pvc -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,VOLUME:.spec.volumeName,CLASS:.spec.storageClassName
kubectl exec app -- df -h /usr/share/nginx/html
kubectl explain pv.spec --recursive | less
Exam tips:
- PV/PVC have no imperative
kubectl create; copy a skeleton from the docs (“Configure a Pod to Use a PersistentVolume for Storage”). - Make
storageClassName, access mode, and capacity match exactly between PV and PVC. - Use
kubectl get pvcto confirmBoundbefore creating the Pod.