Tancàvem el mòdul 1 amb botiga-web funcionant a rutas-norte-dev i amb una lliçó incòmoda apresa: en esborrar aquell pod solt, no va tornar mai. Abans d'arreglar-ho amb controladors, cal entendre bé la peça sobre la qual es construeix tota la resta. El Pod és la unitat mínima de desplegament de Kubernetes: no desplegues contenidors, desplegues pods que contenen contenidors. Tota la maquinària del curs —ReplicaSets, Deployments, Serveis, sondes, autoescalat, polítiques de xarxa— existeix per crear, mantenir, assolir, vigilar o protegir pods. En aquesta lliçó obrim el pod en canal: per què és la unitat mínima i no el contenidor, què comparteixen exactament els contenidors que hi viuen dins, com es llegeix un manifest camp per camp, què significa cada fase i cada estat que veuràs a kubectl get pods, com neix i com mor un pod, i com fer servir pods efímers com a eina de diagnòstic. En acabar, sabràs interpretar un ImagePullBackOff o un CrashLoopBackOff sense buscar per internet.

Contingut

  1. Per què la unitat mínima és el pod i no el contenidor
  2. Què comparteixen els contenidors d'un mateix pod (i què no)
  3. El contenidor de pausa: el que sosté el pod
  4. Anatomia d'un manifest de pod: api-reserves camp per camp
  5. Fases del pod i estats dels contenidors
  6. Motius típics: ContainerCreating, ImagePullBackOff, CrashLoopBackOff
  7. restartPolicy: qui reinicia què
  8. Cicle de vida complet: de la creació al SIGTERM
  9. Pods efímers per depurar
  10. Per què en producció mai no es gestionen pods solts

  1. Per què la unitat mínima és el pod i no el contenidor

És la primera pregunta que es fa tothom qui ve de Docker. Si Kubernetes orquestra contenidors, per què introdueix un embolcall intermedi en lloc de planificar contenidors directament?

La resposta és que hi ha processos que necessiten anar enganxats. No "a prop" ni "a la mateixa màquina", sinó literalment compartint xarxa i disc, arrencant i morint alhora, planificats com una sola unitat indivisible. Exemples que veuràs en qualsevol plataforma seriosa:

  • Un servidor d'aplicació i un recol·lector de logs que llegeix els fitxers que aquell escriu.
  • Un servei i un proxy que li acaba el TLS i li afegeix mètriques (el patró service mesh).
  • Un contenidor principal i un altre que descarrega configuració o certificats abans que arrenqui.

Si la unitat mínima fos el contenidor, el planificador podria col·locar el recol·lector de logs en un node i l'aplicació en un altre, i el disseny es trencaria. Kubernetes ho resol amb un contracte clar:

El pod és un grup d'un o més contenidors que es planifiquen junts al mateix node, comparteixen xarxa i emmagatzematge, i viuen i moren com una unitat.

Dues conseqüències pràctiques que convé gravar des d'ara:

  • El pod és la unitat de planificació. El scheduler assigna pods a nodes, no contenidors. Els recursos que se sumen per decidir si "hi cap" en un node són els de tots els seus contenidors.
  • El pod és la unitat d'escalat. Quan escales api-reserves a 6 rèpliques, crees 6 pods sencers. No pots escalar un contenidor dins d'un pod.

Aquesta segona conseqüència és la regla de disseny més important: si dos processos escalen a ritmes diferents, no van al mateix pod. A Rutas Norte, api-reserves i redis-cache podrien semblar bons companys de pod (l'API consulta la memòria cau constantment), però seria un error greu: cada rèplica de l'API tindria la seva pròpia cau aïllada, i escalar l'API multiplicaria les caus. Van en pods separats, i es parlen a través d'un Servei.

El cas aclaparadorament majoritari, i el que farem servir gairebé sempre al curs, és un contenidor per pod. Els patrons multicontenidor (init containers i sidecars) tenen la seva pròpia lliçó: Init Containers, Sidecars i Patrons Multicontenidor.

  1. Què comparteixen els contenidors d'un mateix pod (i què no)

Un pod no és una metàfora: és una construcció molt concreta de namespaces de Linux compartits entre processos. Aquesta taula és la referència exacta.

Recurs Es comparteix dins del pod? Què implica a la pràctica
Namespace de xarxa Sí Tots els contenidors tenen la mateixa IP i el mateix espai de ports; es veuen entre ells per localhost
Namespace IPC Sí Poden fer servir memòria compartida i semàfors de System V entre ells
Volums (spec.volumes) Sí, els que cadascun munti Un contenidor escriu un fitxer i un altre el llegeix, si tots dos munten el mateix volum
Cicle de vida i node Sí Es planifiquen al mateix node; el pod s'esborra sencer, mai a mitges
Namespace d'UTS (hostname) Sí Mateix hostname, que per defecte és el nom del pod
Sistema de fitxers arrel No Cada contenidor té el seu, el de la seva imatge. /app d'un no és /app de l'altre
Namespace de PID No per defecte Cada contenidor veu només els seus processos, tret que activis shareProcessNamespace: true
Recursos (CPU, memòria) No requests i limits es declaren per contenidor, no per pod
Estat de reinici No El kubelet reinicia contenidors individualment; no reinicia el pod sencer
flowchart TB
    subgraph POD["Pod api-reserves · IP 10.244.0.34"]
        direction TB
        NET["Namespace de xarxa compartit<br/>una sola IP, un sol espai de ports"]
        subgraph C1["Contenidor: api"]
            FS1["Sistema de fitxers propi<br/>imatge node:20-alpine"]
        end
        subgraph C2["Contenidor: recollector-logs"]
            FS2["Sistema de fitxers propi<br/>imatge fluent-bit"]
        end
        VOL[("Volum compartit<br/>/var/log/app")]
    end
    C1 -.->|escriu| VOL
    C2 -.->|llegeix| VOL
    C1 --- NET
    C2 --- NET

La conseqüència més útil i la més perillosa del namespace de xarxa compartit:

  • Útil: dins d'un pod, dos contenidors es parlen per http://localhost:8080. Sense DNS, sense Service, sense latència de xarxa.
  • Perillosa: no hi pot haver dos contenidors escoltant al mateix port dins d'un pod. Si poses dos nginx al pod, el segon fallarà amb "address already in use".

  1. El contenidor de pausa: el que sosté el pod

Com aconsegueix Kubernetes que diversos contenidors comparteixin la xarxa si cadascun arrenca i mor pel seu compte? Amb un truc elegant: per cada pod, el kubelet arrenca un contenidor invisible anomenat contenidor de pausa (pause, també anomenat contenidor d'infraestructura o sandbox).

El seu comportament:

  1. Es crea el primer, abans que cap contenidor teu.
  2. Crea i posseeix els namespaces de xarxa, IPC i UTS del pod. És qui rep la IP.
  3. La resta de contenidors del pod s'arrenquen unint-se als namespaces del contenidor de pausa.
  4. La seva única feina és dormir. El seu codi font ocupa unes poques desenes de línies: es queda bloquejat esperant senyals i fa reap dels processos orfes.

El seu valor rau en el fet que sobreviu als reinicis dels teus contenidors. Si el procés d'api-reserves cau i el kubelet el reinicia, el contenidor de pausa segueix viu, així que la IP del pod no canvia. Només quan es destrueix el pod sencer desapareix el sandbox i s'allibera la IP. Això explica exactament el que vas observar a l'exercici final del mòdul 1: en matar el procés, RESTARTS pujava a 1 però la IP continuava sent la mateixa.

El pots veure al node, tot i que no apareix mai a kubectl get pods:

minikube ssh --profile=rutas-norte -- "sudo crictl pods | head -5"
POD ID              CREATED             STATE     NAME              NAMESPACE
7b2c9d1f4a8e3       12 minutes ago      Ready     botiga-web        rutas-norte-dev

No l'has de gestionar mai. Però conèixer-lo evita confusions quan llegeixis documentació o depuris a baix nivell.

  1. Anatomia d'un manifest de pod: api-reserves camp per camp

Escriurem el pod d'api-reserves, el segon component de Rutas Norte. Com que registry.rutasnorte.example és fictici, simulem l'API amb una imatge pública de Node.js que aixeca un servidor mínim. Desa'l al repositori del projecte.

# k8s/base/api-reserves-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: api-reserves
  namespace: rutas-norte-dev
  labels:
    app: api-reserves
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
  annotations:
    rutasnorte.example/responsable: [email protected]
spec:
  restartPolicy: Always
  terminationGracePeriodSeconds: 30
  containers:
    - name: api
      image: node:20-alpine
      command: ["node", "-e"]
      args:
        - |
          const http = require('http');
          http.createServer((req, res) => {
            res.writeHead(200, {'Content-Type': 'application/json'});
            res.end(JSON.stringify({servei: 'api-reserves', ruta: req.url}));
          }).listen(3000, () => console.log('api-reserves escoltant al 3000'));
      ports:
        - name: http
          containerPort: 3000
          protocol: TCP
      env:
        - name: ENTORN
          value: "dev"
        - name: NODE
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName
      resources:
        requests:
          cpu: "100m"
          memory: "128Mi"
        limits:
          cpu: "500m"
          memory: "256Mi"
      volumeMounts:
        - name: temporal
          mountPath: /tmp/reserves
  volumes:
    - name: temporal
      emptyDir: {}

Repàs camp per camp:

  • apiVersion: v1 / kind: Pod: el Pod pertany al grup core de l'API, per això no porta prefix de grup. Ho vam veure a Objectes, Manifests YAML i el Model Declaratiu.
  • metadata.name: únic dins del namespace. Serà també el hostname del pod.
  • metadata.labels: les tres etiquetes obligatòries del projecte. Encara no les fa servir ningú, però a la lliçó següent seran la clau amb què un ReplicaSet reconeix els "seus" pods. La seva sintaxi completa és a Etiquetes, Selectors i Anotacions.
  • spec.restartPolicy: Always: és el valor per defecte; l'escrivim per fer-lo explícit. Apartat 7.
  • spec.terminationGracePeriodSeconds: 30: segons que es concedeixen al contenidor per acabar ordenadament abans de matar-lo per la força. També és el valor per defecte. Apartat 8.
  • containers[].name: identificador del contenidor dins del pod. És el que passes a kubectl logs -c o kubectl exec -c quan n'hi ha diversos.
  • image: etiqueta explícita, mai latest, segons la convenció del projecte.
  • command / args: sobreescriuen l'ENTRYPOINT i el CMD de la imatge respectivament. Aquí els fem servir perquè una imatge genèrica de Node es comporti com la nostra API. Compte amb l'equivalència, que és una font clàssica d'errors:
Docker Kubernetes
ENTRYPOINT command
CMD args
  • ports: informatiu, no obre res. Serveix per documentar i, sobretot, per donar un nom al port (http), que després un Service podrà referenciar pel nom en lloc de pel número.
  • env: variables d'entorn. La primera és un valor literal; la segona fa servir fieldRef per injectar una dada del propi pod (la downward API). Les fonts serioses de configuració —ConfigMaps i Secrets— arriben a Variables d'Entorn.
  • resources: requests és el que el scheduler reserva; limits és el sostre que imposa el kubelet. Detall complet a Quotes i Límits de Recursos.
  • volumeMounts / volumes: el volum emptyDir és un directori buit que viu i mor amb el pod. Útil per a fitxers temporals i per compartir dades entre contenidors del mateix pod. L'emmagatzematge persistent és el mòdul 5.

Aplica'l i comprova'l:

kubectl apply -f k8s/base/api-reserves-pod.yaml
kubectl get pods
pod/api-reserves created

NAME           READY   STATUS    RESTARTS   AGE
api-reserves   1/1     Running   0          6s
botiga-web     1/1     Running   0          22m
kubectl exec api-reserves -- wget -qO- http://localhost:3000/disponibilitat
{"servei":"api-reserves","ruta":"/disponibilitat"}

Fixa't que hem cridat localhost:3000 des de dins del propi pod: això és el namespace de xarxa compartit en acció.

  1. Fases del pod i estats dels contenidors

Aquí hi ha dos conceptes que es confonen constantment i que convé separar amb nitidesa.

5.1. La fase del pod (status.phase)

És un resum d'alt nivell del pod sencer. Només té cinc valors possibles:

Fase Significat Situació típica
Pending Acceptat per l'API, però algun contenidor encara no està en marxa Esperant el scheduler, o descarregant imatges
Running Assignat a un node, tots els contenidors creats i almenys un en execució Estat normal d'una càrrega de servei
Succeeded Tots els contenidors han acabat amb èxit i no es reiniciaran Un Job o un pod amb restartPolicy: Never que ha acabat bé
Failed Tots han acabat i almenys un ha fallat (codi de sortida diferent de 0) Un procés que ha avortat amb restartPolicy: Never
Unknown No es pot determinar l'estat, normalment perquè no hi ha contacte amb el node Node caigut o partició de xarxa

Un detall important: Running no vol dir "funcionant bé". Vol dir "hi ha processos vius". Un pod pot estar Running i retornar errors 500 a tothom. Distingir "viu" de "sa" és exactament la feina de les sondes, a Verificacions de Salut i Sondes.

5.2. L'estat de cada contenidor (status.containerStatuses[].state)

Cada contenidor té el seu propi estat, amb només tres valors, cadascun amb un motiu (reason) que és el que de debò t'explica què passa:

Estat Significat Motius habituals
Waiting Encara no s'executa; està fent alguna cosa prèvia o esperant ContainerCreating, ImagePullBackOff, ErrImagePull, CrashLoopBackOff, CreateContainerConfigError
Running S'està executant; inclou startedAt —
Terminated Ha acabat; inclou exitCode, reason, startedAt i finishedAt Completed (codi 0), Error, OOMKilled

La columna STATUS de kubectl get pods és un invent de la CLI que barreja les dues coses per ser útil: mostra la fase, tret que hi hagi un motiu de contenidor més informatiu, cas en què mostra aquest. Per això veus ImagePullBackOff en aquella columna encara que no sigui una fase.

Per veure les dades reals, sense la barreja:

kubectl get pod api-reserves -o jsonpath='{.status.phase}{"\n"}'
kubectl get pod api-reserves -o jsonpath='{range .status.containerStatuses[*]}{.name}{" -> "}{.state}{"\n"}{end}'
Running
api -> map[running:map[startedAt:2026-08-05T10:41:07Z]]

  1. Motius típics: ContainerCreating, ImagePullBackOff, CrashLoopBackOff

Aquests tres motius cobreixen la gran majoria dels problemes que veuràs. Mereixen tractament individual perquè el diagnòstic de cadascun és diferent.

6.1. ContainerCreating

Estat transitori i normal: el kubelet està descarregant la imatge, muntant volums i configurant la xarxa del pod. Només és un problema si no en passa d'aquí. Si s'hi queda minuts, kubectl describe et dirà per què: gairebé sempre un volum que no es pot muntar o un Secret/ConfigMap que no existeix.

6.2. ImagePullBackOff i ErrImagePull

El kubelet no pot descarregar la imatge. Primer veuràs ErrImagePull (fallada immediata) i després ImagePullBackOff (reintents amb espera creixent). Provoquem-ho a propòsit, fent servir el registre fictici del projecte:

kubectl run prova-registre --image=registry.rutasnorte.example/api-reserves:2.4.0
kubectl get pod prova-registre
pod/prova-registre created

NAME             READY   STATUS             RESTARTS   AGE
prova-registre   0/1     ImagePullBackOff   0          35s

El diagnòstic és sempre als esdeveniments:

kubectl describe pod prova-registre | tail -8
Events:
  Type     Reason     Age                From               Message
  ----     ------     ----               ----               -------
  Normal   Scheduled  40s                default-scheduler  Successfully assigned rutas-norte-dev/prova-registre to rutas-norte
  Normal   Pulling    39s                kubelet            Pulling image "registry.rutasnorte.example/api-reserves:2.4.0"
  Warning  Failed     24s                kubelet            Failed to pull image: dial tcp: lookup registry.rutasnorte.example: no such host
  Warning  Failed     24s                kubelet            Error: ErrImagePull
  Normal   BackOff    10s (x2 over 23s)  kubelet            Back-off pulling image

Les quatre causes possibles, en ordre de freqüència:

  1. Nom o etiqueta mal escrits (la més comuna amb diferència).
  2. Registre privat sense credencials: falta un imagePullSecrets. Es veu al mòdul 8.
  3. Registre inabastable des del node, com en aquest exemple.
  4. L'etiqueta no existeix al registre: típic en desplegar una versió que encara no s'ha publicat.

Neteja la prova:

kubectl delete pod prova-registre

6.3. CrashLoopBackOff

El més malinterpretat. No és un error en si mateix: vol dir que el contenidor arrenca, acaba, el kubelet el reinicia, i torna a acabar. Per no consumir el node, el kubelet espera cada cop més entre reintents: 10s, 20s, 40s, 80s… fins a un màxim de 5 minuts. CrashLoopBackOff és el nom d'aquella espera.

kubectl run api-trencada --image=node:20-alpine -- node -e "console.error('falta DATABASE_URL'); process.exit(1)"
kubectl get pod api-trencada -w
NAME           READY   STATUS             RESTARTS      AGE
api-trencada   0/1     ContainerCreating  0             0s
api-trencada   0/1     Error              0             3s
api-trencada   0/1     CrashLoopBackOff   1 (5s ago)    8s
api-trencada   0/1     Error              2 (18s ago)   21s
api-trencada   0/1     CrashLoopBackOff   2 (14s ago)   35s

La clau del diagnòstic: el motiu no és mai a describe, és als logs de l'intent anterior.

kubectl logs api-trencada --previous
falta DATABASE_URL

Aquí hi ha la causa real. Recorda el flag --previous (o -p): sense ell, kubectl logs intenta llegir el contenidor actual, que pot estar encara en l'espera i no tenir res a explicar.

Les causes més freqüents de CrashLoopBackOff:

Causa Com es detecta
Error de configuració (falta una variable, credencials invàlides) kubectl logs --previous mostra el missatge
El procés acaba tan bon punt arrenca perquè no és un servidor exitCode: 0 i tot i així reinicia: restartPolicy: Always mal triada
Manca de memòria reason: OOMKilled a describe; puja el limits.memory
Un command mal escrit exitCode: 127 (ordre no trobada) o 126 (no executable)
Sonda de vitalitat mal configurada Reinicis cíclics amb l'aplicació aparentment sana (07-01)
kubectl delete pod api-trencada

  1. restartPolicy: qui reinicia què

spec.restartPolicy s'aplica a tots els contenidors del pod i només admet tres valors. És un camp immutable: no es pot canviar en un pod ja creat.

Valor Comportament Quan es fa servir
Always (per defecte) Reinicia el contenidor sempre que acabi, amb èxit o amb error Serveis de llarga durada: botiga-web, api-reserves, redis-cache
OnFailure Reinicia només si acaba amb codi diferent de 0 Tasques que han de completar-se: informes-ocupacio
Never No reinicia mai Tasques d'un sol intent, depuració

Tres precisions que eviten confusions molt comunes:

  1. Qui aplica la política és el kubelet del node, no un controlador del pla de control. Per això funciona fins i tot en pods solts, sense cap Deployment al darrere.
  2. Reiniciar un contenidor no és recrear el pod. El pod continua sent el mateix objecte, amb el mateix nom, el mateix node i la mateixa IP. Només puja el comptador RESTARTS. Aquesta és la diferència exacta que vas comprovar a l'exercici 3 del mòdul 1.
  3. Els Deployments exigeixen Always. Com veurem a Deployments, la seva plantilla de pod no admet cap altre valor. OnFailure i Never són terreny de Treballs i CronJobs.

  1. Cicle de vida complet: de la creació al SIGTERM

8.1. Naixement

Aquest és el recorregut que ja vas veure a Arquitectura de Kubernetes, ara centrat en el pod:

flowchart TD
    A["kubectl apply<br/>manifest del pod"] --> B["kube-apiserver valida,<br/>aplica valors per defecte<br/>i persisteix a etcd"]
    B --> C["Fase: Pending<br/>spec.nodeName és buit"]
    C --> D["kube-scheduler tria node<br/>i escriu spec.nodeName"]
    D --> E["El kubelet del node ho detecta<br/>i crea el contenidor de pausa"]
    E --> F["Estat del contenidor: Waiting<br/>motiu ContainerCreating"]
    F --> G["Descàrrega d'imatge<br/>i muntatge de volums"]
    G --> H["Arrencada dels contenidors"]
    H --> I["Fase: Running<br/>estat del contenidor: Running"]

8.2. Mort: l'aturada ordenada

Aquesta part és la que gairebé ningú estudia i la que causa els errors 502 durant els desplegaments. Quan esborres un pod, això és el que passa exactament:

  1. L'API marca el pod amb deletionTimestamp i li assigna un termini de gràcia (terminationGracePeriodSeconds, 30 s per defecte). La fase passa a Terminating.
  2. En paral·lel, el pod s'elimina dels Endpoints de tots els Serveis que el seleccionaven, per deixar de rebre trànsit nou (02-05).
  3. El kubelet envia SIGTERM al procés PID 1 de cada contenidor. Aquí és on la teva aplicació ha de deixar d'acceptar connexions noves, acabar les que tingui en curs i tancar netament.
  4. S'espera fins a exhaurir el termini de gràcia.
  5. Si en acabar el termini el procés continua viu, el kubelet envia SIGKILL. Sense negociació possible.
  6. Es destrueix el sandbox i l'objecte desapareix de l'API.
kubectl delete pod api-reserves
pod "api-reserves" deleted

Per veure-ho a càmera lenta, esborra amb un termini llarg des d'una altra terminal i observa:

kubectl delete pod api-reserves --grace-period=60 &
kubectl get pod api-reserves
NAME           READY   STATUS        RESTARTS   AGE
api-reserves   1/1     Terminating   0          4m12s

Conseqüències pràctiques per a Rutas Norte:

  • worker-notificacions ha de capturar SIGTERM. Si l'ignora, cada desplegament el mata als 30 segons amb SIGKILL a mig enviament de correu, i un client es queda sense la seva confirmació.
  • --grace-period=0 --force és perillós. Diu a l'API que doni el pod per mort sense esperar confirmació del kubelet. En una base de dades com postgres-reserves pot provocar corrupció o dues instàncies escrivint alhora. Fes-lo servir només amb pods encallats en un node perdut.
  • Un termini de gràcia massa curt talla peticions en curs. Si api-reserves té peticions que triguen 20 segons, un termini de 10 les tallarà en sec.

  1. Pods efímers per depurar

Un pod no és només una càrrega de treball: també és la millor navalla suïssa de diagnòstic dins del clúster. kubectl run amb --rm -it crea un pod, et dona una terminal a dins i l'esborra en sortir.

kubectl run depurador --rm -it --image=nicolaka/netshoot --restart=Never -- /bin/bash

A dins tens curl, dig, nslookup, ping, tcpdump, netstat... tot el que la imatge de la teva aplicació no porta. Desglossament dels flags:

Flag Què fa
--rm Esborra el pod en acabar la sessió
-it Terminal interactiva connectada (stdin + tty)
--restart=Never Crea un Pod solt en lloc d'un Deployment
-- /bin/bash Ordre a executar dins del contenidor

Un exemple real que farem servir molt a partir de la lliçó de Serveis: comprovar si api-reserves respon des de dins del clúster.

kubectl run test-api --rm -it --image=curlimages/curl:8.8.0 --restart=Never -- \
  curl -s http://10.244.0.34:3000/disponibilitat
{"servei":"api-reserves","ruta":"/disponibilitat"}
pod "test-api" deleted

Si el que vols és depurar un pod que ja existeix i la imatge del qual no té eines (nginx:alpine no porta ni curl), la solució moderna són els contenidors efímers, que s'injecten en un pod viu sense reiniciar-lo:

kubectl debug -it botiga-web --image=nicolaka/netshoot --target=nginx

Comparteix el namespace de xarxa del contenidor nginx, així que pots fer curl http://localhost:80 com si fossis ell. És l'eina que aprofundirem a Depuració d'Aplicacions i Esdeveniments del Clúster.

  1. Per què en producció mai no es gestionen pods solts

Ja ho vas experimentar al final del mòdul 1, però ara que coneixes el cicle de vida pots entendre'n la raó profunda: el pod no té mecanisme de recuperació propi. El kubelet reinicia els seus contenidors, sí, però el pod com a objecte només existeix mentre algú el declari.

I hi ha situacions, molt freqüents, en què el pod desapareix sense que ningú l'esborri:

Situació Què li passa al pod solt
El node s'apaga o es reinicia Desapareix per sempre; ningú el recrea en un altre node
El node es queda sense memòria El kubelet el desallotja (Evicted) i no torna
Es drena el node per manteniment S'elimina i no es reprograma
Algú l'esborra per error No torna

Per a Rutas Norte, qualsevol d'aquestes quatre files equival a la caiguda nocturna de quatre hores i mitja que va patir l'empresa al març. Per això la regla del projecte és taxativa:

A Rutas Norte, l'únic pod solt admès és un pod efímer de depuració amb --rm. Tota la resta va governada per un controlador.

El que necessitem és alguna cosa que vigili permanentment i digui "vull 3 pods com aquest, sempre". Això és un ReplicaSet, i és la lliçó següent.

Abans de continuar, deixa el clúster amb els dos pods en marxa:

kubectl apply -f k8s/base/api-reserves-pod.yaml
kubectl get pods
NAME           READY   STATUS    RESTARTS   AGE
api-reserves   1/1     Running   0          5s
botiga-web     1/1     Running   0          38m

Errors Comuns i Consells

  • Posar diversos processos sense relació al mateix pod. La pregunta de control és: "escalarien al mateix ritme i moririen alhora?". Si la resposta és no, són pods separats. Un api-reserves amb el seu redis-cache a dins seria un error de disseny greu.
  • Confondre "reiniciar el contenidor" amb "recrear el pod". El primer el fa el kubelet, conserva nom i IP i puja RESTARTS. El segon el fa un controlador i genera un objecte nou amb IP nova.
  • Buscar la causa d'un CrashLoopBackOff a kubectl describe. És a kubectl logs --previous. describe et dirà que reinicia, no per què.
  • Interpretar Running com a "funciona". Només vol dir que hi ha processos vius. La salut real la determinen les sondes.
  • Oblidar --previous amb contenidors que reinicien. Sense ell perds justament el log que t'interessa.
  • Ignorar SIGTERM a l'aplicació. És la causa número u de peticions tallades durant els desplegaments. Captura el senyal, deixa d'acceptar connexions noves, drena les obertes i surt amb codi 0.
  • Abusar de --force --grace-period=0. Se salta l'aturada ordenada. En càrregues amb estat pot corrompre dades.
  • Posar dos contenidors escoltant al mateix port dins d'un pod. Comparteixen l'espai de ports: el segon fallarà en arrencar.
  • Consell: genera l'esquelet de qualsevol manifest amb kubectl run api-reserves --image=node:20-alpine --dry-run=client -o yaml > pod.yaml i edita'l. És molt més ràpid i segur que escriure YAML des de zero.
  • Consell: kubectl explain pod.spec.containers.lifecycle --recursive és la teva documentació offline i sempre coincideix amb la versió del teu clúster.

Exercicis

Exercici 1: Diagnosticar tres pods trencats

Crea deliberadament aquests tres pods a rutas-norte-dev i, per a cadascun, identifica la fase del pod, l'estat i el motiu del contenidor i l'ordre exacta que revela la causa arrel:

  1. pod-a: imatge nginx:9.9.9-inventada.
  2. pod-b: imatge redis:7.2-alpine amb command: ["redis-server", "--fitxer-inexistent"].
  3. pod-c: imatge busybox:1.36 amb command: ["sh", "-c", "echo informe generat"] i restartPolicy: Never.

Explica per què el tercer no és un error, encara que el seu STATUS no sigui Running.

Exercici 2: Compartició dins d'un pod

Escriu k8s/base/pod-compartit.yaml amb un pod anomenat demo-compartit a rutas-norte-dev que tingui dos contenidors i un volum emptyDir anomenat intercanvi:

  • escriptor: imatge busybox:1.36, escriu cada 5 segons una línia amb la data a /dades/ocupacio.log.
  • lector: imatge busybox:1.36, fa tail -f d'aquest mateix fitxer muntat a /entrada/ocupacio.log.

Després demostra amb ordres: (a) que el lector veu el que escriu l'escriptor, (b) que tots dos contenidors comparteixen la mateixa IP i (c) que no comparteixen el sistema de fitxers arrel.

Exercici 3: Aturada ordenada de worker-notificacions

Crea un pod worker-notificacions amb busybox:1.36 que capturi SIGTERM, escrigui al log tancant: acabant enviaments pendents, esperi 5 segons i surti. Fixa terminationGracePeriodSeconds: 30.

  1. Esborra'l i comprova als logs que el missatge es va escriure abans de morir.
  2. Repeteix l'experiment amb terminationGracePeriodSeconds: 2 i explica què canvia i per què és perillós per a Rutas Norte.

Solucions

Solució 1

kubectl run pod-a --image=nginx:9.9.9-inventada
kubectl run pod-b --image=redis:7.2-alpine -- redis-server --fitxer-inexistent
kubectl run pod-c --image=busybox:1.36 --restart=Never -- sh -c "echo informe generat"
kubectl get pods
NAME    READY   STATUS             RESTARTS      AGE
pod-a   0/1     ImagePullBackOff   0             45s
pod-b   0/1     CrashLoopBackOff   3 (28s ago)   75s
pod-c   0/1     Completed          0             45s
Pod Fase Estat i motiu del contenidor Ordre de diagnòstic
pod-a Pending Waiting / ImagePullBackOff kubectl describe pod pod-a (secció Events)
pod-b Running Waiting / CrashLoopBackOff kubectl logs pod-b --previous
pod-c Succeeded Terminated / Completed, exitCode: 0 kubectl logs pod-c
kubectl describe pod pod-a | grep -A3 Events
kubectl logs pod-b --previous
kubectl logs pod-c
  Warning  Failed  30s  kubelet  Failed to pull image "nginx:9.9.9-inventada": not found

Bad directive: --fitxer-inexistent

informe generat

pod-c no és un error: l'etiqueta 9.9.9-inventada de pod-a no existeix al registre i el redis-server de pod-b rebutja el paràmetre. En canvi pod-c va fer exactament el que se li va demanar, va acabar amb codi 0 i, gràcies a restartPolicy: Never, no es va reiniciar: fase Succeeded. És el comportament normal d'una tasca finita com informes-ocupacio. Si li haguéssim deixat restartPolicy: Always, entraria en CrashLoopBackOff tot i no fallar mai, perquè Always reinicia també les sortides amb èxit.

kubectl delete pod pod-a pod-b pod-c

Solució 2

# k8s/base/pod-compartit.yaml
apiVersion: v1
kind: Pod
metadata:
  name: demo-compartit
  namespace: rutas-norte-dev
  labels:
    app: demo-compartit
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
spec:
  containers:
    - name: escriptor
      image: busybox:1.36
      command: ["sh", "-c"]
      args: ["while true; do echo \"$(date) ocupacio calculada\" >> /dades/ocupacio.log; sleep 5; done"]
      volumeMounts:
        - name: intercanvi
          mountPath: /dades
    - name: lector
      image: busybox:1.36
      command: ["sh", "-c"]
      args: ["sleep 3; tail -f /entrada/ocupacio.log"]
      volumeMounts:
        - name: intercanvi
          mountPath: /entrada
  volumes:
    - name: intercanvi
      emptyDir: {}
kubectl apply -f k8s/base/pod-compartit.yaml
# (a) el lector veu el que escriu l'escriptor
kubectl logs demo-compartit -c lector --tail=3
Wed Aug  5 11:02:14 UTC 2026 ocupacio calculada
Wed Aug  5 11:02:19 UTC 2026 ocupacio calculada
Wed Aug  5 11:02:24 UTC 2026 ocupacio calculada
# (b) mateixa IP: la del pod
kubectl exec demo-compartit -c escriptor -- hostname -i
kubectl exec demo-compartit -c lector -- hostname -i
10.244.0.37
10.244.0.37
# (c) sistemes de fitxers arrel diferents
kubectl exec demo-compartit -c escriptor -- ls /dades
kubectl exec demo-compartit -c lector -- ls /dades
ocupacio.log
ls: /dades: No such file or directory

El fitxer només existeix sota /dades per a l'escriptor i sota /entrada per al lector: el volum es comparteix, el sistema de fitxers arrel no. Cada contenidor veu el volum a la ruta on l'ha muntat, i res més de l'altre.

kubectl delete -f k8s/base/pod-compartit.yaml

Solució 3

# k8s/base/worker-notificacions-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: worker-notificacions
  namespace: rutas-norte-dev
  labels:
    app: worker-notificacions
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
spec:
  terminationGracePeriodSeconds: 30
  containers:
    - name: worker
      image: busybox:1.36
      command: ["sh", "-c"]
      args:
        - |
          trap 'echo "tancant: acabant enviaments pendents"; sleep 5; exit 0' TERM
          echo "worker-notificacions a punt"
          while true; do sleep 1; done
kubectl apply -f k8s/base/worker-notificacions-pod.yaml
kubectl delete pod worker-notificacions --wait=false
sleep 3 && kubectl logs worker-notificacions
worker-notificacions a punt
tancant: acabant enviaments pendents

El procés va rebre SIGTERM, va executar la seva rutina de tancament i va sortir pel seu propi peu dins del termini de 30 segons.

Amb terminationGracePeriodSeconds: 2, el kubelet envia SIGTERM, espera només 2 segons i etziba SIGKILL quan el worker encara és al seu sleep 5. El procés mor a mig tancament. Per a Rutas Norte això significa un correu de confirmació de reserva que es queda a mig enviar a cada desplegament: el client ha pagat el bitllet i no rep justificant. La regla és dimensionar el termini de gràcia per sobre del temps real que triga el component a drenar la feina pendent.

kubectl delete pod worker-notificacions --ignore-not-found

Conclusió

El pod ha deixat de ser una caixa negra. Saps per què Kubernetes va triar el pod i no el contenidor com a unitat mínima: hi ha processos que necessiten compartir xarxa, volums i cicle de vida, i el planificador els ha de tractar com una sola cosa indivisible. Coneixes exactament què comparteixen els seus contenidors —namespace de xarxa amb una única IP, localhost, IPC, volums, node i destí— i què no —sistema de fitxers arrel, PIDs i recursos—, i saps que qui sosté aquests namespaces és el discret contenidor de pausa, la raó que la IP sobrevisqui als reinicis.

Has escrit el manifest d'api-reserves camp per camp, distingint command/args d'ENTRYPOINT/CMD, i has après a llegir l'estat real d'un pod separant les cinc fases (Pending, Running, Succeeded, Failed, Unknown) dels tres estats de contenidor (Waiting, Running, Terminated) amb els seus motius: ContainerCreating és normal, ImagePullBackOff es diagnostica als esdeveniments i CrashLoopBackOff a kubectl logs --previous. Saps què fa restartPolicy i qui l'aplica, i has seguit el cicle de vida complet fins al final incòmode: SIGTERM, termini de gràcia i SIGKILL, la diferència entre un desplegament net i un client sense la seva confirmació de reserva. I ja tens al cinturó els pods efímers --rm -it i kubectl debug com a eines de diagnòstic.

Queda pendent el que ens ha portat fins aquí: el pod, tot sol, no es recupera. Si el node cau, si el desallotgen per manca de memòria o si algú l'esborra, api-reserves desapareix del clúster i no torna. A la lliçó següent, ReplicaSets, coneixerem el primer controlador del curs: el que executa sense descans el bucle de reconciliació que vam estudiar a l'arquitectura, compta quants pods hi ha davant de quants n'hi hauria d'haver i crea els que falten. Esborrarem un pod de botiga-web a propòsit i el veurem renéixer en segons.

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