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

  1. Per què el sistema de fitxers d'un contenidor és efímer
  2. Demostració: perdre dades a postgres-reserves
  3. Què és un volum a Kubernetes
  4. La parella volumes i volumeMounts
  5. mountPath, readOnly, subPath i subPathExpr
  6. emptyDir en detall
  7. Compartir un emptyDir entre contenidors del mateix pod
  8. hostPath: què és i per què és perillós
  9. Els volums de configuració: configMap, secret, downwardAPI
  10. El volum projected: tots en un directori
  11. Taula resum de tipus de volum
  12. Volums efímers genèrics: el pont a la resta del mòdul

  1. 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.

  1. Demostració: perdre dades a postgres-reserves

Fes-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.txt
ERROR:  no existeix la relacio «reserves»

cat: /var/lib/postgresql/data/TESTIMONI.txt: No such file or directory
command terminated with exit code 1

S'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.

  1. 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:

  1. El volum es declara al pod, no al contenidor. Viu a spec.volumes del pod. Els contenidors només el munten en una ruta, mitjançant spec.containers[].volumeMounts. Per això dos contenidors del mateix pod poden veure el mateix volum en rutes diferents.
  2. 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/>&nbsp;&nbsp;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.

  1. La parella volumes i volumeMounts

Tot 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é un name ú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 (com configMap) 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. El name é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.

  1. mountPath, readOnly, subPath i subPathExpr

Els 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'arrel

En 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-abc123

Cada pod de worker-notificacions escriu els seus registres en un subdirectori amb el seu propi nom, i no es trepitgen entre ells.

  1. emptyDir en detall

emptyDir é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
kubectl exec mata un procés secundari
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:

minikube ssh -p rutas-norte
sudo ls /var/lib/kubelet/pods/*/volumes/kubernetes.io~empty-dir/

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:

  volumes:
    - name: adjunts
      emptyDir:
        sizeLimit: 500Mi     # el kubelet desallotja el pod si el supera

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-weblimits.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.

  1. Compartir un emptyDir entre contenidors del mateix pod

Aquest é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 mountPath diferents. El generador escriu a /sortida; nginx llegeix a /usr/share/nginx/html/horaris. És el mateix directori del node.
  • readOnly: true al 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: Memory amb sizeLimit: 64Mi, i el limits.memory de nginx elevat a 192Mi per absorbir-lo. Sense aquest ajust, un pic de plantilles mataria el contenidor amb OOMKilled.
  • 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

  1. hostPath: què és i per què és perillós

hostPath munta un fitxer o directori del sistema de fitxers del node dins del pod.

  volumes:
    - name: registres-del-node
      hostPath:
        path: /var/log
        type: Directory

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:

  • Un agent de recollida de logs que llegeix /var/log/pods de cada node (el patró DaemonSet de 06-02 i de la pila EFK de 07-05).
  • Un exportador de mètriques del node que llegeix /proc i /sys.
  • Un connector CSI que necessita accedir al directori de muntatge del kubelet.

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:

  1. Escalada a root al node. Un pod que munta / o /etc amb 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.
  2. 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 de postgres-reserves.
  3. 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.
  4. Dades que no viatgen. Encara que no hi hagués risc de seguretat, un hostPath està 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.

  1. Els volums de configuració: configMap, secret, downwardAPI

Ja 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 propietari

Dos 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 rollout quan canvia.
  • Un muntatge amb subPath queda congelat: rep el contingut del moment del muntatge i no s'actualitza mai. És la contrapartida de poder muntar un únic fitxer.

  1. El volum projected: tots en un directori

projected 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-reserves

Muntat 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.

  1. 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

  1. 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)
Classe d'emmagatzematge No aplica
Snapshots i expansió No
Sobreviu a delete pod No No

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:

  1. 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à.
  2. Inspecciona els volums efectius d'un pod en marxa, que no sempre són els que et pensaves, amb kubectl exec ... -- df -h i kubectl exec ... -- mount | grep -E "tmpfs|overlay".
  3. kubectl describe pod mostra la secció Volumes: i Mounts: de cada contenidor: és el lloc més ràpid per verificar que el muntatge és el que pretenies i si és ro o rw.
  4. kubectl explain pod.spec.volumes llista 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:

  1. Mata el procés principal del contenidor per forçar un reinici i comprova què sobreviu.
  2. Esborra el pod, recrea'l i comprova què sobreviu.
  3. Verifica amb mount quin dels dos està en tmpfs.
  4. Intenta escriure 50 MiB a /en-ram i 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 (imatge nginx:1.27.1) que el munta a /usr/share/nginx/html/horaris en només lectura.
  • generador-plantilles (fes servir busybox:1.36 amb un bucle que escrigui l'hora en un fitxer HTML cada 30 segons) que el munta a /sortida en 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/postgres

Solucions

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: 32Mi
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 -- 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.txt
tmpfs on /en-ram type tmpfs (rw,relatime,size=32768k)
/dev/vda1 on /persisteix-reinici type ext4 (rw,relatime)

disc
ram

Tots 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=50
/en-ram:
/persisteix-reinici:

dd: writing '/en-ram/gran': No space left on device
32+0 records out

Els 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

Mòdul 2: Components Principals de Kubernetes

Mòdul 3: Gestió de Configuració i Secrets

Mòdul 4: Xarxes a Kubernetes

Mòdul 5: Emmagatzematge a Kubernetes

Mòdul 6: Conceptes Avançats de Kubernetes

Mòdul 7: Monitoratge i Registre

Mòdul 8: Seguretat a Kubernetes

Mòdul 9: Escalat i Rendiment

Mòdul 10: Ecosistema i Eines de Kubernetes

Mòdul 11: Estudis de Cas i Aplicacions del Món Real

Mòdul 12: Preparació per a la Certificació de Kubernetes

© Copyright 2026. Tots els drets reservats