Home › CKAD practice tasks › Persist application data with a PersistentVolume
CKAD · Application Design and Build

Persist App Data with a PersistentVolume and Claim

Deployment `records-app` in namespace `records` appends a line to `/data/records.log` every five seconds, but `/data` is an `emptyDir`, so the records are lost whenever the Pod is replaced. This cluster has a single node; the data may live under `/srv/records` on that node.

Live lab access

This task description is public. Live practice requires full access unless this is the selected free weekly task. AI Lab Coach is available in single-task labs, including the free weekly task. See this week's free task.

Create an account to continue →

Read this week's free task →

On a phone? Read the task and create your account here, then sign in on a desktop or laptop for the terminal lab. Nothing starts until you choose Start.

The task

Deployment records-app in namespace records appends a line to /data/records.log every five seconds, but /data is an emptyDir, so the records are lost whenever the Pod is replaced. This cluster has a single node; the data may live under /srv/records on that node.

Requirements
1. Create PersistentVolume records-pv with capacity 256Mi, access mode ReadWriteOnce, reclaim policy Retain, storage class manual and a hostPath of /srv/records.
2. Create PersistentVolumeClaim records-pvc in records that requests 128Mi with access mode ReadWriteOnce from storage class manual.
3. Ensure the claim is bound to records-pv.
4. Change records-app so the data volume uses records-pvc instead of emptyDir, keeping the mount at /data, and ensure the new Pod writes its records there.

The cluster also has a default StorageClass; this claim must use the volume you created.

Original Prepium guide · Written for Kubernetes v1.35 and checked against the official documentation on 6 October 2026. Use a disposable practice cluster. A single-node one is best, because the example volume is a directory on one node.

Make a claim bind to your volume, not to the default StorageClass

A PersistentVolumeClaim never names its disk directly by default; the control plane looks for a PersistentVolume whose class, access modes and size all satisfy the claim, then binds the two one-to-one. The trap on clusters with a default StorageClass is that a claim which says nothing about its class is quietly given the default class, gets a freshly provisioned volume, and ignores the volume you made by hand.

notes-pv  (300Mi, RWO, class "")  ◀── bound ──  notes-claim (100Mi, RWO, class "")
                                                   ▲ volume in Deployment notes-writer at /notes

1. See what the cluster would do by default

kubectl get storageclass
kubectl get pv

A class marked (default) carries the annotation storageclass.kubernetes.io/is-default-class: "true". If one exists, any new claim without a storageClassName key is assigned it, and can then bind only to volumes of that class.

2. Create a classless volume and a claim that asks for no class

Save this as ckad-pvc-notes.yaml. Both objects set storageClassName: "". On the claim, the explicit empty string means "a volume with no class", which also stops the default from being filled in. Leaving the key out would not mean the same thing.

apiVersion: v1
kind: PersistentVolume
metadata:
  name: notes-pv
spec:
  capacity:
    storage: 300Mi
  accessModes:
  - ReadWriteOnce
  storageClassName: ""          # no class: only claims asking for "" can bind
  persistentVolumeReclaimPolicy: Retain
  hostPath:
    path: /var/tmp/prepium-notes
    type: DirectoryOrCreate
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: notes-claim
  namespace: prepium-tutorial
spec:
  storageClassName: ""          # explicit empty string, so the default class is NOT applied
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 100Mi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: notes-writer
  namespace: prepium-tutorial
spec:
  replicas: 1
  selector:
    matchLabels:
      app: notes-writer
  template:
    metadata:
      labels:
        app: notes-writer
    spec:
      containers:
      - name: writer
        image: busybox:1.36
        command: ["sh", "-c", "while true; do date >> /notes/stamps.txt; sleep 10; done"]
        volumeMounts:
        - name: notes
          mountPath: /notes
      volumes:
      - name: notes
        persistentVolumeClaim:
          claimName: notes-claim
kubectl create namespace prepium-tutorial
kubectl apply -f ckad-pvc-notes.yaml
kubectl get pv notes-pv
kubectl -n prepium-tutorial get pvc notes-claim

Both should report Bound, and the PV's CLAIM column names prepium-tutorial/notes-claim. The claim's CAPACITY shows 300Mi, not 100Mi: a claim gets at least what it asked for, and a statically created volume is bound whole.

3. Know the matching rules

When nothing matches, the claim stays Pending indefinitely and binds as soon as a matching volume appears. To force one particular volume, set spec.volumeName on the claim; the other rules still apply.

4. Prove the data outlives the Pod

kubectl -n prepium-tutorial rollout status deployment/notes-writer
kubectl -n prepium-tutorial exec deploy/notes-writer -- tail -n 2 /notes/stamps.txt
kubectl -n prepium-tutorial rollout restart deployment/notes-writer
kubectl -n prepium-tutorial rollout status deployment/notes-writer
kubectl -n prepium-tutorial exec deploy/notes-writer -- wc -l /notes/stamps.txt

The new Pod finds the earlier lines; with an emptyDir the count would restart from zero.

hostPath caveats

hostPath is a directory on whichever node runs the Pod, so it is only trustworthy on a single-node practice cluster; on several nodes a rescheduled Pod can land beside a different, empty directory. DirectoryOrCreate creates a missing directory with mode 0755 owned like the kubelet, so a non-root container may not be able to write to it. Host mounts also carry real security risk, which is why production uses a CSI driver or a local volume instead.

Common mistakes

Example checkEvidence
Claim bound to the intended volumespec.volumeName on the claim names your PV; both show Bound
Matching attributesSame storageClassName; compatible access modes; capacity at least the request
Workload uses the claimvolumes[].persistentVolumeClaim.claimName in the Pod template
Data persistsLine count keeps growing across a rollout restart

Cleanup: kubectl delete namespace prepium-tutorial, then kubectl delete pv notes-pv (PVs are cluster-scoped and Retain keeps it around). The files stay in /var/tmp/prepium-notes on the node until you remove them. References: Persistent Volumes, hostPath volumes, Storage Classes. Next, try binding to a named PV or StorageClass binding modes.

Difficulty
Medium · 8 minutes target
Domain
Application Design and Build
Grading
Live checks · partial credit

Practice scope

This additional curriculum lab is available in single-task practice with the existing 12-minute limit. The three CKAD mocks keep their original 17-task sets. Hints, an explained solution and requirement-by-requirement feedback are available in the lab.

Browse CKAD practice tasks →