Chapter 3

Persistent volumes

PVC, PV, StorageClass, dynamic and static provisioning, access modes, snapshots, and ephemeral claims.

2.5 hoursKubernetes Foundations

Shopbot can lose chat transcripts every time its Pod restarts if data sits only in the container filesystem or an emptyDir. Durable storage needs a life cycle that outlives any one Pod. Kubernetes separates what the app asks for from what the platform attaches.

Goals

  • Request storage with a PersistentVolumeClaim (PVC).
  • Understand how StorageClasses and CSI drivers create PersistentVolumes (PVs).
  • Choose access modes and reclaim behaviour that match shopbot’s workload.
  • Resize, snapshot, restore, and use claim templates for ephemeral volumes.

The three objects

Application manifests name a PersistentVolumeClaim. The claim asks for a size, access mode, and StorageClass. The cluster binds that claim to a PersistentVolume, which points at real storage. A StorageClass tells the CSI provisioner which technology and parameters to use. Developers rarely create PV objects by hand on modern clusters.

Interactive — who owns which pieceClick a box

PVC

A PersistentVolumeClaim states size, access mode, and StorageClass. Ownership of the volume lives with the claim, not with one Pod.

kind: PersistentVolumeClaim
metadata:
  name: shopbot-history
spec:
  accessModes: ["ReadWriteOnce"]
  resources:
    requests:
      storage: 20Gi
  storageClassName: standard-ssd

Dynamic and static paths

Dynamic provisioning creates a PV when you create a PVC. The administrator installs CSI drivers and StorageClasses once; users only ship claims. Static provisioning is the admin publishing PVs up front—common for node-local disks or a fixed NFS export. One cluster can use both.

Interactive — dynamic vs staticClick a path

Default on most managed clusters. Create shopbot-history PVC; the StorageClass’s CSI driver allocates a disk and a PV. Delete the claim and the reclaim policy (often Delete) decides whether that disk disappears.

Claim storage for shopbot

A minimal claim for transcript history. Omit storageClassName only when the cluster defines a default class; otherwise set it explicitly.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: shopbot-history
  namespace: retail-chat
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
  storageClassName: standard-ssd

Apply it, then list claims with kubectl get pvc -n retail-chat. Status may stay Pending until a Pod consumes the claim if the StorageClass uses WaitForFirstConsumer. Immediate binding provisions as soon as the PVC exists.

Mount the claim in the Pod

apiVersion: v1
kind: Pod
metadata:
  name: shopbot-web
  namespace: retail-chat
spec:
  volumes:
    - name: chat-history
      persistentVolumeClaim:
        claimName: shopbot-history
  containers:
    - name: shopbot
      image: acme/shopbot:2.1
      volumeMounts:
        - name: chat-history
          mountPath: /var/lib/shopbot/history

Delete the Pod and recreate it against the same claim: the files remain. Delete the claim only when you intend to release the volume. Multiple Pods may reference one claim if the access mode and backend allow it.

After bind
  Pod shopbot-web
       │ claimName: shopbot-history
       ▼
  PVC shopbot-history  ──bound──►  PV pvc-…
                                       │
                                       ▼
                               CSI disk / file share

Access modes

Access modes describe concurrency, not filesystem permissions inside the container. Pick the mode your StorageClass actually supports—block disks are usually RWO; shared file services unlock RWX.

Interactive — access modesClick a mode

ReadWriteOnce

Common for block disks. Two shopbot replicas on different nodes will not both get the volume.

StorageClass and CSI

Inspect classes with kubectl get sc. Important fields include the provisioner name, reclaimPolicy (Delete vs Retain), volumeBindingMode, and allowVolumeExpansion. Several classes may share one CSI driver with different parameters (balanced SSD vs archival).

CSI drivers register as CSIDriver objects. Each driver typically runs a controller that provisions volumes and a DaemonSet-style node agent that attaches and mounts. You install drivers; you do not invent their low-level spec by hand.

kubectl get sc
kubectl get csidrivers
kubectl explain storageclass

When the claim is deleted

Releasing a PVC does not always erase data the same way. The bound PV’s reclaim policy decides:

  • Delete — common for dynamic volumes; PV and backend often go together.
  • Retain — PV becomes Released; an admin must scrub or re-publish before another claim can use it.

Change reclaim policy on a PV before deleting a precious claim if the class default is Delete and you need a manual hold.

Static local volumes

Ultra-fast shopbot embedding caches sometimes need NVMe on a specific node. An admin creates a StorageClass with kubernetes.io/no-provisioner, then a PV that names a path and nodeAffinity so the scheduler places consumers on that node.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: shop-local-nvme
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: shop-node-a-nvme
spec:
  capacity:
    storage: 400Gi
  accessModes: ["ReadWriteOnce"]
  persistentVolumeReclaimPolicy: Retain
  storageClassName: shop-local-nvme
  local:
    path: /mnt/fast-shopbot
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values: ["shop-node-a"]

A PVC that asks for shop-local-nvme binds when the first Pod appears. After release under Retain, recreate or clear claimRef before reuse; deleting the PV object does not delete the files on disk—operators must clean the path themselves.

Grow, snapshot, restore

If the StorageClass allows expansion, raise spec.resources.requests.storage on the PVC. Some backends finish filesystem growth only after Pods using the claim restart. Shrinking is not supported.

Snapshots need a VolumeSnapshotClass and a CSI driver that implements snapshot APIs. You create a VolumeSnapshot that names the source PVC; the driver produces a cluster-scoped VolumeSnapshotContent. Restore by creating a new PVC whose dataSourceRef points at that snapshot (set apiGroup: snapshot.storage.k8s.io).

apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
  name: shopbot-history-snap-1
  namespace: retail-chat
spec:
  volumeSnapshotClassName: csi-snap
  source:
    persistentVolumeClaimName: shopbot-history
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: shopbot-history-restore
  namespace: retail-chat
spec:
  accessModes: ["ReadWriteOnce"]
  resources:
    requests:
      storage: 20Gi
  dataSourceRef:
    apiGroup: snapshot.storage.k8s.io
    kind: VolumeSnapshot
    name: shopbot-history-snap-1

Cloning without a snapshot uses dataSourceRef with kind: PersistentVolumeClaim toward an existing claim—handy for seeding a read-only FAQ volume from a writer claim.

Ephemeral claims

An ephemeral volume embeds a PVC template in the Pod. Kubernetes creates a claim named like <pod>-<volume>, binds a PV, and deletes the claim when the Pod goes away. You get StorageClass features (size caps, snapshots where supported) without managing a long-lived PVC for scratch space.

volumes:
  - name: scratch
    ephemeral:
      volumeClaimTemplate:
        spec:
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 5Gi
          storageClassName: standard-ssd

Platform note

On OpenShift or HyperShift-hosted bare-metal spokes, StorageClasses still come from the platform team (OCS/ODF, cloud CSI, local-storage operator). App YAML for shopbot stays portable: claim a class name, mount the claim, leave disk SKUs to the operators that own the cluster.

Mini-lab

kubectl get sc
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: shopbot-history
spec:
  accessModes: ["ReadWriteOnce"]
  resources:
    requests:
      storage: 1Gi
EOF

kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: shopbot-history-probe
spec:
  volumes:
    - name: chat-history
      persistentVolumeClaim:
        claimName: shopbot-history
  containers:
    - name: probe
      image: alpine:3.20
      command: ["sh", "-c", "echo ok > /data/ping.txt; sleep 3600"]
      volumeMounts:
        - name: chat-history
          mountPath: /data
EOF

kubectl get pvc shopbot-history
kubectl exec shopbot-history-probe -- cat /data/ping.txt

Checklist

  • I request storage with a PVC, not a hard-coded disk ID in the Pod.
  • I know when Pending means WaitForFirstConsumer versus a missing StorageClass.
  • I can pick RWO / RWX / ROX / RWOP for shopbot’s concurrency needs.
  • I understand Delete vs Retain before I remove a claim.
  • I can expand a claim, snapshot when the driver allows it, and restore via dataSourceRef.

Where this leaves you

Shopbot’s history can survive Pod replacement. Next we return to configuration delivered as files—mounting ConfigMaps and Secrets—so settings and credentials travel beside this durable data path.