Persistent storage
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.
💡 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.
WaitForFirstConsumerexplains PVCs in Pending with no Pod: it is not an error.- The same
volumes+volumeMountspattern works for ConfigMaps, emptyDir and PVCs: the Pod is agnostic to the source.
- Previous lesson
- Downward API
- Next lesson
- Namespaces, RBAC and ServiceAccounts