Almacenamiento persistente
Los contenedores tienen memoria de pez: todo lo que escriben en su sistema de archivos desaparece con ellos. Ya montaste un volumen en la lección anterior (aquel ConfigMap en /etc/config), pero era de solo lectura. Esta lección va de escribir datos y de la pregunta clave: ¿cuánto tiempo sobreviven?
Verás dos respuestas: el emptyDir, que vive lo que vive el Pod, y el PersistentVolumeClaim, que vive lo que tú digas.
El capítulo de almacenamiento del libro explica la cadena StorageClass → PersistentVolume → PersistentVolumeClaim de arriba abajo. Aquí la vas a recorrer en sentido contrario: pidiendo un volumen y viendo quién responde. Trabaja desde la pestaña dev-machine.
Paso 1: emptyDir, el volumen efímero
Crea pod-efimero.yaml:
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
El YAML, en dos preguntas
¿Qué es exactamente un emptyDir?
Un directorio vacío que Kubernetes crea en el nodo cuando el Pod arranca y destruye cuando el Pod desaparece. Sobrevive a reinicios del contenedor (caídas incluidas), pero no a la eliminación del Pod. Su uso natural: caché, archivos temporales y datos compartidos entre contenedores del mismo Pod.
¿Por qué {} como valor?
Es un objeto vacío: emptyDir no necesita configuración. Admite opciones como medium: Memory (montarlo en RAM) o sizeLimit, pero los valores por defecto bastan aquí.
Aplícalo, escribe un dato y ejecuta el experimento de la decepción:
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
El último comando falla: archivo inexistente. El Pod nuevo estrenó un emptyDir vacío. Para datos que importan, hace falta otra cosa.
Paso 2: El PersistentVolumeClaim
Kubernetes separa el almacenamiento en tres piezas. El PersistentVolume (PV) es el disco real; la StorageClass es la fábrica capaz de crear PVs bajo demanda; y el PersistentVolumeClaim (PVC) es tu solicitud: "necesito tanto espacio, con tal modo de acceso". Tú, como usuario del clúster, casi siempre tocas solo la tercera.
Mira la fábrica que k3s trae de serie:
kubectl get storageclass
Esa local-path (marcada default) aprovisiona directorios en el disco del nodo. Ahora tu solicitud, 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
El YAML, explicado con preguntas y respuestas
¿Qué significa ReadWriteOnce?
Que el volumen puede montarse en lectura y escritura por un solo nodo a la vez. Es el modo típico de los discos de bloque. Existen ReadOnlyMany y ReadWriteMany (varios nodos a la vez), pero requieren almacenamiento que lo soporte, como NFS.
¿Qué pasa al declarar storageClassName: local-path?
Activas el aprovisionamiento dinámico: al no existir un PV libre que encaje, la StorageClass creará uno a medida. Como es la clase default podrías omitir el campo, pero declararlo hace el manifiesto autoexplicativo.
¿resources.requests como en los Pods?
La misma idea aplicada a espacio: pides un mínimo de 100Mi y el aprovisionador te da un volumen que lo cumple.
Aplícalo y observa algo curioso:
kubectl apply -f pvc.yaml
kubectl get pvc datos-db-0
El PVC se queda en Pending, y es normal: local-path usa la política WaitForFirstConsumer, que retrasa la creación del volumen hasta saber en qué nodo correrá el Pod que lo use. Sin Pod, no hay decisión que tomar.
Paso 3: El Pod que escribe
Crea 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
El YAML, en una pregunta
¿Qué cambia respecto al Pod del emptyDir?
Solo la fuente del volumen: donde antes decía emptyDir: {} ahora dice persistentVolumeClaim con el nombre de tu solicitud. El patrón volumes más volumeMounts es idéntico. Esa es la elegancia del diseño: el Pod no sabe si detrás hay un directorio local, un disco de cloud o un NFS.
Aplícalo y comprueba la reacción en cadena:
kubectl apply -f pod-db.yaml
kubectl wait --for=condition=Ready pod/db --timeout=120s
kubectl get pvc,pv
El PVC pasó a Bound y apareció un PV creado por la StorageClass. La cadena completa: Pod, PVC, StorageClass, PV.
Ahí está el WaitForFirstConsumer del que hablábamos, visto desde el otro lado: el enlace no ocurre al crear el PVC, sino al programarse el Pod. Por eso el wait va antes del get; sin él lo que verías todavía sería Pending.
Escribe el dato que va a protagonizar la prueba final:
kubectl exec db -- sh -c 'echo "este dato es persistente" > /data/mensaje.txt'
Paso 4: La prueba de fuego
Repite exactamente lo que en el paso 1 destruyó el dato:
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
Esta vez el mensaje sigue ahí. El Pod desapareció, el PVC y su PV no: el volumen quedó a la espera y el Pod nuevo lo montó de vuelta.
💡 ¿Y si borras el PVC? Depende de la reclaimPolicy del PV: con Delete (el default de local-path) el volumen y sus datos desaparecen con el claim; con Retain el PV sobrevive para inspección manual. Compruébalo con kubectl get pv.
Resumen
- Un emptyDir comparte la vida del Pod: perfecto para caché, fatal para datos.
- El PVC es tu solicitud de almacenamiento; la StorageClass fabrica el PV que la satisface.
WaitForFirstConsumerexplica los PVC en Pending sin Pod: no es un error.- El mismo patrón
volumes+volumeMountssirve para ConfigMaps, emptyDir y PVCs: el Pod es agnóstico a la fuente.
- Previous lesson
- Downward API
- Next lesson
- Namespaces, RBAC y ServiceAccounts