Tancàvem el mòdul 4 amb una plataforma accessible, xifrada i segmentada, i amb un deute escrit en un comentari del manifest des del mòdul 2: postgres-reserves no té emmagatzematge persistent. Aquest mòdul el salda, i comença pel principi, que no és el PersistentVolume sinó una cosa més bàsica: entendre per què el sistema de fitxers d'un contenidor desapareix i què és exactament un volum a Kubernetes. Veuràs que el volum és una propietat del pod, no del contenidor, i que aquesta distinció resol el reinici d'un contenidor però no el reemplaçament del pod —justament el problema que arrossega la base de dades—. En aquesta lliçó demostraràs la pèrdua de dades amb les teves mans, dominaràs la parella volumes/volumeMounts que regeix tots els tipus de volum de la resta del mòdul, i treballaràs a fons els dos tipus que no depenen de cap infraestructura externa: emptyDir i hostPath.
Contingut
- Per què el sistema de fitxers d'un contenidor és efímer
- Demostració: perdre dades a
postgres-reserves - Què és un volum a Kubernetes
- La parella
volumesivolumeMounts mountPath,readOnly,subPathisubPathExpremptyDiren detall- Compartir un
emptyDirentre contenidors del mateix pod hostPath: què és i per què és perillós- Els volums de configuració:
configMap,secret,downwardAPI - El volum
projected: tots en un directori - Taula resum de tipus de volum
- Volums efímers genèrics: el pont a la resta del mòdul
- Per què el sistema de fitxers d'un contenidor és efímer
Una imatge de contenidor està formada per capes de només lectura apilades. Quan el runtime (containerd al teu minikube) arrenca un contenidor a partir de postgres:16.4, no copia aquestes capes: les munta apilades mitjançant un sistema de fitxers d'unió (overlayfs) i hi afegeix al damunt una única capa d'escriptura, buida i exclusiva d'aquest contenidor.
flowchart TB
subgraph CONT["Contenidor en execucio"]
RW["Capa d'escriptura (rw)<br/>buida en arrencar<br/>ES DESTRUEIX en recrear el contenidor"]
end
subgraph IMG["Imatge postgres:16.4 (nomes lectura, compartida)"]
L3["Capa 3: configuracio de PostgreSQL"]
L2["Capa 2: binaris de PostgreSQL"]
L1["Capa 1: sistema base Debian"]
end
RW --> L3 --> L2 --> L1
Tot el que el procés escriu —un fitxer nou, la modificació d'un d'existent, els fitxers de dades de PostgreSQL a /var/lib/postgresql/data— va a parar a aquesta capa d'escriptura. I aquesta capa té una propietat demolidora: el seu cicle de vida és exactament el del contenidor. Quan el contenidor es destrueix, la capa s'esborra.
Convé distingir amb precisió dos successos que sonen semblants i no ho són:
| Succés | Què passa amb el contenidor | Què passa amb la capa d'escriptura |
|---|---|---|
El procés principal falla i restartPolicy: Always el reinicia |
Es crea un contenidor nou al mateix pod | Es perd (capa nova i buida) |
kubectl delete pod o un rollout restart |
Es crea un pod nou, amb contenidors nous | Es perd |
| El node cau i el pod es reprograma en un altre | Pod nou en un altre node | Es perd |
kubectl exec entra i surt |
Res, el contenidor segueix viu | Es conserva |
És a dir: qualsevol reinici, de la classe que sigui, esborra el que s'ha escrit. I això no és un defecte de Kubernetes; és la definició mateixa de contenidor i la raó que siguin lleugers i reemplaçables. El preu és que l'estat s'ha de treure fora del contenidor de manera explícita, i aquest "fora" és el volum.
Recordaràs de 03-04 que aquesta capa d'escriptura consumeix ephemeral-storage del node i que un contenidor que l'omple provoca el desallotjament del pod. Els volums també formen part d'aquesta comptabilitat, com veurem amb emptyDir.
- Demostració: perdre dades a
postgres-reserves
postgres-reservesFes-ho. És la pràctica que dona sentit al mòdul sencer. Partim de l'estat actual: postgres-reserves funcionant com a Deployment a rutas-norte-dev, sense cap volum. Creem una reserva real dins de la base de dades i, a més, un fitxer testimoni qualsevol:
POD=$(kubectl get pod -n rutas-norte-dev -l app=postgres-reserves \
-o jsonpath='{.items[0].metadata.name}')
# 1. Una taula i una reserva ficticia
kubectl exec -n rutas-norte-dev "$POD" -- psql -U rutasnorte -d reserves -c \
"CREATE TABLE IF NOT EXISTS reserves (
id serial PRIMARY KEY, client text, trajecte text, data date);
INSERT INTO reserves (client, trajecte, data)
VALUES ('Marta Iglesias', 'Bilbao-Santander', '2026-08-15');"
# 2. Un fitxer testimoni a la capa d'escriptura
kubectl exec -n rutas-norte-dev "$POD" -- \
sh -c 'echo "prova de persistencia" > /var/lib/postgresql/data/TESTIMONI.txt'Ara provoquem el que en producció provocaria un desplegament, un desallotjament o la caiguda d'un node:
kubectl delete pod -n rutas-norte-dev "$POD"
kubectl wait --for=condition=ready pod -n rutas-norte-dev \
-l app=postgres-reserves --timeout=120s
POD=$(kubectl get pod -n rutas-norte-dev -l app=postgres-reserves \
-o jsonpath='{.items[0].metadata.name}')
kubectl exec -n rutas-norte-dev "$POD" -- psql -U rutasnorte -d reserves -c \
"SELECT * FROM reserves;"
kubectl exec -n rutas-norte-dev "$POD" -- cat /var/lib/postgresql/data/TESTIMONI.txtERROR: no existeix la relacio «reserves»
cat: /var/lib/postgresql/data/TESTIMONI.txt: No such file or directory
command terminated with exit code 1S'ha perdut tot. El ReplicaSet ha fet la seva feina a la perfecció —hi ha un pod Running, el Service enruta, l'aplicació arrenca— i tanmateix el negoci ha perdut totes les reserves. Aquest és el punt exacte que separa una càrrega de treball sense estat d'una amb estat: per a botiga-web recrear el pod és gratis; per a postgres-reserves és una catàstrofe.
- Què és un volum a Kubernetes
Un volum és un directori, amb contingut i suport variables segons el seu tipus, que Kubernetes posa a disposició dels contenidors d'un pod.
La definició formal importa menys que aquestes dues afirmacions, que són les que cal interioritzar:
- El volum es declara al pod, no al contenidor. Viu a
spec.volumesdel pod. Els contenidors només el munten en una ruta, mitjançantspec.containers[].volumeMounts. Per això dos contenidors del mateix pod poden veure el mateix volum en rutes diferents. - El cicle de vida del volum està lligat al pod, no al contenidor. Un volum sobreviu al reinici d'un contenidor. El que passi quan el pod desapareix depèn del tipus de volum: uns moren amb ell (
emptyDir) i d'altres no (els suportats per un PersistentVolumeClaim).
flowchart TB
subgraph POD["Pod"]
M1["Contenidor A<br/>volumeMounts: dades -> /var/lib/dades"]
M2["Contenidor B<br/>volumeMounts: dades -> /entrada (readOnly)"]
V["volumes:<br/>- name: dades<br/> emptyDir: {}"]
end
M1 --> V
M2 --> V
Aquesta segona afirmació explica exactament fins on arriba aquesta lliçó. Un emptyDir arregla el cas "el contenidor de PostgreSQL ha caigut i el kubelet l'ha reiniciat": el contenidor nou torna a muntar el mateix directori i troba les seves dades. No arregla el cas "el pod ha estat reemplaçat", que és el que acabes de demostrar. Per a això calen els PersistentVolumes de 05-02.
- La parella
volumes i volumeMounts
volumes i volumeMountsTot volum requereix dues declaracions que s'enllacen pel nom. És el patró més repetit de Kubernetes i no canvia sigui quin sigui el tipus:
apiVersion: v1
kind: Pod
metadata:
name: demo-volum
namespace: rutas-norte-dev
labels: { app: demo-volum, app.kubernetes.io/part-of: rutas-norte, entorn: dev }
spec:
containers:
- name: escriptor
image: busybox:1.36
command: ["sh", "-c", "echo hola > /dades/salutacio.txt && sleep 3600"]
volumeMounts: # (2) ON el munta AQUEST contenidor
- name: espai-temporal # ha de coincidir amb volumes[].name
mountPath: /dades
volumes: # (1) QUIN volum te el POD
- name: espai-temporal
emptyDir: {}Els dos blocs, amb precisió:
spec.volumes(nivell de pod): una llista de volums. Cada element té unnameúnic dins del pod i exactament una clau de tipus (emptyDir,hostPath,configMap,secret,persistentVolumeClaim,projected,ephemeral...). Declarar un volum que cap contenidor no munta és legal però inútil; per a alguns tipus (comconfigMap) implica de totes maneres que el pod no arrencarà si l'objecte referenciat no existeix.spec.containers[].volumeMounts(nivell de contenidor): on es veu aquest volum dins d'aquest contenidor concret. Elnameés la referència creuada.
Si el name no casa, l'error és immediat i explícit en crear el pod:
The Pod "demo-volum" is invalid: spec.containers[0].volumeMounts[0].name:
Not found: "espai-temporl"Els initContainers també poden muntar volums, i és precisament el mecanisme que fa útil el patró d'inicialització que s'estudia a 06-04: un init container prepara fitxers en un volum i el contenidor principal se'ls troba ja llestos.
mountPath, readOnly, subPath i subPathExpr
mountPath, readOnly, subPath i subPathExprEls camps de volumeMounts mereixen detall perquè cadascun resol un problema real.
| Camp | Què fa | Compte |
|---|---|---|
mountPath |
Ruta absoluta dins del contenidor on apareix el volum | Amaga el contingut previ d'aquesta ruta a la imatge |
readOnly |
Munta el volum en només lectura | Per defecte false; posa'l a true sempre que puguis |
subPath |
Munta un subdirectori o un fitxer del volum, no la seva arrel | No rep actualitzacions de ConfigMap/Secret |
subPathExpr |
Igual que subPath però amb variables d'entorn expandides |
Excloent amb subPath |
mountPropagation |
Propagació de submuntatges cap al host o des d'ell | Només per a casos molt especials |
mountPath amaga el que hi havia
Aquest és l'efecte que més sorprèn al principi. Si muntes un volum a /etc/nginx/conf.d de la imatge de botiga-web, tot el que la imatge portava en aquest directori deixa de veure's, igual que en muntar un disc sobre un directori a Linux. No s'esborra: queda tapat. Per això muntar sobre /etc o /usr trenca el contenidor.
subPath: muntar un sol fitxer
Ja el vas fer servir a 03-01 per injectar un únic fitxer de configuració sense tapar el directori sencer. La mateixa tècnica serveix per als volums de dades:
volumeMounts:
- name: dades-postgres
mountPath: /var/lib/postgresql/data
subPath: pgdata # es munta <volum>/pgdata, no l'arrelEn el cas de PostgreSQL hi ha una raó operativa concreta per fer-ho, que s'explica en detall a 05-03: molts volums de bloc formatats porten un directori lost+found a la seva arrel, i PostgreSQL es nega a inicialitzar un directori de dades que no estigui buit. A Rutas Norte ho vam resoldre ja al mòdul 3 per la via equivalent de la variable PGDATA, i a 05-03 unificarem tots dos enfocaments.
subPathExpr: una ruta per pod
subPathExpr expandeix variables d'entorn del contenidor, incloses les de la Downward API de 03-03. El cas canònic és que cada rèplica escrigui al seu propi subdirectori d'un volum compartit:
- name: worker
image: registry.rutasnorte.example/worker-notificacions:1.8.0
env:
- name: NOM_POD
valueFrom:
fieldRef:
fieldPath: metadata.name
volumeMounts:
- name: registres
mountPath: /var/log/notificacions
subPathExpr: $(NOM_POD) # /registres/worker-notificacions-abc123Cada pod de worker-notificacions escriu els seus registres en un subdirectori amb el seu propi nom, i no es trepitgen entre ells.
emptyDir en detall
emptyDir en detallemptyDir és el tipus de volum més simple i el més utilitzat amb diferència.
Quan es crea: en el moment en què el pod s'assigna a un node. Comença sempre buit, d'aquí el nom.
Quan s'esborra: quan el pod s'elimina del node, de manera definitiva i irrecuperable. Que quedi molt clar què el sobreviu i què no:
| Succés | Sobreviu l'emptyDir? |
|---|---|
| Un contenidor del pod falla i es reinicia | Sí |
kubectl exec mata un procés secundari |
Sí |
El pod s'esborra (kubectl delete pod, rollout, desallotjament) |
No |
| El node es reinicia | No (el pod es recrea) |
On viu: per defecte, al disc del node, sota /var/lib/kubelet/pods/<uid>/volumes/kubernetes.io~empty-dir/. Ho pots veure a minikube:
sizeLimit
Sense límit, un emptyDir pot omplir el disc del node i provocar DiskPressure, amb el desallotjament de pods aliens que vas estudiar a 03-04. S'acota amb sizeLimit:
Quan se supera el límit, el kubelet desallotja el pod (no retorna un error d'escriptura al procés). L'esdeveniment ho diu sense embuts:
Warning Evicted pod/worker-notificacions-7c9d5-k4m2n
Usage of EmptyDir volume "adjunts" exceeds the limit "500Mi".medium: Memory
Amb medium: Memory, el volum es recolza en un tmpfs: RAM, no disc. És rapidíssim i el contingut no toca mai un disc, cosa que el fa idoni per a material sensible —és exactament el que Kubernetes fa per sota amb els volums de secret, com vas veure a 03-02—. Es declara amb emptyDir: { medium: Memory, sizeLimit: 64Mi }.
L'avís important: el que hi escriguis compta contra el limit de memòria del contenidor que l'utilitza. Si botiga-web té limits.memory: 256Mi i omples 200 MiB de tmpfs, al procés li queden 56 MiB i acabarà OOMKilled sense que cap gràfic de "memòria del procés" ho expliqui. Regla: si uses medium: Memory, suma'l al limit de memòria i posa sempre sizeLimit. Sense sizeLimit, el tmpfs pot créixer fins a la meitat de la RAM del node.
- Compartir un
emptyDir entre contenidors del mateix pod
emptyDir entre contenidors del mateix podAquest és el cas d'ús que justifica per si sol l'existència d'emptyDir, i encaixa amb el que vas aprendre a 02-01: els contenidors d'un pod comparteixen xarxa i poden compartir volums, però no el seu sistema de fitxers.
A Rutas Norte, botiga-web renderitza plantilles HTML d'horaris i trajectes. Hi afegim un contenidor secundari que les genera cada cinc minuts a partir de l'API i les deixa en un directori; nginx simplement les serveix. Tots dos veuen el mateix emptyDir, i nginx el munta en només lectura.
# k8s/base/botiga-web-deployment.yaml (fragment: el podTemplate)
spec:
containers:
# --- Contenidor principal: serveix l'HTML ---
- name: nginx
image: nginx:1.27.1
ports: [{ name: http, containerPort: 8080 }]
volumeMounts:
- name: cache-plantilles
mountPath: /usr/share/nginx/html/horaris
readOnly: true # nginx NO ha d'escriure aqui
resources:
requests: { cpu: 50m, memory: 128Mi }
limits: { cpu: 200m, memory: 192Mi } # 128Mi proces + 64Mi tmpfs
# --- Contenidor secundari: regenera les plantilles ---
- name: generador-plantilles
image: registry.rutasnorte.example/generador-plantilles:1.2.0
env:
- name: API_URL
value: "http://api-reserves:8080" # el Service del modul 2
- name: INTERVAL_SEGONS
value: "300"
volumeMounts:
- name: cache-plantilles
mountPath: /sortida # UNA ALTRA ruta, MATEIX volum
resources:
requests: { cpu: 25m, memory: 64Mi }
limits: { cpu: 100m, memory: 128Mi }
volumes:
- name: cache-plantilles
# RAM: es regenera cada 5 min, no cal disc
emptyDir: { medium: Memory, sizeLimit: 64Mi }Els punts de disseny que cal saber llegir en aquest manifest:
- Un sol volum, dos
mountPathdiferents. El generador escriu a/sortida; nginx llegeix a/usr/share/nginx/html/horaris. És el mateix directori del node. readOnly: trueal consumidor. Si demà una vulnerabilitat permet escriure a través de nginx, l'atacant no pot alterar les plantilles que serveix. Costa una línia.medium: MemoryambsizeLimit: 64Mi, i ellimits.memoryde nginx elevat a 192Mi per absorbir-lo. Sense aquest ajust, un pic de plantilles mataria el contenidor ambOOMKilled.- No és persistència. Si el pod mor, la memòria cau es perd i el generador la refà en l'arrencada següent. Aquest és exactament el criteri per triar
emptyDir: dades reconstruïbles.
Verificació, inclosa la prova que readOnly funciona:
kubectl exec -n rutas-norte-dev deploy/botiga-web -c generador-plantilles -- \
sh -c 'echo "<h1>Bilbao-Santander</h1>" > /sortida/prova.html'
kubectl exec -n rutas-norte-dev deploy/botiga-web -c nginx -- \
cat /usr/share/nginx/html/horaris/prova.html
kubectl exec -n rutas-norte-dev deploy/botiga-web -c nginx -- \
sh -c 'echo intrus > /usr/share/nginx/html/horaris/intrus.html'<h1>Bilbao-Santander</h1>
sh: can't create /usr/share/nginx/html/horaris/intrus.html: Read-only file system
command terminated with exit code 1
hostPath: què és i per què és perillós
hostPath: què és i per què és perillóshostPath munta un fitxer o directori del sistema de fitxers del node dins del pod.
El camp type valida què s'espera trobar en aquesta ruta, i és la diferència entre una fallada clara i un comportament silenciós i inesperat:
type |
Comportament |
|---|---|
| (buit) | Sense cap comprovació. Compatibilitat cap enrere; evita'l |
Directory |
Ha d'existir un directori; si no, el pod no arrenca |
DirectoryOrCreate |
Si no existeix, el kubelet el crea (permisos 0755, propietat del kubelet) |
File |
Ha d'existir un fitxer |
FileOrCreate |
Si no existeix, es crea buit |
Socket |
Ha d'existir un socket UNIX (cas típic: /var/run/docker.sock) |
CharDevice / BlockDevice |
Dispositiu de caràcters o de blocs |
Usos legítims, tots ells de components d'infraestructura, no d'aplicacions:
Per què és un risc greu en producció
hostPath trenca l'aïllament entre el pod i el node. Les conseqüències són de gravetat màxima:
- Escalada a root al node. Un pod que munta
/o/etcamb escriptura pot afegir una clau SSH a/root/.ssh/authorized_keys, o deixar un manifest a/etc/kubernetes/manifests/que el kubelet arrencarà com a pod estàtic amb tots els privilegis. Això és control total del node. - Accés als secrets dels altres pods. Sota
/var/lib/kubelet/pods/hi ha muntats els volums de tots els pods d'aquell node, inclosos els Secrets amb les credencials depostgres-reserves. - Fuita del runtime. Muntar
/var/run/containerd/containerd.sock(o el socket de Docker) permet crear contenidors privilegiats sense passar per l'API de Kubernetes ni per RBAC. - Dades que no viatgen. Encara que no hi hagués risc de seguretat, un
hostPathestà lligat a un node concret: si el pod es reprograma en un altre, es troba un directori diferent (o buit) i les dades "desapareixen" sense cap error.
Per això hostPath no és una solució de persistència per a postgres-reserves, encara que a primera vista ho sembli. I per això els Pod Security Standards en el seu nivell baseline prohibeixen hostPath completament; ho veuràs aplicat a 08-03, i l'enduriment del contenidor que l'acompanya a 08-02.
Regla de Rutas Norte: cap manifest d'aplicació no fa servir hostPath. Si n'apareix un en una revisió de codi, es rebutja sense discussió. La necessitat legítima —persistència real— es cobreix amb PVC.
- Els volums de configuració:
configMap, secret, downwardAPI
configMap, secret, downwardAPIJa has fet servir els tres al mòdul 3; els recol·loquem aquí perquè són volums, amb la mateixa mecànica volumes/volumeMounts, i convé veure'ls en la mateixa família.
| Tipus | Origen del contingut | Suport | S'actualitza sol |
|---|---|---|---|
configMap |
Claus d'un ConfigMap | Disc del node | Sí (llevat de subPath o immutable: true) |
secret |
Claus d'un Secret | tmpfs (RAM) | Sí (llevat de subPath o immutable: true) |
downwardAPI |
Camps i recursos del propi pod | Disc del node | Només labels i annotations |
volumes:
- name: configuracio
configMap:
name: api-reserves-config
defaultMode: 0444
items: # nomes aquestes claus, amb nom triat
- key: application.yaml
path: app.yaml
- name: credencials
secret:
secretName: postgres-reserves-credencials
defaultMode: 0400 # nomes lectura per al propietariDos recordatoris que sovint s'obliden:
- L'actualització automàtica del contingut no reinicia el procés. Per això a 03-03 vam afegir una anotació amb el hash de la configuració al pod, per forçar un
rolloutquan canvia. - Un muntatge amb
subPathqueda congelat: rep el contingut del moment del muntatge i no s'actualitza mai. És la contrapartida de poder muntar un únic fitxer.
- El volum
projected: tots en un directori
projected: tots en un directoriprojected combina diverses fonts —configMap, secret, downwardAPI i serviceAccountToken— en un únic directori. Serveix per no omplir el contenidor de punts de muntatge i, sobretot, per demanar tokens de ServiceAccount amb audiència i caducitat controlades, com vas veure a 03-06.
volumes:
- name: identitat-i-config
projected:
defaultMode: 0400
sources:
- secret:
name: postgres-reserves-credencials
items:
- key: password
path: db/password # -> /etc/rutasnorte/db/password
- configMap:
name: api-reserves-config
items:
- key: application.yaml
path: app/application.yaml
- serviceAccountToken:
path: token
expirationSeconds: 3600
audience: api-reservesMuntat a /etc/rutasnorte, el contenidor veu un sol arbre: app/application.yaml, db/password i token.
Restriccions per recordar: dins d'un projected no hi caben ni persistentVolumeClaim ni emptyDir (només les quatre fonts citades), i no es pot repetir el mateix path en dues fonts: el pod es rebutja per conflicte.
- Taula resum de tipus de volum
Els tipus que es fan servir avui en un clúster 1.30+, amb el seu cicle de vida i el seu cas d'ús. Els tipus "in-tree" de proveïdors concrets (awsElasticBlockStore, gcePersistentDisk, azureDisk...) estan eliminats o migrats a CSI; no els escriguis en manifests nous.
| Tipus | Cicle de vida | Suport | Cas d'ús típic a Rutas Norte |
|---|---|---|---|
emptyDir |
Mor amb el pod | Disc del node | Memòria cau de plantilles de botiga-web, fitxers temporals |
emptyDir + medium: Memory |
Mor amb el pod | RAM (tmpfs) | Dades sensibles temporals, memòria cau ràpida petita |
configMap |
Mor amb el pod | Disc del node | application.yaml d'api-reserves |
secret |
Mor amb el pod | RAM (tmpfs) | postgres-reserves-credencials |
downwardAPI |
Mor amb el pod | Disc del node | Nom del pod i entorn per als registres |
projected |
Mor amb el pod | Mixt | Token de ServiceAccount amb audiència |
hostPath |
Sobreviu al pod, lligat al node | Disc del node | Només infraestructura (agents, DaemonSets) |
local |
Sobreviu, lligat al node, via PV | Disc del node | Emmagatzematge local d'alt rendiment (05-02) |
persistentVolumeClaim |
Sobreviu al pod | El que proveeixi el PV | Les dades de postgres-reserves (05-03) |
ephemeral (genèric) |
Mor amb el pod | El que proveeixi la StorageClass | Espai de treball gran i llençable |
csi (en línia) |
Segons el driver | Driver CSI | Injecció de secrets externs (Vault, gestors del núvol) |
nfs |
Sobreviu al pod | Servidor NFS | Compartir fitxers entre diversos pods |
- Volums efímers genèrics: el pont a la resta del mòdul
Queda un tipus que fa de frontissa entre aquesta lliçó i les següents: el volum efímer genèric. És un volum que s'aprovisiona com un PVC —amb tota la potència de l'emmagatzematge real: la classe que vulguis, la mida que vulguis, expansió, snapshots— però el cicle de vida del qual és el del pod: en esborrar-se el pod, el PVC creat automàticament s'esborra amb ell.
volumes:
- name: espai-informes
ephemeral:
volumeClaimTemplate:
metadata:
labels: { app: informes-ocupacio, entorn: dev }
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: standard
resources: { requests: { storage: 20Gi } }És el que cal fer servir quan necessites molt espai temporal —més del que cap raonablement al disc del node— sense voler conservar-lo: la generació d'un informe voluminós, un processament per lots, una descàrrega gran. A Rutas Norte encaixarà amb informes-ocupacio quan arribi a 06-03.
La comparació tanca el mapa mental de la lliçó:
emptyDir |
ephemeral genèric |
persistentVolumeClaim |
|
|---|---|---|---|
| Cicle de vida | Pod | Pod | Independent del pod |
| Mida garantida | No (només topall) | Sí | Sí |
| Classe d'emmagatzematge | No aplica | Sí | Sí |
| Snapshots i expansió | No | Sí | Sí |
Sobreviu a delete pod |
No | No | Sí |
I amb això queda plantejat el que falta: per a postgres-reserves necessitem la tercera columna. Aquesta columna la construeixen les cinc lliçons restants del mòdul.
Errors Comuns i Consells
| Error | Símptoma | Solució |
|---|---|---|
Creure que emptyDir dona persistència |
Dades perdudes després d'un rollout |
emptyDir mor amb el pod; fes servir un PVC (05-03) |
name diferent a volumes i volumeMounts |
spec.containers[0].volumeMounts[0].name: Not found |
Revisa la referència creuada; són dos blocs enllaçats per nom |
| Muntar sobre un directori de la imatge | El contenidor arrenca però "falten fitxers" | mountPath amaga el que hi hagués; fes servir subPath per a un sol fitxer |
medium: Memory sense ajustar limits.memory |
OOMKilled inexplicable |
El tmpfs compta contra el límit del contenidor (03-04) |
emptyDir sense sizeLimit |
Node en DiskPressure, pods aliens desallotjats |
Posa sempre sizeLimit |
Esperar que un subPath de ConfigMap s'actualitzi |
La configuració canvia i el fitxer no | subPath congela el contingut; munta el directori o força un rollout |
Fer servir hostPath per a dades d'aplicació |
Dades "desaparegudes" en reprogramar el pod, i forat de seguretat | Prohibit en aplicacions; fes servir PVC |
hostPath amb type buit |
Es crea un directori inesperat o es munta el que no tocava | Especifica sempre type |
| Muntar en escriptura el que només es llegeix | Superfície d'atac innecessària | readOnly: true per defecte als consumidors |
Quatre consells que estalvien hores:
- Abans d'escriure un volum, respon: què passa si això es perd? Si la resposta és "es regenera",
emptyDir. Si és "perdem dades del negoci", PVC. No hi ha terme mitjà. - Inspecciona els volums efectius d'un pod en marxa, que no sempre són els que et pensaves, amb
kubectl exec ... -- df -hikubectl exec ... -- mount | grep -E "tmpfs|overlay". kubectl describe podmostra la seccióVolumes:iMounts:de cada contenidor: és el lloc més ràpid per verificar que el muntatge és el que pretenies i si ésroorw.kubectl explain pod.spec.volumesllista tots els tipus disponibles a la teva versió del clúster, que és la font de veritat davant de qualsevol documentació desactualitzada.
Exercicis
Exercici 1: demostrar i acotar la efimeritat
A rutas-norte-dev, crea un pod prova-efimer amb la imatge busybox:1.36 i dos volums: un emptyDir normal muntat a /persisteix-reinici i un altre emptyDir amb medium: Memory i sizeLimit: 32Mi muntat a /en-ram. Escriu un fitxer a cadascun. Després:
- Mata el procés principal del contenidor per forçar un reinici i comprova què sobreviu.
- Esborra el pod, recrea'l i comprova què sobreviu.
- Verifica amb
mountquin dels dos està entmpfs. - Intenta escriure 50 MiB a
/en-rami descriu què passa.
Exercici 2: memòria cau compartida per a botiga-web
Escriu k8s/base/botiga-web-cache.yaml: un Deployment de botiga-web a rutas-norte-dev amb dos contenidors que comparteixin un emptyDir anomenat cache-plantilles:
nginx(imatgenginx:1.27.1) que el munta a/usr/share/nginx/html/horarisen només lectura.generador-plantilles(fes servirbusybox:1.36amb un bucle que escrigui l'hora en un fitxer HTML cada 30 segons) que el munta a/sortidaen escriptura.
Etiquetes i resources segons les convencions del curs. Demostra que nginx veu el que escriu el generador i que no hi pot escriure ell.
Exercici 3: revisió de codi
Un company envia aquest manifest per donar persistència a postgres-reserves al clúster de preproducció. Troba quatre problemes diferents i explica la conseqüència de cadascun.
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres-reserves
namespace: rutas-norte-pre
spec:
replicas: 2
selector:
matchLabels: { app: postgres-reserves, entorn: pre }
template:
metadata:
labels: { app: postgres-reserves, entorn: pre }
spec:
containers:
- name: postgres
image: postgres:16.4
volumeMounts:
- name: dades
mountPath: /var/lib/postgresql
volumes:
- name: dades
hostPath:
path: /dades/postgresSolucions
Exercici 1
# prova-efimer.yaml
apiVersion: v1
kind: Pod
metadata:
name: prova-efimer
namespace: rutas-norte-dev
labels:
app: prova-efimer
app.kubernetes.io/part-of: rutas-norte
entorn: dev
spec:
containers:
- name: caixa
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
volumeMounts:
- name: en-disc
mountPath: /persisteix-reinici
- name: en-ram
mountPath: /en-ram
resources:
requests: { cpu: 10m, memory: 32Mi }
limits: { cpu: 50m, memory: 96Mi } # 64Mi proces + 32Mi de tmpfs
volumes:
- name: en-disc
emptyDir:
sizeLimit: 64Mi
- name: en-ram
emptyDir:
medium: Memory
sizeLimit: 32Mikubectl apply -f prova-efimer.yaml
kubectl wait --for=condition=ready pod/prova-efimer -n rutas-norte-dev --timeout=60s
kubectl exec -n rutas-norte-dev prova-efimer -- sh -c \
'echo disc > /persisteix-reinici/f.txt; echo ram > /en-ram/f.txt'
# 3. Que hi ha en tmpfs
kubectl exec -n rutas-norte-dev prova-efimer -- mount | grep -E "/en-ram|/persisteix"
# 1. Reinici del CONTENIDOR: el volum sobreviu
kubectl exec -n rutas-norte-dev prova-efimer -- kill 1
kubectl wait --for=condition=ready pod/prova-efimer -n rutas-norte-dev --timeout=60s
kubectl exec -n rutas-norte-dev prova-efimer -- cat /persisteix-reinici/f.txt /en-ram/f.txttmpfs on /en-ram type tmpfs (rw,relatime,size=32768k)
/dev/vda1 on /persisteix-reinici type ext4 (rw,relatime)
disc
ramTots dos sobreviuen: el volum està lligat al pod, no al contenidor. Fins i tot el de RAM, perquè el tmpfs pertany al pod.
# 2. Esborrat del POD: es perd tot
kubectl delete pod prova-efimer -n rutas-norte-dev
kubectl apply -f prova-efimer.yaml
kubectl wait --for=condition=ready pod/prova-efimer -n rutas-norte-dev --timeout=60s
kubectl exec -n rutas-norte-dev prova-efimer -- ls /persisteix-reinici /en-ram
# 4. Superar el sizeLimit del tmpfs
kubectl exec -n rutas-norte-dev prova-efimer -- \
dd if=/dev/zero of=/en-ram/gran bs=1M count=50Els dos directoris queden buits: és exactament el que li passa avui a postgres-reserves. I amb medium: Memory el sizeLimit és la mida real del tmpfs, així que la fallada és immediata i dins del contenidor. En un emptyDir de disc el comportament és diferent: l'escriptura té èxit i és el kubelet qui, en detectar l'excés, desallotja el pod amb l'esdeveniment Usage of EmptyDir volume ... exceeds the limit. Comprova-ho amb kubectl get events -n rutas-norte-dev --sort-by=.lastTimestamp.
Exercici 2
# k8s/base/botiga-web-cache.yaml (fragment: el podTemplate)
spec:
containers:
- name: nginx
image: nginx:1.27.1
ports: [{ name: http, containerPort: 80 }]
volumeMounts:
- name: cache-plantilles
mountPath: /usr/share/nginx/html/horaris
readOnly: true # el consumidor NO escriu
resources:
requests: { cpu: 50m, memory: 96Mi }
limits: { cpu: 200m, memory: 160Mi } # 128Mi + 32Mi de tmpfs
- name: generador-plantilles
image: busybox:1.36
command: ["sh", "-c", "while true; do
echo \"<h1>Horaris</h1><p>$(date)</p>\" > /sortida/horaris.html;
sleep 30; done"]
volumeMounts:
- name: cache-plantilles
mountPath: /sortida # UNA ALTRA ruta, MATEIX volum
resources:
requests: { cpu: 10m, memory: 32Mi }
limits: { cpu: 50m, memory: 64Mi }
volumes:
- name: cache-plantilles
emptyDir: { medium: Memory, sizeLimit: 32Mi }kubectl apply -f k8s/base/botiga-web-cache.yaml
kubectl rollout status deploy/botiga-web -n rutas-norte-dev
# nginx veu el que escriu el generador, i ho serveix per HTTP
kubectl exec -n rutas-norte-dev deploy/botiga-web -c nginx -- \
curl -s localhost/horaris/horaris.html
# pero no hi pot escriure
kubectl exec -n rutas-norte-dev deploy/botiga-web -c nginx -- \
sh -c 'touch /usr/share/nginx/html/horaris/x' || echo "DENEGAT (correcte)"<h1>Horaris Rutas Norte</h1><p>Wed Aug 5 09:14:22 UTC 2026</p>
touch: cannot touch '...': Read-only file system
DENEGAT (correcte)Exercici 3
Els quatre problemes:
| # | Problema | Conseqüència |
|---|---|---|
| 1 | hostPath per a dades d'aplicació |
Les dades queden lligades a un node: si el pod es reprograma, la base de dades apareix buida sense cap error. A més és un forat de seguretat i el prohibeix el nivell baseline dels Pod Security Standards (08-03) |
| 2 | replicas: 2 |
Dos processos de PostgreSQL escrivint sobre el mateix directori: corrupció del clúster de dades. PostgreSQL protegeix amb un fitxer de bloqueig, però només dins del mateix node; en nodes diferents amb un emmagatzematge compartit, la corrupció és real. Una base de dades així va a replicas: 1 i a strategy: Recreate (o a un StatefulSet, 06-01) |
| 3 | mountPath: /var/lib/postgresql en lloc de /var/lib/postgresql/data |
Es munta un nivell de més i s'amaga el contingut que la imatge porta en aquest directori (el home de l'usuari postgres, amb la seva configuració). El símptoma és una arrencada que falla o un initdb que es refà |
| 4 | Falten resources, strategy: Recreate, l'etiqueta app.kubernetes.io/part-of i el type del hostPath |
Sense resources el pod és Burstable o BestEffort i pot ser desallotjat, quan la decisió de 03-05 és que sigui Guaranteed; amb strategy per defecte (RollingUpdate) el pod nou conviu amb el vell sobre les mateixes dades; i sense type el hostPath no valida res |
La correcció no és retocar aquest manifest sinó canviar de mecanisme: un PersistentVolumeClaim, que és el que construeixen les lliçons següents.
Conclusió
Has demostrat amb les teves mans el problema que dona nom al mòdul: vas crear una reserva a postgres-reserves, vas esborrar el pod i la reserva va desaparèixer. La causa no és una fallada: és la capa d'escriptura d'overlayfs, que neix buida amb cada contenidor i mor amb ell. D'aquí surt la regla que governa tota la resta: a Kubernetes l'estat s'ha de treure del contenidor de manera explícita.
Aquest "fora" és el volum, i ara el saps llegir amb precisió. Es declara al pod (spec.volumes), no al contenidor; els contenidors només el munten (volumeMounts), i tots dos blocs s'enllacen pel name. Domines els camps del muntatge: mountPath, que amaga el que hi hagués en aquesta ruta; readOnly, que hauries de posar per defecte a tot consumidor; subPath, per muntar un únic fitxer al preu de congelar-ne el contingut; i subPathExpr, per donar a cada rèplica el seu propi subdirectori.
Coneixes a fons els dos tipus que no depenen d'infraestructura externa. emptyDir neix buit en assignar-se el pod a un node i mor amb el pod: sobreviu al reinici d'un contenidor però no al seu reemplaçament; s'acota amb sizeLimit per no provocar DiskPressure, i amb medium: Memory viu a la RAM —ràpid i sense tocar disc, però comptant contra el limit de memòria del contenidor—. L'has posat a treballar en el cas que li correspon: la memòria cau de plantilles que comparteixen nginx i el generador dins del pod de botiga-web, amb el consumidor en només lectura i dades reconstruïbles. I hostPath, amb els seus type i amb el seu veredicte clar: legítim només per a infraestructura, prohibit en aplicacions, perquè trenca l'aïllament amb el node —escalada a root, accés als secrets dels altres pods, fuita pel socket del runtime— i perquè lliga les dades a una màquina concreta. Has recol·locat a més els volums de configuració que ja feies servir —configMap, secret, downwardAPI— i el projected que els reuneix en un sol directori, i tens la taula de tots els tipus amb el seu cicle de vida.
El que no has resolt és precisament el que obria la lliçó: cap volum dels que hem vist no sobreviu al reemplaçament del pod. La frontissa la vam deixar apuntada amb el volum efímer genèric (ephemeral.volumeClaimTemplate), que ja s'aprovisiona com a emmagatzematge de veritat encara que continuï morint amb el pod. Falta tallar l'últim fil: desacoblar la vida del disc de la vida del pod. Això exigeix un objecte nou, amb existència pròpia al clúster, administrat per qui gestiona la infraestructura i consumit per qui desplega l'aplicació. És el PersistentVolume, i és la lliçó següent: Volums Persistents.
Curs de Kubernetes
Mòdul 1: Introducció a Kubernetes
- Què és Kubernetes?
- Arquitectura de Kubernetes
- Conceptes i Terminologia Clau
- Configuració d'un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objectes, Manifests YAML i el Model Declaratiu
- El Projecte del Curs: la Plataforma Rutas Norte
Mòdul 2: Components Principals de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualitzacions, Rollbacks i Estratègies de Desplegament
- Serveis
- Namespaces
- Etiquetes, Selectors i Anotacions
Mòdul 3: Gestió de Configuració i Secrets
- ConfigMaps
- Secrets
- Variables d'Entorn
- Quotes i Límits de Recursos
- LimitRanges i Classes de Qualitat de Servei (QoS)
- ServiceAccounts i Accés a l'API des dels Pods
Mòdul 4: Xarxes a Kubernetes
- Xarxes de Clúster
- Tipus de Serveis
- DNS Intern i Descobriment de Serveis
- Controladors d'Ingress
- TLS i Gestió de Certificats amb cert-manager
- Polítiques de Xarxa
Mòdul 5: Emmagatzematge a Kubernetes
- Volums
- Volums Persistents
- Reclamacions de Volums Persistents
- Classes d'Emmagatzematge
- Aprovisionament Dinàmic, Expansió i Snapshots
- Còpies de Seguretat i Restauració de Dades
Mòdul 6: Conceptes Avançats de Kubernetes
- StatefulSets
- DaemonSets
- Treballs i CronJobs
- Init Containers, Sidecars i Patrons Multicontenidor
- Planificació: Afinitat, Taints i Toleracions
- Definicions de Recursos Personalitzats (CRDs)
- Operadors i el Patró Controlador
Mòdul 7: Monitoratge i Registre
- Verificacions de Salut i Sondes
- Servidor de Mètriques i kubectl top
- Monitoratge amb Prometheus
- Visualització i Alertes amb Grafana i Alertmanager
- Registre Centralitzat amb Elasticsearch, Fluentd i Kibana (EFK)
- Depuració d'Aplicacions i Esdeveniments del Clúster
Mòdul 8: Seguretat a Kubernetes
- Control d'Accés Basat en Rols (RBAC)
- Contextos de Seguretat i Enduriment del Contenidor
- Polítiques de Seguretat de Pods i Pod Security Standards
- Seguretat de Xarxa
- Seguretat d'Imatges
- Auditoria, Escaneig i Gestió de Vulnerabilitats
Mòdul 9: Escalat i Rendiment
- Autoescalat Horitzontal de Pods
- Autoescalat Vertical de Pods
- Autoescalat de Clúster
- Escalat per Esdeveniments i Mètriques Personalitzades amb KEDA
- Alta Disponibilitat: PodDisruptionBudgets i Topologia
- Ajust de Rendiment
Mòdul 10: Ecosistema i Eines de Kubernetes
- Minikube i Entorns Locals amb kind
- Kubeadm
- Helm
- Kustomize
- GitOps amb Argo CD i Flux
- Kubernetes Gestionat: EKS, AKS i GKE
Mòdul 11: Estudis de Cas i Aplicacions del Món Real
- Desplegament d'una Aplicació Web
- Execució d'Aplicacions amb Estat
- CI/CD amb Kubernetes
- Estratègies de Desplegament: Blue-Green i Canary
- Gestió Multi-Clúster
- Operació en Producció: Incidències, Runbooks i Costos
