Skip to content

Volumes & Persistent Storage — Full Reference

Reference: https://kubernetes.io/docs/concepts/storage/persistent-volumes/


Exam Priority — Quickest Path

For simple volume tasks, dry-run + minimal edit:

kubectl run alpine-pod --image=alpine:latest --restart=Never --dry-run=client -o yaml > pod.yaml
# edit pod.yaml — add volumeMounts + volumes sections only
kubectl apply -f pod.yaml

kubectl run cannot set volumes imperatively — you always need YAML for this. But you never start from scratch — dry-run gives you the scaffold, you only add the two sections you need.

When you need a PV/PVC: the question asks for persistent storage that survives pod deletion. A ConfigMap volume is read-only config — it's not persistent storage. Know which one the question is asking for.


Volume Types — What Each One Is

Type Survives pod deletion? Use case
emptyDir No — deleted with pod Scratch space, sharing data between containers in the same pod
configMap N/A — read from ConfigMap object Inject config files into a container
secret N/A — read from Secret object Inject sensitive files into a container
hostPath Yes — on the node Access node filesystem (logs, Docker socket). Dangerous in prod.
persistentVolumeClaim Yes — independent lifecycle Databases, stateful apps, anything needing durable storage

ConfigMap as a Volume

The raw notes exercise: mount a ConfigMap as a volume so a container can read it as files.

apiVersion: v1
kind: Pod
metadata:
  name: alpine-pod
spec:
  restartPolicy: Never
  containers:
  - name: alpine-container
    image: alpine:latest
    command: ["/bin/sh"]
    args: ["-c", "tail -f /config/log.txt"]
    volumeMounts:
    - name: config-volume       # must match the name in volumes below
      mountPath: /config        # directory inside the container where files appear
  volumes:
  - name: config-volume         # the link between volumeMounts and this definition
    configMap:
      name: log-configmap       # name of the ConfigMap object in the cluster

How the linking works: volumeMounts.name and volumes.name must be identical — that's how Kubernetes knows which volume to mount at which path. The name is arbitrary, just has to match.

Result: every key in log-configmap becomes a file inside /config/. If the ConfigMap has a key log.txt, the file /config/log.txt is created with its value as contents.

command vs args and the -c flag

command: ["/bin/sh"]
args: ["-c", "tail -f /config/log.txt"]

command = overrides Docker's ENTRYPOINT. Here it's /bin/sh — the shell binary.

args = passed to the command as arguments. -c is a shell flag meaning "execute this string as a command."

Without -c: /bin/sh opens an interactive shell and waits for input. Nothing runs. With -c "tail -f /config/log.txt": the shell runs that string as a command and starts tailing the file.

So the full execution is: /bin/sh -c "tail -f /config/log.txt" — run the shell, pass it a command string.

The $do alias

export do="--dry-run=client -o yaml"

Set this at the start of every exam/session. Then:

kubectl run alpine-pod --image=alpine:latest --restart=Never $do > pod.yaml

Instead of typing --dry-run=client -o yaml every time. $do expands to that string.

--dry-run=client = generate the YAML locally, don't send it to the API server (nothing gets created). -o yaml = output as YAML.

Together: generate a base pod spec you can edit before creating.

kubectl explain — When You Forget the Field Names

kubectl explain pod.spec.volumes                      # all volume types and their fields
kubectl explain pod.spec.containers.volumeMounts      # what goes in volumeMounts
kubectl explain pod.spec.volumes.configMap            # configMap volume fields specifically
kubectl explain pod.spec.volumes.persistentVolumeClaim

In the exam, if you forget whether it's mountPath or path, just run kubectl explain. It shows field names, types, and descriptions inline — faster than searching the docs.


Persistent Volumes — The Full Model

The problem emptyDir doesn't solve: a pod dies and restarts — emptyDir is wiped. A database needs storage that outlives the pod.

The PV/PVC model separates two concerns:

  • PersistentVolume (PV) — the actual storage resource. Created by an admin (or dynamically by a StorageClass). Exists at the cluster level, independent of any pod.
  • PersistentVolumeClaim (PVC) — a request for storage by a user/app. Says "I need 5Gi, ReadWriteOnce." Kubernetes binds it to a PV that satisfies the request.
  • Pod — mounts the PVC. Doesn't care about the underlying storage type.
Admin creates PV (10Gi, hostPath)
User creates PVC (requests 5Gi, RWO)
Kubernetes binds PVC to PV (PV must satisfy PVC's requirements)
Pod references PVC by name in volumes section

PersistentVolume YAML

apiVersion: v1
kind: PersistentVolume
metadata:
  name: my-pv
spec:
  capacity:
    storage: 10Gi               # how much storage this PV provides

  accessModes:
  - ReadWriteOnce               # who can mount it and how (see below)

  persistentVolumeReclaimPolicy: Retain   # what happens when PVC is deleted (see below)

  storageClassName: manual      # must match the PVC's storageClassName

  hostPath:                     # the actual storage backend
    path: /mnt/data             # path on the node (exam uses this — simple)

PersistentVolumeClaim YAML

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-pvc
  namespace: default
spec:
  accessModes:
  - ReadWriteOnce               # must match the PV

  resources:
    requests:
      storage: 5Gi              # how much you're requesting (PV must have >= this)

  storageClassName: manual      # must match the PV's storageClassName

Kubernetes binds the PVC to a PV when: - The PV's storageClassName matches the PVC's storageClassName - The PV's accessModes contains the mode the PVC requests - The PV's capacity.storage >= PVC's resources.requests.storage - The PV isn't already bound to another PVC


Access Modes

Mode Short Meaning
ReadWriteOnce RWO Mounted read-write by one node at a time
ReadOnlyMany ROX Mounted read-only by many nodes simultaneously
ReadWriteMany RWX Mounted read-write by many nodes simultaneously
ReadWriteOncePod RWOP Mounted read-write by one pod (stricter than RWO)

Critical: RWO means one node, not one pod. Multiple pods on the same node can all mount an RWO volume. RWX is what you need for shared storage across nodes (NFS, CephFS, etc.). hostPath only supports RWO.


Reclaim Policy

What happens to the PV when the PVC that's bound to it is deleted:

Policy What happens
Retain PV stays, data stays. Status becomes Released. Admin must manually clean up.
Delete PV and its underlying storage are deleted automatically (used with cloud storage)
Recycle Deprecated. Don't use.

For the exam: Retain = data is safe but PV is not reusable until manually cleaned. Delete = cloud-style cleanup.


Mount a PVC in a Pod

apiVersion: v1
kind: Pod
metadata:
  name: app-pod
spec:
  containers:
  - name: app
    image: nginx
    volumeMounts:
    - name: storage             # must match volumes.name below
      mountPath: /data          # where to mount inside the container
  volumes:
  - name: storage
    persistentVolumeClaim:
      claimName: my-pvc         # name of the PVC object

The pod doesn't reference the PV directly — it always goes through the PVC. The PVC abstracts away what storage is actually backing it.


StorageClass — Dynamic Provisioning

With static provisioning (above), an admin creates PVs manually before users can claim them. With dynamic provisioning, a StorageClass automatically creates a PV when a PVC is submitted.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast
provisioner: kubernetes.io/aws-ebs     # what creates the actual storage
parameters:
  type: gp2
reclaimPolicy: Delete
allowVolumeExpansion: true

PVC that uses it:

spec:
  storageClassName: fast      # matches StorageClass name
  resources:
    requests:
      storage: 20Gi

Kubernetes calls the provisioner → provisioner creates an EBS volume → PV is auto-created → PVC binds to it.

In the exam: if the question doesn't mention StorageClass and uses hostPath, you're doing static provisioning. If it mentions a StorageClass name, include storageClassName in both PV and PVC.


PV / PVC Lifecycle States

State Meaning
Available PV exists, not yet bound to any PVC
Bound PV is bound to a PVC
Released PVC was deleted, PV still exists (Retain policy) — not yet available for new claims
Failed Automatic reclamation failed
kubectl get pv                     # check STATUS column
kubectl get pvc                    # check STATUS — should be Bound for a working setup

If PVC is stuck in Pending: the PV doesn't match (wrong storageClassName, wrong accessMode, insufficient capacity, or already bound).


All Volume Commands

# PersistentVolumes (cluster-scoped — no -n flag)
kubectl get pv
kubectl describe pv my-pv
kubectl delete pv my-pv

# PersistentVolumeClaims (namespace-scoped)
kubectl get pvc
kubectl get pvc -n my-namespace
kubectl describe pvc my-pvc
kubectl delete pvc my-pvc

# StorageClasses (cluster-scoped)
kubectl get storageclass
kubectl get sc                     # short name

# Check what's mounted in a pod
kubectl describe pod my-pod        # Volumes section shows what's mounted

# kubectl explain for field reference
kubectl explain pv.spec
kubectl explain pvc.spec
kubectl explain pod.spec.volumes.persistentVolumeClaim
kubectl explain pod.spec.containers.volumeMounts

Common Exam Patterns

"Create a PV and PVC, mount it in a pod":

# 1. Create PV
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolume
metadata:
  name: task-pv
spec:
  capacity:
    storage: 1Gi
  accessModes: [ReadWriteOnce]
  persistentVolumeReclaimPolicy: Retain
  storageClassName: manual
  hostPath:
    path: /mnt/data
EOF

# 2. Create PVC
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: task-pvc
spec:
  accessModes: [ReadWriteOnce]
  storageClassName: manual
  resources:
    requests:
      storage: 500Mi
EOF

# 3. Verify binding
kubectl get pv,pvc                 # both should show Bound

# 4. Mount in pod — generate scaffold + edit
kubectl run task-pod --image=nginx --restart=Never --dry-run=client -o yaml > pod.yaml
# add volumeMounts + volumes sections, then:
kubectl apply -f pod.yaml

Debugging why PVC is Pending:

kubectl describe pvc my-pvc       # Events section shows exact reason
kubectl get pv                    # check if a matching PV exists and is Available

Most common causes: - storageClassName mismatch between PV and PVC - PV accessModes doesn't include what PVC requests - PV capacity is less than PVC's request - PV is already Bound to a different PVC