hands-on labKubernetes in your browser

Persistent Storage for AI Workloads: PVCs, StorageClass & the Checkpoint Pattern

Stop losing your training checkpoints when pods restart. Learn the PersistentVolumeClaim model end-to-end, StorageClass selection, accessModes (and the RWO-is-per-node trap), the bind/mount/persist lifecycle, and three triage scenarios where storage chains break.

time
35 min
checked steps
5
level
Intermediate
setup
None

$29.99a month with Pro

Pro opens this lab and every other one. Cancel any time.

The job

You're 4 hours into a fine-tuning run when the pod restarts. The trainer comes back and starts from scratch, no checkpoint, no progress, four hours of GPU time gone. The fix is one PersistentVolumeClaim short. This lab teaches you the storage model that prevents that scenario, and the three places it most commonly breaks in production.

5 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    The ephemeral storage problem: why pods need PVCs

    A pod's filesystem is ephemeral by default.

    read this step below ↓

  2. 2

    Author your first PVC and bind it to a pod

    Your task: create a PersistentVolumeClaim for a 1Gi training-checkpoints volume, plus a writer pod that mounts it at /data and drops a marker file.

  3. 3

    Persistence across pod restarts: the checkpoint pattern

    The whole point of using a PVC is that the storage outlives the pod.

  4. 4

    AccessModes: RWO is per-node, RWOP is per-pod

    This is the step where most engineers' mental model of accessModes breaks.

  5. 5

    Triage Day: three pods broken at three different storage layers

    The lab platform has just deployed three pods that should be Running but aren't.

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: The ephemeral storage problem: why pods need PVCs

The ephemeral storage problem

A pod's filesystem is ephemeral by default. Anything written to /tmp, /data, /checkpoints, or any other path inside the container lives in the container runtime's writable layer — a copy-on-write overlay on top of the image. When the pod is deleted, the layer is destroyed. When the container restarts inside an existing pod (e.g., from a crash), the writable layer is also discarded; you come back to the image's original state.

This is fine for stateless workloads (HTTP servers, batch jobs that read input and write output to S3). It is catastrophic for AI workloads — model checkpoints, dataset caches, embedding indices, fine-tuning state, all gone when something restarts.

The fix is PersistentVolumeClaims (PVCs). A PVC is a request for storage that outlives the pod. The cluster matches the request to a PersistentVolume (PV), the PV mounts a real disk somewhere, and the pod mounts the PV at a path of your choosing.

The three objects you'll work with

ObjectAuthored byWhat it represents
StorageClass (cluster-scoped)Cluster adminA recipe for provisioning storage — names a CSI driver and its parameters. Persistent across pods.
PersistentVolume (PV, cluster-scoped)Usually auto-created by the StorageClass's provisionerAn actual disk — backed by a hostPath, a network volume, a cloud disk.
PersistentVolumeClaim (PVC, namespaced)Pod author (you)A request for storage — size, accessMode, optional StorageClass. The pod references it by name.

The lifecycle:

1. You write a PVC: "I need 5Gi RWO from default StorageClass"
2. Cluster sees the PVC; consults the named StorageClass's provisioner
3. Provisioner creates a PV (a real disk) of that size
4. PVC.status.phase: Pending → Bound (linked to the PV)
5. Your pod's spec.volumes[].persistentVolumeClaim.claimName = <pvc-name>
6. Kubelet on the chosen node attaches the volume + mounts it inside the container
7. Pod runs; reads/writes to the mount path are durable

Read the cluster

Run the step's command (or the Run button) to see what's available. The platform pre-installed one StorageClass (local-path) — that's what we'll use throughout the lab.

# 1. All StorageClasses with their provisioners and key fields.
kubectl get storageclass -o wide

# 2. PVCs in this namespace (should be empty — fresh start).
kubectl get pvc

# 3. The "default" StorageClass — used when a PVC omits storageClassName.
kubectl get sc -o jsonpath='{range .items[*]}{.metadata.name}: {.metadata.annotations.storageclass\.kubernetes\.io/is-default-class}{"\n"}{end}'

Anatomy of the StorageClass you have

The output of kubectl get sc -o wide has six columns that every NCA-AIIO student should be able to interpret on sight:

ColumnWhat it meansLocal-path's value
NAMEStorageClass name; PVCs reference it via storageClassNamelocal-path
PROVISIONERThe CSI driver that creates PVs for this classrancher.io/local-path
RECLAIMPOLICYWhat happens to the PV when the PVC is deleted: Delete (destroy the disk) or Retain (preserve the data, leave the PV in Released)Delete
VOLUMEBINDINGMODEWhen does the PV get provisioned: Immediate (right when the PVC is created) or WaitForFirstConsumer (defer until a pod references the PVC, so the scheduler can pick a node first)WaitForFirstConsumer
ALLOWVOLUMEEXPANSIONCan a PVC be resized after creation?false for local-path
AGEObject agevaries

Two of these are pedagogically critical:

reclaimPolicy: Delete — when you kubectl delete pvc, the underlying disk + data is destroyed. For training checkpoints in a real production cluster, you'd typically want Retain (keep the data) plus a backup mechanism (snapshots, copy-to-S3-on-finalize). For lab work, Delete is fine.

volumeBindingMode: WaitForFirstConsumer — when you kubectl apply -f pvc.yaml, the PVC will sit Pending and produce a "WaitForFirstConsumer" event. This is normal. It only binds when a pod references the PVC. The reason: the scheduler picks the pod's node first, then the provisioner creates a PV that's local to that node. If the PV were created up front, the pod might be unable to schedule on the node where the PV lives.

A common newcomer mistake: applying a PVC, seeing it Pending, panicking. With WaitForFirstConsumer, "Pending until a pod claims it" is correct behavior. We'll see this in the next step.

Why this matters in practice: every NCA-AIIO student has seen a colleague's training run lose hours of progress to ephemeral storage. The PVC model is the answer, but only if you understand the StorageClass parameters that control durability (reclaimPolicy) and binding timing (volumeBindingMode). The rest of this lab makes those parameters concrete.

what you get

  • A Kubernetes cluster and terminal in your browser
  • A check at the end of every step
  • Hints and the full solution
  • Nothing to install or configure

before you start

  • Completed `nca-aiio-resource-requests-limits` (or comfortable with kubectl + pod manifests)
  • Familiar with `kubectl describe`, reading pod events, and the get/edit/delete/apply pattern

Exam domains covered

AI Infrastructure & OperationsStorage & Data Management

Part of the GPU and Kubernetes track

Lab 8 of 20. GPU scheduling on your own Kubernetes cluster, the GPU Operator, CUDA kernels and profiling.

Questions about this lab

What's the difference between PV, PVC, and StorageClass?

A PersistentVolume (PV) is a piece of storage in the cluster, a disk, a network mount, a cloud volume, represented as an API object. A PersistentVolumeClaim (PVC) is a request for storage written by a pod author: "I need 5Gi, ReadWriteOnce, on this StorageClass." A StorageClass describes how to dynamically provision PVs that satisfy claims, its provisioner field names the CSI driver, and parameters are passed through to that driver. The flow: pod author writes PVC → cluster matches a PV (or asks the StorageClass's provisioner to create one) → PVC binds to PV → pod's volumes.persistentVolumeClaim.claimName mounts the PV inside the pod's filesystem. Most production clusters use dynamic provisioning (PVCs trigger PV creation); static PVs are rare and usually for legacy / shared infrastructure.

What does ReadWriteOnce actually mean?

ReadWriteOnce (RWO) means the volume can be mounted read-write by one node at a time, not one pod. This is the field that bites engineers most often. On a single-node cluster (or when your scheduler co-locates pods), two pods can share an RWO volume just fine; the kernel sees a single mount and both pods write through the same filesystem. RWO blocks pods on different nodes from attaching the same volume simultaneously. If you actually need single-pod-only access, use ReadWriteOncePod (Kubernetes 1.22+), which the scheduler enforces by refusing to schedule a second pod that references the same PVC. For multi-node concurrent access, use ReadWriteMany (RWX), which requires a StorageClass backed by a network filesystem (NFS, CephFS, EFS, FSx, GlusterFS), block-device storage classes (most cloud disks, local-path) cannot satisfy RWX.

Why is my PVC stuck Pending?

Three common reasons and how to tell which one. (1) volumeBindingMode: WaitForFirstConsumer, the PVC won't bind until a pod references it. Look at kubectl describe pvc events for "WaitForFirstConsumer", that's normal and resolves when the pod is created. (2) The StorageClass's provisioner can't satisfy the claim, wrong accessMode (asking RWX from a block-device class), oversized request, or the provisioner's CSI controller is down. Look at events for "ProvisioningFailed". (3) The named storageClassName doesn't exist in the cluster. kubectl get storageclass to confirm what's available; the default class is whichever has is-default-class: "true" in annotations. The PVC's events are the diagnostic, kubectl describe pvc <name> shows whether the cluster has tried to bind, why it failed, and whether it's still waiting.

What happens to my data when I delete the PVC?

The StorageClass's reclaimPolicy decides: Delete (the default for most dynamic provisioners) tears down the underlying PV and the storage backend's actual data, your training checkpoints are gone. Retain leaves the PV in Released state with the data intact, but the PV won't be re-bindable to a new PVC without manual intervention (you must kubectl edit it to clear the claimRef field). For production, the safe default is to author StorageClasses with reclaimPolicy: Retain for any data you can't afford to lose, and have a backup mechanism on top (snapshots, velero, application-level checkpointing to object storage). For ephemeral caches and scratch space, Delete is correct, you want the disk to come back when the experiment ends.

What you'll learn

Persistent storage is what separates Kubernetes pods from disposable processes. Without it, your training checkpoints, your model weights, your dataset caches all evaporate at the next pod restart. This lab teaches the PersistentVolumeClaim model the way platform engineers actually use it: as a claim (your declarative request) that the cluster's StorageClass resolves into a PersistentVolume (the actual disk). You'll author claims, read the binding state machine, mount volumes into pods, and test the survival of data across pod deletions.

The lab also covers what trips engineers up most: the three accessModes (ReadWriteOnce, ReadWriteOncePod, ReadWriteMany) and the surprise that ReadWriteOnce is per node, not per pod, which means two pods on the same node CAN share an RWO volume, and your assumption that "RWO blocks scale-out" is probably wrong. You finish with a Triage Day where three pods break at three different links in the storage chain (PVC reference / accessMode collision / missing StorageClass), and you fix each one by reading the cluster's response.