Lesson  in  Kubernetes 101

Persistent storage

From shared memory to a real disk: compare emptyDir with a PersistentVolumeClaim backed by a StorageClass, and prove that the data survives the Pod being recreated.

Containers have the memory of a goldfish: everything they write to their filesystem disappears with them. You already mounted a volume in the previous lesson (that ConfigMap in /etc/config), but it was read-only. This lesson is about writing data and about the key question: how long does it survive?

You will see two answers: the emptyDir, which lives as long as the Pod lives, and the PersistentVolumeClaim, which lives as long as you say.

The book's Storage chapter explains the chain StorageClass → PersistentVolume → PersistentVolumeClaim from the top down. Here you are going to walk it the other way: asking for a volume and seeing who answers. Work from the dev-machine tab.

Step 1: emptyDir, the ephemeral volume

Create pod-efimero.yaml (efimero means "ephemeral"):

cat << 'EOF' > pod-efimero.yaml
apiVersion: v1
kind: Pod
metadata:
  name: efimero
spec:
  containers:
  - name: app
    image: ghcr.io/iximiuz/labs/nginx:alpine
    volumeMounts:
    - name: cache
      mountPath: /cache
  volumes:
  - name: cache
    emptyDir: {}
EOF

The YAML, in two questions

What exactly is an emptyDir?

An empty directory that Kubernetes creates on the node when the Pod starts and destroys when the Pod disappears. It survives container restarts (crashes included), but not the deletion of the Pod. Its natural use: cache, temporary files and data shared between containers of the same Pod.

Why {} as the value?

It is an empty object: emptyDir needs no configuration. It accepts options such as medium: Memory (mount it in RAM) or sizeLimit, but the defaults are enough here.

Apply it, write a piece of data and run the disappointment experiment:

kubectl apply -f pod-efimero.yaml
kubectl wait --for=condition=Ready pod/efimero --timeout=60s
kubectl exec efimero -- sh -c 'echo hola > /cache/hola.txt'
kubectl exec efimero -- cat /cache/hola.txt
kubectl delete pod efimero
kubectl apply -f pod-efimero.yaml
kubectl wait --for=condition=Ready pod/efimero --timeout=60s
kubectl exec efimero -- cat /cache/hola.txt

The last command fails: no such file. The new Pod started out with a fresh, empty emptyDir. For data that matters, something else is needed.

Step 2: The PersistentVolumeClaim

Kubernetes splits storage into three pieces. The PersistentVolume (PV) is the real disk; the StorageClass is the factory able to create PVs on demand; and the PersistentVolumeClaim (PVC) is your request: "I need this much space, with this access mode". You, as a user of the cluster, almost always touch only the third.

Look at the factory k3s ships out of the box:

kubectl get storageclass

That local-path (marked default) provisions directories on the node's disk. Now your request, pvc.yaml:

cat << 'EOF' > pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: datos-db-0
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: local-path
  resources:
    requests:
      storage: 100Mi
EOF

The YAML, explained in questions and answers

What does ReadWriteOnce mean?

That the volume can be mounted read-write by a single node at a time. It is the typical mode of block disks. There are ReadOnlyMany and ReadWriteMany (several nodes at once), but they require storage that supports it, such as NFS.

What happens when I declare storageClassName: local-path?

You turn on dynamic provisioning: as there is no free PV that fits, the StorageClass will create one to fit. Since it is the default class you could omit the field, but declaring it makes the manifest self-explanatory.

resources.requests, as in Pods?

The same idea applied to space: you ask for a minimum of 100Mi and the provisioner gives you a volume that meets it.

Apply it and observe something curious:

kubectl apply -f pvc.yaml
kubectl get pvc datos-db-0

The PVC stays in Pending, and that is normal: local-path uses the WaitForFirstConsumer policy, which delays creating the volume until it knows on which node the Pod that uses it will run. With no Pod, there is no decision to make.

Step 3: The Pod that writes

Create pod-db.yaml:

cat << 'EOF' > pod-db.yaml
apiVersion: v1
kind: Pod
metadata:
  name: db
spec:
  containers:
  - name: app
    image: ghcr.io/iximiuz/labs/nginx:alpine
    volumeMounts:
    - name: db
      mountPath: /data
  volumes:
  - name: db
    persistentVolumeClaim:
      claimName: datos-db-0
EOF

The YAML, in one question

What changes compared with the emptyDir Pod?

Only the source of the volume: where it used to say emptyDir: {} it now says persistentVolumeClaim with the name of your request. The volumes plus volumeMounts pattern is identical. That is the elegance of the design: the Pod does not know whether behind it there is a local directory, a cloud disk or an NFS.

Apply it and check the chain reaction:

kubectl apply -f pod-db.yaml
kubectl wait --for=condition=Ready pod/db --timeout=120s
kubectl get pvc,pv

The PVC went to Bound and a PV created by the StorageClass appeared. The complete chain: Pod, PVC, StorageClass, PV.

There is the WaitForFirstConsumer we were talking about, seen from the other side: the binding does not happen when the PVC is created, but when the Pod is scheduled. That is why the wait goes before the get; without it, what you would see would still be Pending.

Write the piece of data that will star in the final test (the message says "this data is persistent"):

kubectl exec db -- sh -c 'echo "este dato es persistente" > /data/mensaje.txt'

Step 4: The acid test

Repeat exactly what destroyed the data in step 1:

kubectl delete pod db
kubectl apply -f pod-db.yaml
kubectl wait --for=condition=Ready pod/db --timeout=120s
kubectl exec db -- cat /data/mensaje.txt

This time the message is still there. The Pod disappeared, the PVC and its PV did not: the volume sat waiting and the new Pod mounted it back.

Note

💡 And if you delete the PVC? It depends on the PV's reclaimPolicy: with Delete (the local-path default) the volume and its data disappear with the claim; with Retain the PV survives for manual inspection. Check it with kubectl get pv.

Summary

  • An emptyDir shares the Pod's life: perfect for cache, fatal for data.
  • The PVC is your storage request; the StorageClass manufactures the PV that satisfies it.
  • WaitForFirstConsumer explains PVCs in Pending with no Pod: it is not an error.
  • The same volumes + volumeMounts pattern works for ConfigMaps, emptyDir and PVCs: the Pod is agnostic to the source.
Previous lesson
Downward API