Persistent storage
Persistent storage
A container’s writable layer dies with the container, and emptyDir dies with
the Pod. Anything that must outlive a reschedule has to come from outside the
cluster.
Three objects
- PersistentVolume (PV) - a piece of storage that exists. Cluster-scoped.
- PersistentVolumeClaim (PVC) - “I need 1 GiB, ReadWriteOnce”. Namespaced. The Pod references the claim, never the volume.
- StorageClass - a factory that creates PVs automatically (dynamic provisioning).
This cluster ships with no StorageClass and no CSI driver, so at first
nothing is created automatically: you write the PV yourself, pointing at one of
the NFS exports on support01 (/storage1, /storage2, /storage3). That is the
on-premise reality a cloud cluster hides from you - and the second half of Lab 18
fixes it by installing a driver.
Static NFS PV
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-storage1
spec:
capacity:
storage: 1Gi
accessModes: ["ReadWriteMany"]
persistentVolumeReclaimPolicy: Retain
storageClassName: "" # explicitly no class
nfs:
server: support01
path: /storage1
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data
spec:
accessModes: ["ReadWriteMany"]
resources:
requests:
storage: 1Gi
storageClassName: "" # match PVs with no class
The binding is done by the control plane: it looks for a PV that is at least as large, has compatible access modes and the same class. Binding is exclusive - one PVC owns one PV, even if the PVC asked for less.
Dynamic provisioning: CSI + StorageClass
Writing a PV per volume does not scale. A CSI driver teaches the cluster how to talk to a storage system, and a StorageClass is a named recipe using it. A PVC that asks for a class gets a PV created on the spot.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-csi
provisioner: nfs.csi.k8s.io
parameters:
server: support01
share: /storage3
reclaimPolicy: Retain # inherited by every PV this class creates
volumeBindingMode: Immediate # or WaitForFirstConsumer
The PVC then names the class and nothing else:
spec:
accessModes: ["ReadWriteMany"]
resources: { requests: { storage: 300Mi } }
storageClassName: nfs-csi
A driver is just workload: a DaemonSet doing the mounting on every node, plus a
controller Deployment creating and deleting volumes. Nodes still need
nfs-common - CSI drives the kernel mount, it does not replace it.
In-tree vs out-of-tree. The nfs: field in the static PV above is the
in-tree plugin, compiled into Kubernetes. In-tree storage plugins are frozen
and being removed; all current drivers live out of tree and speak CSI. Prefer
the CSI driver for anything new.
A class marked storageclass.kubernetes.io/is-default-class: "true" is used by
any PVC that does not name one. This cluster has no default, which is why an
unclassed PVC stays Pending forever instead of quietly getting a volume.
Access modes
| Mode | Meaning |
|---|---|
ReadWriteOnce (RWO) | mounted read-write by one node |
ReadOnlyMany (ROX) | read-only by many nodes |
ReadWriteMany (RWX) | read-write by many nodes - NFS can do this, block storage cannot |
Reclaim policy
Retain- the PV keeps the data after the PVC is deleted, and must be cleaned up by hand. Right for anything you care about.Delete- the volume goes away with the claim, data included. The common default for dynamic provisioning, and the reason people lose databases.
Set on the PV when you write it by hand, on the StorageClass when the driver
writes it. A Retain PV whose claim is gone goes to Released and stays there:
to hand it to a new PVC you clear claimRef and the new claim names it with
volumeName. That manual step is the feature - a human confirms the old data may
be reused.
What usually breaks
| Symptom | Cause |
|---|---|
PVC stuck Pending | no PV matches size / access mode / class - kubectl describe pvc says which |
Pod stuck ContainerCreating | mount failing: nfs-common missing on the node, wrong export path, firewall |
PV stays Released | the PVC was deleted; with Retain it will not rebind until you clear claimRef |