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

graph LR PV[PersistentVolume<br/>the actual storage<br/>cluster-scoped] --- B((bind)) PVC[PersistentVolumeClaim<br/>a request for storage<br/>namespaced] --- B POD[Pod<br/>mounts the claim] --> PVC SC[StorageClass<br/>creates PVs on demand] -.optional.-> PV
  • 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.

graph LR PVC[PVC: 300Mi, class nfs-csi] --> SC[StorageClass nfs-csi<br/>provisioner nfs.csi.k8s.io] SC --> D[CSI driver pods] D -->|creates subdir on the NFS server| PV[new PV, auto-named] PV --- PVC
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

ModeMeaning
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

SymptomCause
PVC stuck Pendingno PV matches size / access mode / class - kubectl describe pvc says which
Pod stuck ContainerCreatingmount failing: nfs-common missing on the node, wrong export path, firewall
PV stays Releasedthe PVC was deleted; with Retain it will not rebind until you clear claimRef

References