Week 11: Storage in Kubernetes — PV, PVC & StorageClasses

Week 3 solved persistence for one Docker host with a named volume. Kubernetes needs a more general answer: a Pod might be rescheduled to a completely different physical node, so "the disk on this machine" isn't good enough. PersistentVolumes and PersistentVolumeClaims are that answer — a storage abstraction layer that lets a Pod ask for storage without caring exactly where or how it's physically provisioned.

Module 11 of 17 Week 11 of 18 ~3.5 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Explain the relationship between a PersistentVolume and a PersistentVolumeClaim
  • Use a StorageClass for dynamic provisioning instead of pre-creating volumes by hand
  • Run a database on Kubernetes whose data survives its Pod being rescheduled

1. PersistentVolumes & Claims

Kubernetes splits storage into two objects, deliberately separating "what storage exists" from "what a Pod asked for" — the same split you'll recognize from real infrastructure procurement.

the two halves
PersistentVolume (PV)       # a piece of ACTUAL storage in the cluster — could be
                             # a cloud disk, NFS share, or local disk. Cluster-scoped,
                             # created by an admin or dynamically by a StorageClass.

PersistentVolumeClaim (PVC) # a Pod's REQUEST for storage — "I need 10Gi, ReadWriteOnce."
                             # Kubernetes finds (or provisions) a PV that satisfies it
                             # and BINDS the two together.
pvc.yaml — what a Pod actually asks for
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pg-storage
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
mounting the claim into a Pod
spec:
  containers:
    - name: postgres
      image: postgres:16
      volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: pg-storage

The Pod spec never mentions cloud disks, NFS servers, or physical paths — it just names a claim. That indirection is exactly what lets the same manifest work unchanged on a local cluster, AWS, GCP or on-prem, differing only in how the PV behind the claim gets provisioned.

This is the same problem as Week 3, one level up

A Docker named volume solved "survive a container being recreated on the same host." A PVC solves the harder version: "survive a Pod being recreated on possibly a DIFFERENT node." The concept — name the storage, mount it at the right path — carries over directly.

2. StorageClasses & Dynamic Provisioning

Manually pre-creating a PV for every PVC doesn't scale. A StorageClass tells Kubernetes how to provision storage automatically the moment a PVC asks for it — no admin has to create a PV by hand first.

checking available StorageClasses
kubectl get storageclass
# NAME                 PROVISIONER
# standard (default)   k8s.io/minikube-hostpath

# On a cloud cluster you'd typically see something like:
# gp3 (default)         ebs.csi.aws.com
requesting dynamic provisioning explicitly
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pg-storage
spec:
  storageClassName: standard   # <- triggers automatic PV creation
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi

Apply that PVC and, within seconds, a matching PV appears automatically — created by the StorageClass's provisioner, sized exactly to the request, bound to your claim. Most managed cloud clusters ship with a sensible default StorageClass, which is why you often don't even need to specify one explicitly.

Verify with kubectl get pv after applying a PVC

If a new PV doesn't appear and your PVC stays "Pending," check the StorageClass name for typos, or confirm your cluster actually has a provisioner configured — a common local-cluster gotcha, since not every local setup ships dynamic provisioning out of the box.

3. Access Modes & Reclaim Policy

Two settings shape how a PV can be used and what happens to it after its claim is deleted — both easy to get wrong the first time.

access modes
ReadWriteOnce (RWO)   # mountable read-write by a SINGLE node at a time. Most common —
                       # fits a database with one Pod writing to it.

ReadOnlyMany (ROX)    # mountable read-only by MANY nodes simultaneously. Good for
                       # shared, static reference data.

ReadWriteMany (RWX)   # mountable read-write by MANY nodes simultaneously. Needs a
                       # storage backend that supports it (NFS, some cloud file
                       # systems) — most cloud block storage (EBS, etc.) is RWO only.
reclaim policy — what happens after the PVC is deleted
Retain   # the PV and its data survive PVC deletion, but move to "Released" status —
         # NOT automatically reusable until an admin manually intervenes. Safest default
         # for anything you can't afford to lose.

Delete   # the underlying storage is deleted along with the PVC. Common default for
         # dynamically-provisioned dev/test volumes — convenient, but genuinely
         # destructive if applied to production data by mistake.
Check your StorageClass's default reclaimPolicy before trusting it with real data

Many dynamically-provisioned StorageClasses default to Delete — meaning an accidental kubectl delete pvc on production data is unrecoverable through Kubernetes alone. For anything that matters, explicitly set reclaimPolicy: Retain, or rely on a separate backup strategy (the same lesson from Week 3's volume backups).

4. A Database on Kubernetes

Putting it together: PostgreSQL, on Kubernetes, with dynamically-provisioned storage that survives the Pod being deleted and rescheduled.

pvc.yaml + deployment.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pg-storage
spec:
  accessModes: [ReadWriteOnce]
  resources:
    requests:
      storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:16
          env:
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: DB_PASSWORD
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: pg-storage

Note replicas: 1 — a plain Deployment with a shared PVC doesn't safely support multiple replicas writing to the same ReadWriteOnce volume simultaneously. Running a genuinely multi-replica, stateful database correctly is exactly what StatefulSets (Week 15) exist for; a single-replica Deployment is a reasonable, simpler starting point for one database instance.

Many teams run production databases OFF the cluster

Even with all these tools, a large number of real production setups use a managed database service (RDS, Cloud SQL) rather than self-hosting a database on Kubernetes — stateful data adds real operational weight. Knowing how to do it yourself is valuable regardless of which choice you make.

5. Hands-on Exercise

Hands-on

Run a database on Kubernetes and prove its data survives Pod deletion

Set up a stateful workload with a PVC, populate it with real data, then delete and recreate the Pod to confirm the storage abstraction is doing its job.

Part 1 — Set it up:

  1. Write a PVC requesting storage from your cluster's default StorageClass, and a single-replica Deployment for Postgres (or MySQL/MongoDB) mounting it at the correct data path.
  2. Apply both, and confirm with kubectl get pvc that it's Bound, and kubectl get pv that a matching PV was dynamically created.
  3. Exec into the running Pod, connect to the database, and create a table with a few rows.
Hint

If your PVC stays in Pending status, run kubectl describe pvc <name> and check the Events at the bottom — it'll usually tell you exactly why (no matching StorageClass, no provisioner configured, etc.).

Part 2 — Prove the persistence, the hard way:

  1. Delete the Pod directly (not the Deployment) — kubectl delete pod <pod-name> — and wait for the Deployment controller to create a replacement.
  2. Connect to the new Pod and confirm your table and data are still there.
  3. Now delete the entire Deployment (kubectl delete deployment postgres, leaving the PVC alone) and recreate it fresh from your manifest. Confirm the data survives this too.
  4. Finally, delete the PVC itself and check kubectl get pv — is the underlying PV gone, or does it still exist in a "Released" state? Explain what this tells you about your StorageClass's reclaim policy, based on what you observe.
Hint

If you're on Minikube's default hostpath provisioner, its default reclaim policy is typically Delete — a good, safe environment to actually witness data loss on purpose, which is a much better place to learn this lesson than on a real production cluster.

6. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

What's the relationship between a PersistentVolume and a PersistentVolumeClaim?

A PersistentVolume represents actual provisioned storage in the cluster. A PersistentVolumeClaim is a Pod's request for storage meeting certain criteria (size, access mode). Kubernetes binds a matching PV to a PVC, and the Pod mounts the claim — never referencing the underlying storage directly.

Q2

What does a StorageClass let you avoid doing manually?

Pre-creating a PersistentVolume by hand for every claim. A StorageClass defines HOW to dynamically provision storage on demand, so a PV is created automatically the moment a matching PVC is submitted, sized exactly to what was requested.

Q3

Why can't you safely run multiple replicas of a Deployment all writing to the same ReadWriteOnce PVC?

ReadWriteOnce means the volume can be mounted read-write by only a single node at a time — multiple Pods (likely on different nodes) trying to write to it concurrently isn't safely supported. A single-replica Deployment sidesteps this; genuinely replicated stateful workloads need StatefulSets with per-replica storage instead.

Q4

What's the practical risk of a StorageClass with reclaimPolicy: Delete on production data?

Deleting the PVC (accidentally or otherwise) permanently deletes the underlying storage and all its data along with it, with no recovery path through Kubernetes. For anything you can't afford to lose, use reclaimPolicy: Retain or maintain a separate, independent backup strategy.