01 · Volumes & Persistent Storage¶
Not run against a live cluster
Manifests and output below follow documented Volume/PV/PVC behavior; not executed against a live cluster in this session.
The problem: containers are stateless by default¶
A container's writable filesystem lives inside its container runtime layer. When the container is restarted (crash, image update, node reschedule), that layer is discarded and recreated from the image — anything written to it is gone. Kubernetes Volumes attach external storage to a Pod so that data can survive a container restart, and PersistentVolumes let that data survive even when the Pod itself is deleted and rescheduled elsewhere.
emptyDir: scratch space tied to the Pod's lifetime¶
apiVersion: v1
kind: Pod
metadata:
name: cache-pod
spec:
containers:
- name: app
image: nginx:1.27
volumeMounts:
- name: scratch
mountPath: /cache
volumes:
- name: scratch
emptyDir: {}
emptyDir is created empty when the Pod is scheduled to a node and deleted
permanently when that Pod is removed from the node — a crash and restart of
the container keeps the data (kubelet doesn't recreate the volume, only the
container), but deleting the Pod wipes it. It's ideal for sharing files
between containers in the same Pod (a sidecar writing logs another container
tails) or as disposable scratch space.
hostPath: binding to the node's filesystem¶
hostPath mounts a path from the node's own filesystem into the Pod.
It's tied to that specific node — if the Pod reschedules elsewhere, the data
doesn't follow it, and any file dropped there is directly visible to
anything else running on that node (a real security concern; Level 3/4
covers restricting it via Pod Security Standards). Legitimate uses are
narrow: node-level agents (log collectors, CNI/CSI plugins) that are
supposed to see the host, not general application storage.
PersistentVolumes and PersistentVolumeClaims: storage that outlives the Pod¶
Real application data needs storage independent of any one Pod or node. Two objects split this concern:
- A PersistentVolume (PV) represents an actual piece of storage (a cloud disk, an NFS export, a local disk) — created by an admin or dynamically provisioned.
- A PersistentVolumeClaim (PVC) is a request for storage made by a workload ("I need 5Gi, ReadWriteOnce") — Kubernetes binds it to a matching PV.
# pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: standard
# deployment.yaml (excerpt)
spec:
template:
spec:
containers:
- name: app
image: postgres:16
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
persistentVolumeClaim:
claimName: data-pvc
kubectl apply -f pvc.yaml
kubectl get pvc data-pvc
# NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
# data-pvc Bound pvc-3f2c1a9e-... 5Gi RWO standard 4s
StorageClasses: dynamic provisioning¶
Rather than an admin pre-creating PVs by hand, a StorageClass tells Kubernetes how to create one on demand when a PVC references it:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard
provisioner: kubernetes.io/aws-ebs # or pd.csi.storage.gke.io, disk.csi.azure.com, etc.
parameters:
type: gp3
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
volumeBindingMode: WaitForFirstConsumer delays provisioning until a Pod
using the PVC is actually scheduled, so the volume gets created in the same
zone as the node the Pod lands on (a Pod can't mount a disk provisioned in a
different availability zone).
Access modes¶
| Mode | Meaning |
|---|---|
ReadWriteOnce (RWO) |
Mounted read-write by a single node at a time |
ReadOnlyMany (ROX) |
Mounted read-only by many nodes |
ReadWriteMany (RWX) |
Mounted read-write by many nodes (needs NFS/EFS-class storage, not typical block disks) |
ReadWriteOncePod (RWOP) |
Mounted read-write by a single Pod (stricter than RWO's per-node guarantee) |
Most cloud block storage (EBS, PD, Azure Disk) only supports RWO — this is why a single PVC generally can't be shared read-write across replicas of a Deployment; StatefulSets (Level 3) instead give each replica its own PVC.
Worked example: data survives Pod deletion¶
kubectl apply -f pvc.yaml
kubectl apply -f deployment.yaml
kubectl exec deploy/app -- sh -c 'echo hello > /var/lib/postgresql/data/marker'
kubectl delete pod -l app=app # Deployment recreates the Pod
kubectl exec deploy/app -- cat /var/lib/postgresql/data/marker
# hello -- the new Pod mounted the SAME PVC/PV, data survived
How It Actually Works¶
- The PV/PVC binding is a two-phase controller dance, not a direct
mount. The
PersistentVolumeController(in kube-controller-manager) watches PVCs; when a new one appears unbound, it searches existing PVs for one whose capacity, access modes, and StorageClass satisfy the claim (static provisioning) or, ifstorageClassNamereferences a StorageClass with a provisioner, it does nothing itself and instead the external CSI provisioner sidecar (external-provisioner, watching PVCs via the API server) creates a brand-new PV and its backing cloud volume, then the controller binds the two objects by writing each one's UID into the other's spec. - Actually attaching and mounting is a kubelet + CSI driver job, done
per-node, at Pod start. Once a Pod referencing the PVC is scheduled,
kubelet calls the CSI driver's
NodeStageVolume/NodePublishVolumegRPC methods (over a Unix socket) — this is what actually calls the cloud provider's API to attach the block device to the node's instance, formats it if unformatted, mounts it to a staging directory, then bind-mounts it into the container's mount namespace at the path Docker/containerd sets up when creating the container. WaitForFirstConsumerexists because provisioning and scheduling would otherwise race. Immediate binding mode creates the PV as soon as the PVC appears, picking a zone with no knowledge of where the Pod will land;WaitForFirstConsumerdefers PV creation until the scheduler has already chosen a node, and passes that node's topology labels to the provisioner so the volume is created in a zone the node can actually attach to.reclaimPolicyruns on PVC deletion, not Pod deletion.Deletecauses the same controller to call the CSI driver'sDeleteVolumeto destroy the backing cloud disk once the PVC is removed;Retainleaves the PV (and underlying disk) around in aReleasedstate requiring manual cleanup — deliberately chosen for data an admin doesn't want a developer able to destroy withkubectl delete pvc.
Exercise¶
Create the standard StorageClass, the data-pvc PVC, and a single-replica
Deployment mounting it at /data. Write a file into /data, delete the
Pod, and confirm the ReplicaSet's replacement Pod can still read the file
back — because it mounts the same PVC/PV, not a fresh emptyDir. Then
compare by repeating the same steps with an emptyDir volume instead, and
confirm the file is gone after the Pod is deleted.