CertGrid CertGrid
Hands-on Lab·Certified Kubernetes Application Developer

Pod Volumes and Volume Lifetime

An application needs two kinds of storage: scratch space that can vanish, and data that must not. `emptyDir` and a PersistentVolumeClaim cover both, and the only thing that reliably confuses people is a claim that reports `Pending` while nothing at all is wrong with it. All three are demonstrated, including deleting the Pod and reading the file back from a new one.

Application Design and Build Guide 14 of 44 Beginner

Written against the versions above. `local-path` is this cluster's default StorageClass and uses `WaitForFirstConsumer`, so binding is deferred until a Pod is scheduled - the provisioner has to know which node to put the directory on. An `Immediate` class binds straight away, which is why the same manifest behaves differently on different clusters.

The `local-path` provisioner places the volume on whichever node the Pod is scheduled to, so the node column matters in the last two steps.
Server NameIP AddressOSRolesCPURAMHDD
CKA1001192.168.0.175Ubuntu 26.04 LTSControl Plane Node2 Core4 GB50 GB
CKA1001-NODE01192.168.0.176Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB
CKA1001-NODE02192.168.0.177Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB
CKA1001-NODE03192.168.0.178Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB

Before you start

  1. An emptyDir lives and dies with the Pod

    Two emptyDir volumes: one default, one with medium: Memory.

    written to disk-backed
    written to memory-backed

    Both writable, and df shows what they actually are - the default is the node's disk, the Memory one is a tmpfs.

    Use the default for scratch space, caches and anything two containers need to share (see guide 20). Use medium: Memory when the data must never touch disk, remembering it counts against the container's memory limit - a tmpfs full of files can get you OOMKilled.

    Both are deleted with the Pod. Not with the container - a container restart keeps the emptyDir - but with the Pod.

    bash Example session
    kubectl create namespace ckad-volnamespace/ckad-vol createdkubectl -n ckad-vol wait --for=condition=Ready pod/scratch --timeout=120spod/scratch condition metkubectl -n ckad-vol exec scratch -- sh -c 'echo "written to disk-backed" > /scratch/a.txt; echo "written to memory-backed" > /inmem/b.txt; cat /scratch/a.txt /inmem/b.txt'written to disk-backedwritten to memory-backedkubectl -n ckad-vol exec scratch -- sh -c 'df -h /scratch /inmem | grep -vE "^Filesystem"'/dev/mapper/ubuntu--vg-ubuntu--lv                         47.1G     19.9G     25.0G  44% /scratchtmpfs                    16.0M      4.0K     16.0M   0% /inmem

    Expected resultBoth volumes writable, one on disk and one on tmpfs.

    Success conditionYou can pick between the two ephemeral options and know what each costs.

  2. A claim that waits on purpose

    A PVC with no Pod using it:

    NAME   STATUS    VOLUME   CAPACITY   STORAGECLASS
    data   Pending                          local-path

    Pending, and nothing is wrong. The reason is in the StorageClass:

    NAME         MODE
    local-path   WaitForFirstConsumer

    The provisioner creates the volume on a specific node, so it cannot act until the scheduler has decided which node the Pod goes to. Binding is deliberately deferred - if it bound immediately, it might pick a node the Pod cannot be scheduled to and deadlock.

    This is worth recognising cold, because the instinct is to start debugging storage. The distinction:

    • Pending + WaitForFirstConsumer + no Pod yet -> normal, create the Pod.
    • Pending with a Pod already mounting it -> a real problem. kubectl describe pvc will have events.
    bash Example session
    sleep 10; kubectl -n ckad-vol get pvc dataNAME   STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGEdata   Pending                                      local-path     <unset>                 10skubectl get storageclass local-path -o 'custom-columns=NAME:.metadata.name,MODE:.volumeBindingMode'NAME         MODElocal-path   WaitForFirstConsumer

    Expected resultA Pending claim and a WaitForFirstConsumer class.

    Success conditionYou will not debug a PVC that is behaving correctly.

  3. A Pod binds it

    The moment a Pod mounting the claim is scheduled:

    NAME   STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS
    data   Bound    pvc-3316a118-094b-4be5-9221-714fe45610f3   128Mi      RWO            local-path

    Bound, with a PersistentVolume created automatically and named after the claim's UID. That is dynamic provisioning - you never wrote a PV.

    survives the pod

    Note ACCESS MODES: RWO - ReadWriteOnce, meaning it can be mounted read-write by one node at a time. It is the default and the only mode most provisioners support, and it is why a Deployment with two replicas and one RWO claim will have one Pod stuck if the replicas land on different nodes.

    bash Example session
    kubectl -n ckad-vol wait --for=condition=Ready pod/writer --timeout=180spod/writer condition metkubectl -n ckad-vol get pvc dataNAME   STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGEdata   Bound    pvc-3316a118-094b-4be5-9221-714fe45610f3   128Mi      RWO            local-path     <unset>                 15skubectl -n ckad-vol exec writer -- sh -c 'echo "survives the pod" > /data/keep.txt; cat /data/keep.txt'survives the podkubectl -n ckad-vol get pod writer -o 'custom-columns=POD:.metadata.name,NODE:.spec.nodeName'POD      NODEwriter   cka1001-node01

    Expected resultThe claim Bound and a file written to it.

    Success conditionYou have data in a volume the Pod does not own.

  4. Delete the Pod and look again

    The Pod is deleted. The claim is not:

    NAME   STATUS   VOLUME                                     CAPACITY
    data   Bound    pvc-3316a118-...        128Mi

    and a completely new Pod mounting the same claim reads the file back:

    survives the pod

    That is the whole point. The lifecycle of a PVC is tied to the namespace, not to any Pod. Delete the workload, redeploy it, roll it forward - the data stays.

    What eventually removes it is deleting the claim, at which point the StorageClass's reclaimPolicy decides. local-path here is Delete, so the volume and its data go with the claim. Retain would keep the PV and its data behind for manual recovery.

    bash Example session
    kubectl -n ckad-vol delete pod writer --wait=truepod "writer" deleted from ckad-vol namespacekubectl -n ckad-vol get pvc dataNAME   STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGEdata   Bound    pvc-3316a118-094b-4be5-9221-714fe45610f3   128Mi      RWO            local-path     <unset>                 47skubectl -n ckad-vol wait --for=condition=Ready pod/reader --timeout=180spod/reader condition metkubectl -n ckad-vol exec reader -- cat /data/keep.txtsurvives the podkubectl delete namespace ckad-vol --wait=falsenamespace "ckad-vol" deleted

    Expected resultThe claim survives the Pod and the file is still readable.

    Success conditionYou can prove data outlives the Pod that wrote it.

Troubleshooting

Official sources