Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

🏠 Back to Blog

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: Memory uses 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 via volumeDevices with devicePath, not volumeMounts.

Access modes

ModeShortMeaning
ReadWriteOnceRWORead-write by a single node (many Pods on that node allowed)
ReadOnlyManyROXRead-only by many nodes
ReadWriteManyRWXRead-write by many nodes (NFS, CephFS, Azure Files)
ReadWriteOncePodRWOPRead-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 becomes Released and keeps its data. An admin must clean it and remove the claimRef before it can be reused.
  • Delete: the PV and the backing storage are deleted. Default for dynamically provisioned volumes.
  • Recycle: deprecated (basic rm -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. WaitForFirstConsumer delays 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: true allows growing a PVC by editing spec.resources.requests.storage (growing only, never shrinking).
  • Only one class should be default. A PVC with no storageClassName uses 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.
  • subPath mounts a sub-directory of a volume (for example to avoid lost+found clobbering a database directory).
  • Set securityContext.fsGroup so 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:

SymptomLikely cause
PVC PendingNo matching PV (size, access mode, class, mode) or no provisioner; or WaitForFirstConsumer and no Pod yet
Pod ContainerCreating, FailedMount or FailedAttachVolumeVolume 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 writingMissing 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 pvc to confirm Bound before creating the Pod.