La lliçó anterior va acabar amb una mancança molt concreta: un ReplicaSet manté amb vida N còpies d'una plantilla, però no sap canviar de versió. Canviaves la imatge del template i els pods existents continuaven tan tranquils amb la imatge antiga. El Deployment és la peça que tanca aquell forat i, amb diferència, l'objecte que més escriuràs a la teva vida professional amb Kubernetes. Governa ReplicaSets, i a través d'ells, pods: et dona rèpliques, autoreparació, escalat, actualitzacions controlades, historial de revisions i marxa enrere. En aquesta lliçó entendràs la cadena Deployment → ReplicaSet → Pod i què aporta exactament cada baula, convertiràs per fi el pod solt de botiga-web en un Deployment de 3 rèpliques, crearàs el d'api-reserves, aprendràs a llegir amb precisió les columnes de kubectl get deploy i les conditions de l'estat, escalaràs de dues maneres diferents i observaràs què passa al clúster en canviar una imatge. Acabaràs sabent també quan un Deployment no és la càrrega de treball adequada.

Contingut

  1. La cadena Deployment → ReplicaSet → Pod
  2. De pod solt a Deployment: botiga-web
  3. El manifest complet, comentat
  4. El Deployment d'api-reserves
  5. Llegir kubectl get deploy: READY, UP-TO-DATE, AVAILABLE
  6. status, conditions i kubectl rollout status
  7. Escalat: kubectl scale i manifest
  8. Observació: què passa en canviar la imatge
  9. Quan un Deployment no és la càrrega adequada

  1. La cadena Deployment → ReplicaSet → Pod

Kubernetes hauria pogut ficar tota la funcionalitat en un sol objecte. No ho va fer, i la raó és bona: cada nivell té una responsabilitat i només una.

flowchart TD
    D["<b>Deployment</b> botiga-web<br/>Gestiona VERSIONS<br/>historial, actualització, rollback"]
    RS1["<b>ReplicaSet</b> botiga-web-7d9f8c6b4<br/>versió 1.27.0 · rèpliques: 3<br/>Gestiona QUANTITAT"]
    RS2["<b>ReplicaSet</b> botiga-web-5c8b7a2d1<br/>versió 1.26.0 · rèpliques: 0<br/>revisió anterior, conservada"]
    P1["Pod botiga-web-7d9f8c6b4-4kx7d"]
    P2["Pod botiga-web-7d9f8c6b4-9wq2m"]
    P3["Pod botiga-web-7d9f8c6b4-pv6cl"]
    D --> RS1
    D --> RS2
    RS1 --> P1
    RS1 --> P2
    RS1 --> P3

La divisió de la feina, en una taula:

Nivell La seva única pregunta Què sap fer Què no sap fer
Pod Són vius els meus contenidors? Reiniciar contenidors caiguts (via kubelet i restartPolicy) Recrear-se si desapareix
ReplicaSet Hi ha N pods amb aquestes etiquetes? Crear i esborrar pods fins a quadrar el número Canviar de versió
Deployment Quina versió ha d'estar servint, i com hi arribo? Crear ReplicaSets, traspassar rèpliques entre ells, guardar historial, desfer Res més: delega tota la resta

La clau per entendre els Deployments és aquesta frase: un Deployment no gestiona pods, gestiona ReplicaSets. Cada cop que canvies alguna cosa del template, el Deployment crea un ReplicaSet nou i va movent rèpliques del vell al nou. Un ReplicaSet per revisió, i cadascun sap mantenir la seva pròpia quantitat de pods. El Deployment orquestra el traspàs.

Aquell sufix hexadecimal dels noms (botiga-web-7d9f8c6b4) no és aleatori: és el pod-template-hash, un hash calculat sobre el contingut del template. El Deployment l'afegeix com a etiqueta a cada pod i al selector de cada ReplicaSet, i així aconsegueix que dos ReplicaSets del mateix Deployment no es barallin mai pels mateixos pods. És exactament la mena de selector de conjunts que, com hem vist, el vell ReplicationController no podia expressar.

  1. De pod solt a Deployment: botiga-web

Al final del mòdul 1 vam deixar botiga-web com un pod solt, i vam comprovar que en esborrar-lo no tornava. Ha arribat el moment de saldar aquell deute.

Partim del namespace buit, tal com va quedar al final de la lliçó anterior:

kubectl config set-context --current --namespace=rutas-norte-dev
kubectl get all
No resources found in rutas-norte-dev namespace.

La conversió d'un pod en un Deployment segueix sempre el mateix patró mecànic:

Pod solt Deployment
apiVersion: v1 apiVersion: apps/v1
kind: Pod kind: Deployment
metadata metadata (del Deployment)
— spec.replicas
— spec.selector
spec: (els contenidors) spec.template.spec
metadata.labels spec.template.metadata.labels

És a dir: el pod sencer s'enfonsa un nivell i es converteix en el template, i per sobre apareixen replicas i selector.

  1. El manifest complet, comentat

# k8s/base/botiga-web-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: botiga-web
  namespace: rutas-norte-dev
  labels:
    app: botiga-web
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
  annotations:
    kubernetes.io/change-cause: "Desplegament inicial de botiga-web 1.27.0"
spec:
  replicas: 3
  revisionHistoryLimit: 5
  selector:
    matchLabels:
      app: botiga-web
      entorn: dev
  template:
    metadata:
      labels:
        app: botiga-web
        app.kubernetes.io/part-of: rutas-norte
        entorn: dev
    spec:
      containers:
        - name: nginx
          image: nginx:1.27-alpine
          ports:
            - name: http
              containerPort: 80
          resources:
            requests:
              cpu: "50m"
              memory: "64Mi"
            limits:
              cpu: "200m"
              memory: "128Mi"

El que és nou respecte del ReplicaSet de la lliçó anterior, camp per camp:

  • kind: Deployment, mateix grup apps/v1.
  • metadata.annotations amb kubernetes.io/change-cause: text lliure que quedarà registrat a l'historial de revisions com el "motiu" d'aquest desplegament. L'explotarem a fons a la lliçó següent.
  • spec.replicas: 3: tres rèpliques de botiga-web. El Deployment no les crea ell; ho encarrega al seu ReplicaSet.
  • spec.revisionHistoryLimit: 5: quants ReplicaSets antics (amb 0 rèpliques) es conserven per poder desfer. Per defecte en són 10.
  • spec.selector: obligatori i immutable, igual que al ReplicaSet. I amb el mateix advertiment: que sigui precís, app + entorn.
  • spec.template: idèntic al del ReplicaSet. Aquí hi ha la clau conceptual: qualsevol canvi sota template dispara una revisió nova; els canvis fora d'ell (com ara replicas) no.

No apareix spec.strategy perquè el valor per defecte, RollingUpdate, és el que volem. Aquell camp i tots els seus paràmetres són el contingut íntegre de la lliçó següent, Actualitzacions, Rollbacks i Estratègies de Desplegament.

Aplica'l:

kubectl apply -f k8s/base/botiga-web-deployment.yaml
kubectl get deploy,rs,pods
deployment.apps/botiga-web created

NAME                         READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/botiga-web   3/3     3            3           9s

NAME                                    DESIRED   CURRENT   READY   AGE
replicaset.apps/botiga-web-7d9f8c6b4    3         3         3       9s

NAME                               READY   STATUS    RESTARTS   AGE
pod/botiga-web-7d9f8c6b4-4kx7d     1/1     Running   0          9s
pod/botiga-web-7d9f8c6b4-9wq2m     1/1     Running   0          9s
pod/botiga-web-7d9f8c6b4-pv6cl     1/1     Running   0          9s

Aquí hi ha la cadena completa, materialitzada en tres blocs de sortida. Has creat un objecte i han aparegut tres nivells. Comprova'n la genealogia:

kubectl get rs botiga-web-7d9f8c6b4 -o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'
kubectl get pod botiga-web-7d9f8c6b4-4kx7d -o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'
Deployment/botiga-web
ReplicaSet/botiga-web-7d9f8c6b4

I mira l'etiqueta que ha afegit el Deployment pel seu compte als pods:

kubectl get pods -l app=botiga-web --show-labels
NAME                          READY   STATUS    RESTARTS   AGE   LABELS
botiga-web-7d9f8c6b4-4kx7d    1/1     Running   0          2m    app=botiga-web,app.kubernetes.io/part-of=rutas-norte,entorn=dev,pod-template-hash=7d9f8c6b4

Tu no vas escriure pod-template-hash. L'hi va afegir el Deployment, i és el que li permetrà distingir els pods d'una revisió dels d'una altra.

Verifica que ara sí que s'autorepara:

kubectl delete pod botiga-web-7d9f8c6b4-4kx7d
kubectl get pods -l app=botiga-web
pod "botiga-web-7d9f8c6b4-4kx7d" deleted

NAME                          READY   STATUS    RESTARTS   AGE
botiga-web-7d9f8c6b4-9wq2m    1/1     Running   0          3m
botiga-web-7d9f8c6b4-hs4bd    1/1     Running   0          2s
botiga-web-7d9f8c6b4-pv6cl    1/1     Running   0          3m

El pod solt del mòdul 1 no tornava. Aquest torna en dos segons, i qui el retorna no és el Deployment sinó el seu ReplicaSet, exactament com vas aprendre a la lliçó anterior.

  1. El Deployment d'api-reserves

Segon component. Mateix esquema, amb les particularitats de l'API.

# k8s/base/api-reserves-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-reserves
  namespace: rutas-norte-dev
  labels:
    app: api-reserves
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
  annotations:
    kubernetes.io/change-cause: "Desplegament inicial d'api-reserves 2.4.0"
spec:
  replicas: 2
  revisionHistoryLimit: 5
  selector:
    matchLabels:
      app: api-reserves
      entorn: dev
  template:
    metadata:
      labels:
        app: api-reserves
        app.kubernetes.io/part-of: rutas-norte
        entorn: dev
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: api
          image: node:20-alpine
          command: ["node", "-e"]
          args:
            - |
              const http = require('http');
              const pod = process.env.HOSTNAME;
              http.createServer((req, res) => {
                res.writeHead(200, {'Content-Type': 'application/json'});
                res.end(JSON.stringify({servei: 'api-reserves', version: '2.4.0', pod}));
              }).listen(3000, () => console.log(`api-reserves 2.4.0 a punt a ${pod}`));
          ports:
            - name: http
              containerPort: 3000
          env:
            - name: ENTORN
              value: "dev"
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "256Mi"

Detalls deliberats d'aquest manifest:

  • replicas: 2: en desenvolupament n'hi ha prou amb dues. En producció el número el decidirà l'autoescalat del mòdul 9.
  • terminationGracePeriodSeconds: 30 dins del template.spec: recorda de la lliçó de Pods que és el que permet que una reserva en curs no es talli durant un desplegament.
  • La resposta inclou el nom del pod (process.env.HOSTNAME, que en un pod és el seu nom). Això ens servirà a la lliçó de Serveis per demostrar el balanceig de càrrega: cada petició respondrà amb un pod diferent.
kubectl apply -f k8s/base/api-reserves-deployment.yaml
kubectl get deploy
deployment.apps/api-reserves created

NAME           READY   UP-TO-DATE   AVAILABLE   AGE
api-reserves   2/2     2            2           14s
botiga-web     3/3     3            3           9m

Comprova que l'API respon, fent servir un pod efímer com els de la lliçó de Pods:

kubectl run test --rm -it --image=curlimages/curl:8.8.0 --restart=Never -- \
  curl -s http://$(kubectl get pod -l app=api-reserves -o jsonpath='{.items[0].status.podIP}'):3000/disponibilitat
{"servei":"api-reserves","version":"2.4.0","pod":"api-reserves-6b4c9d7f5-x2jkp"}
pod "test" deleted

Funciona, però fixa't en la incomoditat: hem hagut d'esbrinar la IP d'un pod concret per parlar-hi. Aquella IP canviarà al pròxim desplegament. El problema està servit per a la lliçó de Serveis.

  1. Llegir kubectl get deploy: READY, UP-TO-DATE, AVAILABLE

Aquestes tres columnes són la informació més densa de tot Kubernetes, i confondre-les porta a diagnòstics equivocats. Les tres compten pods, però compten coses diferents.

NAME           READY   UP-TO-DATE   AVAILABLE   AGE
api-reserves   2/2     2            2           14s
Columna Què compta exactament Camp del status
READY pods llestos / pods desitjats. "Llest" = tots els seus contenidors passen les seves comprovacions readyReplicas / replicas
UP-TO-DATE Pods creats amb la revisió actual del template updatedReplicas
AVAILABLE Pods que porten llestos com a mínim minReadySeconds seguits availableReplicas

La manera útil de llegir-les és preguntar-se què significa cada discrepància:

Situació Interpretació
READY 3/3, UP-TO-DATE 3, AVAILABLE 3 Tot correcte i estable
READY 2/3 Un pod no està llest: arrencant, o fallant
UP-TO-DATE 1 de 3 Actualització en curs: només 1 pod té la versió nova
READY 3/3 però AVAILABLE 2 Un pod està llest però encara no ha complert minReadySeconds; es considera "jove"
READY 0/3 durant minuts Desplegament trencat: mira els pods amb describe

Un exemple d'actualització a mitges, que veuràs molt:

NAME           READY   UP-TO-DATE   AVAILABLE   AGE
api-reserves   4/4     2            3           12m

Això es llegeix així: hi ha 4 pods llestos encara que el desitjat en siguin 4, però només 2 tenen la versió nova, i només 3 porten prou temps per comptar-se com a disponibles. Som a mitja actualització progressiva, i encara hi conviuen pods de dues revisions.

Per veure'n més, -o wide afegeix imatges i selector:

kubectl get deploy -o wide
NAME           READY   UP-TO-DATE   AVAILABLE   AGE   CONTAINERS   IMAGES            SELECTOR
api-reserves   2/2     2            2           14m   api          node:20-alpine    app=api-reserves,entorn=dev
botiga-web     3/3     3            3           23m   nginx        nginx:1.27-alpine app=botiga-web,entorn=dev

  1. status, conditions i kubectl rollout status

Com tot objecte de Kubernetes, un Deployment té un spec (el que demanes) i un status (el que hi ha). L'status complet:

kubectl get deploy botiga-web -o jsonpath='{.status}' | python3 -m json.tool
{
    "availableReplicas": 3,
    "conditions": [
        {
            "lastTransitionTime": "2026-08-05T12:03:11Z",
            "lastUpdateTime": "2026-08-05T12:03:11Z",
            "message": "Deployment has minimum availability.",
            "reason": "MinimumReplicasAvailable",
            "status": "True",
            "type": "Available"
        },
        {
            "lastTransitionTime": "2026-08-05T12:03:05Z",
            "lastUpdateTime": "2026-08-05T12:03:11Z",
            "message": "ReplicaSet \"botiga-web-7d9f8c6b4\" has successfully progressed.",
            "reason": "NewReplicaSetAvailable",
            "status": "True",
            "type": "Progressing"
        }
    ],
    "observedGeneration": 1,
    "readyReplicas": 3,
    "replicas": 3,
    "updatedReplicas": 3
}

Les conditions són el mecanisme estàndard de Kubernetes per expressar estats parcials, i un Deployment en té tres tipus:

Condition status: True significa status: False significa
Available Hi ha prou rèpliques disponibles per donar servei El Deployment no està servint amb garanties
Progressing El desplegament avança (o ha acabat bé) S'ha encallat: ha superat progressDeadlineSeconds
ReplicaFailure (només apareix si hi ha problema) No es poden crear pods: quota, permisos, límits —

La combinació Available: True + Progressing: True amb motiu NewReplicaSetAvailable és la signatura d'un Deployment sa i acabat.

Un altre camp que mereix atenció és observedGeneration. Cada canvi al spec incrementa metadata.generation; el controlador copia aquell número a status.observedGeneration quan ha processat el canvi. Si veus generation: 5 i observedGeneration: 4, el controlador encara no ha reaccionat al teu últim canvi.

En el dia a dia no llegeixes el JSON: fas servir l'ordre que l'interpreta per tu.

kubectl rollout status deployment/botiga-web
deployment "botiga-web" successfully rolled out

kubectl rollout status bloqueja la terminal fins que el desplegament acaba i retorna codi de sortida 0 si va bé, diferent de 0 si falla. Per això és la instrucció que es posa en qualsevol canalització de CI/CD just darrere del kubectl apply: converteix "he enviat el canvi" en "el canvi està funcionant". Admet --timeout:

kubectl apply -f k8s/base/botiga-web-deployment.yaml
kubectl rollout status deployment/botiga-web --timeout=120s

  1. Escalat: kubectl scale i manifest

Rutas Norte té un pont a la vista i cal apujar rèpliques d'api-reserves. Dues vies, les mateixes que amb el ReplicaSet.

Imperativa, per reaccionar ja:

kubectl scale deployment api-reserves --replicas=4
kubectl get deploy api-reserves
deployment.apps/api-reserves scaled

NAME           READY   UP-TO-DATE   AVAILABLE   AGE
api-reserves   4/4     4            4           18m

Observa un detall importantíssim:

kubectl get rs -l app=api-reserves
NAME                     DESIRED   CURRENT   READY   AGE
api-reserves-6b4c9d7f5   4         4         4       18m

No ha aparegut cap ReplicaSet nou. Escalar no canvia el template, així que no és una revisió nova: el mateix ReplicaSet passa de 2 a 4. Aquesta distinció —canvis que creen revisió enfront de canvis que no— és l'eix de la lliçó següent.

Declarativa, la correcta segons les convencions del projecte: edita replicas: 4 a k8s/base/api-reserves-deployment.yaml, fes commit i aplica.

kubectl apply -f k8s/base/api-reserves-deployment.yaml

I un advertiment pràctic molt comú: si escales amb kubectl scale i no portes el canvi al fitxer, el següent kubectl apply tornarà les rèpliques al valor del manifest. Perdràs la capacitat extra en el pitjor moment possible. És la raó de la regla del projecte: el clúster reflecteix Git, no a l'inrevés.

Un tercer camí, útil per automatitzar sense editar fitxers a mà:

kubectl patch deployment api-reserves --type=merge -p '{"spec":{"replicas":4}}'

Tornem a 2 abans de continuar:

kubectl scale deployment api-reserves --replicas=2

  1. Observació: què passa en canviar la imatge

Aquesta és l'observació que motiva tota la lliçó següent. Canvia la versió d'nginx de botiga-web i mira l'estructura que apareix, sense preocupar-te encara del mecanisme.

kubectl set image deployment/botiga-web nginx=nginx:1.27.1-alpine
kubectl get rs -l app=botiga-web
deployment.apps/botiga-web image updated

NAME                    DESIRED   CURRENT   READY   AGE
botiga-web-5f6d8b9c7    3         3         3       25s
botiga-web-7d9f8c6b4    0         0         0       31m

Hi ha dos ReplicaSets. El nou (5f6d8b9c7) té les 3 rèpliques; el vell (7d9f8c6b4) s'ha quedat a 0 però no s'ha esborrat. I aquí hi ha la clau de l'historial: aquell ReplicaSet buit conserva la definició completa de la versió anterior, així que tornar enrere consisteix simplement a retornar-li les seves rèpliques.

Compara-ho amb el que passava a la lliçó anterior en fer kubectl set image sobre un ReplicaSet: res, els pods continuaven amb la imatge vella. Aquí, en canvi, els pods són nous i tenen la imatge nova:

kubectl get pods -l app=botiga-web -o custom-columns=NOM:.metadata.name,IMATGE:.spec.containers[0].image
NOM                           IMATGE
botiga-web-5f6d8b9c7-2xk4m    nginx:1.27.1-alpine
botiga-web-5f6d8b9c7-7q9wd    nginx:1.27.1-alpine
botiga-web-5f6d8b9c7-nb3rt    nginx:1.27.1-alpine

I l'historial ja té dues entrades:

kubectl rollout history deployment/botiga-web
deployment.apps/botiga-web
REVISION  CHANGE-CAUSE
1         Desplegament inicial de botiga-web 1.27.0
2         <none>

De moment, queda't amb aquestes quatre observacions:

  1. Canviar el template crea un ReplicaSet nou, amb un pod-template-hash diferent.
  2. El Deployment traspassa rèpliques del vell al nou de forma progressiva, no de cop.
  3. Els ReplicaSets antics es conserven a 0 rèpliques com a historial (fins a revisionHistoryLimit).
  4. Durant el traspàs conviuen pods de dues versions, que és el que fa possible desplegar sense tall de servei.

Com es controla aquell traspàs pas a pas —maxSurge, maxUnavailable, Recreate, pause, undo— és exactament el contingut d'Actualitzacions, Rollbacks i Estratègies de Desplegament. I les variants avançades, blue-green i canary, esperen a 11-04.

Torna botiga-web a la imatge original per començar la lliçó següent en un punt conegut:

kubectl apply -f k8s/base/botiga-web-deployment.yaml
kubectl rollout status deployment/botiga-web
deployment.apps/botiga-web configured
deployment "botiga-web" successfully rolled out

  1. Quan un Deployment no és la càrrega adequada

El Deployment és l'opció per defecte, però no l'única. Està dissenyat per a càrregues sense estat, intercanviables i de llarga durada: els seus pods són bestiar, no mascotes. S'anomenen amb sufixos aleatoris, es creen i es destrueixen en qualsevol ordre i cap no té identitat pròpia.

Quan això no encaixa, hi ha un altre objecte:

Necessitat Objecte correcte Per què falla el Deployment Lliçó
Identitat i emmagatzematge estables per rèplica (bases de dades, cues, clústers amb quòrum) StatefulSet Noms aleatoris, ordre d'arrencada no garantit, totes les rèpliques compartirien el mateix volum 06-01
Exactament un pod a cada node (agents de logs, mètriques, xarxa) DaemonSet Un Deployment reparteix N rèpliques on càpiguen, sense garantir cobertura de nodes 06-02
Tasca que s'executa, acaba i ja està Job Un Deployment reiniciaria el pod eternament en acabar (restartPolicy: Always obligatòria) 06-03
Tasca programada per horari CronJob Un Deployment no té noció de calendari 06-03

Aplicat als sis components de Rutas Norte:

Component Càrrega de treball Motiu
botiga-web Deployment Sense estat, rèpliques idèntiques i intercanviables
api-reserves Deployment Sense estat; tota la persistència és a PostgreSQL i Redis
postgres-reserves StatefulSet Necessita disc propi, nom de xarxa estable i arrencada ordenada
redis-cache Deployment (una rèplica) o StatefulSet Amb una sola instància i estat prescindible, un Deployment ja fa el fet
worker-notificacions Deployment Procés de llarga durada sense estat; no rep trànsit, així que no portarà Service
informes-ocupacio CronJob S'executa a les 02:30, acaba i no torna fins l'endemà

Un advertiment que evita un error clàssic: redis-cache com a Deployment només és acceptable amb una rèplica. Si escalessis a 3, tindries tres memòries cau independents darrere del mateix nom, i cada petició d'api-reserves aniria a una cau diferent amb contingut diferent. El resultat serien encerts de memòria cau erràtics i impossibles de diagnosticar. Escalar no és gratis: només funciona si el component és realment sense estat.

Errors Comuns i Consells

  • Oblidar spec.selector. És obligatori a apps/v1. Sense ell, l'API rebutja el manifest.
  • Intentar canviar el selector d'un Deployment existent. És immutable. Si el necessites, cal esborrar el Deployment (amb --cascade=orphan si no vols tallar el servei) i crear-ne un de nou.
  • Posar les etiquetes només al metadata de dalt. Les que governen els pods són les de spec.template.metadata.labels. És l'error de principiant més repetit.
  • Escalar amb kubectl scale i no tocar el fitxer. El següent apply revertirà el canvi, probablement en mal moment.
  • Confondre READY amb AVAILABLE. READY és "pot atendre"; AVAILABLE és "pot atendre i porta estable el temps mínim exigit".
  • Creure que kubectl apply espera que acabi el desplegament. No espera: només envia l'objecte. Per esperar de debò, encadena kubectl rollout status.
  • Fer servir un Deployment per a PostgreSQL. Amb més d'una rèplica, diverses instàncies intentarien muntar el mateix volum i escriure alhora. Corrupció garantida.
  • Escalar redis-cache a diverses rèpliques amb un Deployment. Tindràs N memòries cau incoherents, no una cau replicada.
  • Consell: genera l'esquelet amb kubectl create deployment api-reserves --image=node:20-alpine --replicas=2 --dry-run=client -o yaml > deploy.yaml i afegeix-hi després etiquetes, recursos i anotacions.
  • Consell: per veure la cadena completa d'un component d'un cop d'ull, kubectl get deploy,rs,pods -l app=botiga-web. Tres nivells en una sola ordre.

Exercicis

Exercici 1: Deployment de worker-notificacions

worker-notificacions és un procés de fons que consumeix una cua i envia correus. No rep trànsit entrant, així que no porta ports ni Service.

Escriu k8s/base/worker-notificacions-deployment.yaml amb 2 rèpliques a rutas-norte-dev, imatge busybox:1.36, una ordre que escrigui cada 10 segons enviant lot de confirmacions des de <nom del pod>, les tres etiquetes del projecte, terminationGracePeriodSeconds: 45 i requests de 50m/64Mi. Després:

  1. Aplica'l i espera amb l'ordre que bloqueja fins que acaba el desplegament.
  2. Mostra la cadena Deployment → ReplicaSet → Pod amb una sola ordre.
  3. Comprova als logs que tots dos pods treballen.
  4. Explica per què aquest component porta un termini de gràcia més gran que botiga-web.

Exercici 2: Llegir l'estat d'un desplegament

Un company t'envia aquesta captura d'un clúster de preproducció i et demana diagnòstic:

NAME           READY   UP-TO-DATE   AVAILABLE   AGE
api-reserves   3/5     2            3           7m
Conditions:
  Type           Status  Reason
  ----           ------  ------
  Available      True    MinimumReplicasAvailable
  Progressing    False   ProgressDeadlineExceeded

Respon, justificant cada resposta:

  1. Quants pods haurien d'existir i quants n'hi ha de llestos?
  2. Quants tenen la versió nova?
  3. Està el servei caigut?
  4. Què ha passat amb el desplegament?
  5. Quines dues ordres executaries, en aquest ordre, per trobar la causa arrel?

Exercici 3: De pod solt a Deployment, i decidir la càrrega correcta

  1. Crea a mà un pod solt redis-cache a rutas-norte-dev amb redis:7.2-alpine i les tres etiquetes del projecte.
  2. Converteix-lo en un Deployment d'1 rèplica anomenat redis-cache, escrivint el manifest i esborrant el pod solt. Verifica que la memòria cau respon a redis-cli ping.
  3. Escala el Deployment a 3 rèpliques i explica per què això és un error de disseny per a Rutas Norte, encara que tècnicament funcioni. Torna a 1.
  4. Per a cadascun dels sis components de Rutas Norte, indica l'objecte correcte i una frase de justificació.

Solucions

Solució 1

# k8s/base/worker-notificacions-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: worker-notificacions
  namespace: rutas-norte-dev
  labels:
    app: worker-notificacions
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
  annotations:
    kubernetes.io/change-cause: "Desplegament inicial de worker-notificacions 1.2.0"
spec:
  replicas: 2
  selector:
    matchLabels:
      app: worker-notificacions
      entorn: dev
  template:
    metadata:
      labels:
        app: worker-notificacions
        app.kubernetes.io/part-of: rutas-norte
        entorn: dev
    spec:
      terminationGracePeriodSeconds: 45
      containers:
        - name: worker
          image: busybox:1.36
          command: ["sh", "-c"]
          args:
            - |
              trap 'echo "tancant: acabant enviaments pendents"; sleep 5; exit 0' TERM
              while true; do
                echo "enviant lot de confirmacions des de $HOSTNAME"
                sleep 10
              done
          resources:
            requests:
              cpu: "50m"
              memory: "64Mi"
            limits:
              cpu: "200m"
              memory: "128Mi"
kubectl apply -f k8s/base/worker-notificacions-deployment.yaml
kubectl rollout status deployment/worker-notificacions
kubectl get deploy,rs,pods -l app=worker-notificacions
kubectl logs -l app=worker-notificacions --tail=2 --prefix
deployment.apps/worker-notificacions created
deployment "worker-notificacions" successfully rolled out

NAME                                    READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/worker-notificacions    2/2     2            2           12s

NAME                                               DESIRED   CURRENT   READY   AGE
replicaset.apps/worker-notificacions-8c7b5d94f     2         2         2       12s

NAME                                          READY   STATUS    RESTARTS   AGE
pod/worker-notificacions-8c7b5d94f-k2xrt      1/1     Running   0          12s
pod/worker-notificacions-8c7b5d94f-p9mzq      1/1     Running   0          12s

[pod/worker-notificacions-8c7b5d94f-k2xrt/worker] enviant lot de confirmacions des de worker-notificacions-8c7b5d94f-k2xrt
[pod/worker-notificacions-8c7b5d94f-p9mzq/worker] enviant lot de confirmacions des de worker-notificacions-8c7b5d94f-p9mzq
  1. El termini de gràcia és més gran perquè worker-notificacions pot estar a mig enviament de correu quan rep el SIGTERM. Si el kubelet el mata abans d'acabar, un client que ha pagat el bitllet es queda sense justificant i el missatge pot quedar en un estat ambigu (consumit de la cua però no enviat). botiga-web només serveix fitxers estàtics: les seves peticions duren mil·lisegons i 30 segons li sobren.

Solució 2

  1. Haurien d'existir 5 pods (spec.replicas = 5, denominador de READY) i n'hi ha 3 de llestos. En falten 2.
  2. Només 2 tenen la versió nova (UP-TO-DATE). Els altres pods llestos són de la revisió anterior.
  3. No està caigut. Available: True amb motiu MinimumReplicasAvailable indica que hi ha prou rèpliques disponibles per atendre trànsit. Sí que està degradat: serveix amb 3 pods en lloc de 5.
  4. El desplegament s'ha encallat: Progressing: False amb motiu ProgressDeadlineExceeded significa que ha superat progressDeadlineSeconds (600 s per defecte) sense avançar. Els 2 pods de la versió nova no arriben a estar llestos, així que el Deployment no pot continuar retirant els vells. Causa típica: imatge inexistent, CrashLoopBackOff per configuració o una sonda mal ajustada.
  5. Les dues ordres, en aquest ordre:
# 1. Veure quins pods estan fallant i amb quin motiu
kubectl get pods -l app=api-reserves -n rutas-norte-pre

# 2. La causa arrel del pod problematic
kubectl describe pod <pod-que-no-arrenca> -n rutas-norte-pre
# i, si el contenidor reinicia:
kubectl logs <pod-que-no-arrenca> -n rutas-norte-pre --previous

Solució 3

# 1. Pod solt
kubectl run redis-cache --image=redis:7.2-alpine \
  --labels="app=redis-cache,app.kubernetes.io/part-of=rutas-norte,entorn=dev"
# 2. k8s/base/redis-cache-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-cache
  namespace: rutas-norte-dev
  labels:
    app: redis-cache
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
  annotations:
    kubernetes.io/change-cause: "Desplegament inicial de redis-cache 7.2"
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis-cache
      entorn: dev
  template:
    metadata:
      labels:
        app: redis-cache
        app.kubernetes.io/part-of: rutas-norte
        entorn: dev
    spec:
      containers:
        - name: redis
          image: redis:7.2-alpine
          ports:
            - name: redis
              containerPort: 6379
          resources:
            requests:
              cpu: "50m"
              memory: "64Mi"
            limits:
              cpu: "200m"
              memory: "256Mi"
kubectl delete pod redis-cache
kubectl apply -f k8s/base/redis-cache-deployment.yaml
kubectl rollout status deployment/redis-cache
kubectl exec deploy/redis-cache -- redis-cli ping
pod "redis-cache" deleted
deployment.apps/redis-cache created
deployment "redis-cache" successfully rolled out
PONG

Nota pràctica: kubectl exec deploy/redis-cache tria automàticament un dels pods del Deployment, així no cal copiar el nom amb el seu sufix aleatori.

# 3. Escalar a 3 i tornar
kubectl scale deployment redis-cache --replicas=3
kubectl get pods -l app=redis-cache
kubectl scale deployment redis-cache --replicas=1

És un error de disseny perquè les tres rèpliques no formen una memòria cau replicada: són tres memòries cau independents i buides, cadascuna amb la seva pròpia memòria. Quan a la lliçó següent hi posem un Service al davant, cada consulta d'api-reserves caurà en una rèplica diferent, així que la disponibilitat de places que es va escriure a la rèplica A no es trobarà en llegir de la B. El resultat seria una taxa d'encerts de memòria cau al voltant del 33 %, consultes extra a postgres-reserves i, el pitjor, respostes incoherents segons a quina rèplica caigui la petició. Replicar Redis de debò requereix un StatefulSet amb configuració de rèplica o clúster, tema del mòdul 6.

  1. Objecte correcte per component:
Component Objecte Justificació
botiga-web Deployment Sense estat; qualsevol rèplica serveix qualsevol petició
api-reserves Deployment Sense estat; ho persisteix tot a PostgreSQL i Redis
postgres-reserves StatefulSet Necessita volum propi, identitat de xarxa estable i arrencada ordenada
redis-cache Deployment d'1 rèplica Estat prescindible i reconstruïble; amb més rèpliques caldria un StatefulSet
worker-notificacions Deployment Procés de llarga durada, sense estat i sense trànsit entrant
informes-ocupacio CronJob Execució programada i finita cada nit a les 02:30

Conclusió

Ja domines l'objecte que sosté la major part de qualsevol plataforma a Kubernetes. Has vist la cadena Deployment → ReplicaSet → Pod amb una responsabilitat nítida a cada baula: el pod manté vius els seus contenidors, el ReplicaSet manté la quantitat, i el Deployment gestiona les versions. Saps que un Deployment no mana sobre pods sinó sobre ReplicaSets, i que el pod-template-hash és el truc que permet que dues revisions convisquin sense barallar-se pels mateixos pods.

Has saldat el deute que arrossegàvem des del mòdul 1: botiga-web ja no és un pod fràgil sinó un Deployment de 3 rèpliques que es recupera sol en dos segons, i api-reserves té el seu amb 2 rèpliques. Saps llegir les tres columnes de kubectl get deploy sense confondre-les —READY és el que atén, UP-TO-DATE el que té la versió actual, AVAILABLE el que a més porta estable el temps mínim— i interpretar les conditions Available i Progressing, que són les que et diran en producció si un desplegament va bé, va degradat o s'ha quedat encallat. Escales amb kubectl scale per apagar focs i amb el manifest perquè el canvi perduri, i saps que el clúster reflecteix Git i no a l'inrevés. A més, tens el criteri per no forçar l'eina: PostgreSQL demana StatefulSet, un agent per node demana DaemonSet, l'informe nocturn demana CronJob, i escalar redis-cache a tres rèpliques trencaria la coherència de la memòria cau.

Queda per explicar el més interessant del que has vist: en canviar la imatge va aparèixer un segon ReplicaSet, el vell es va quedar a 0 rèpliques sense esborrar-se i durant uns segons hi van conviure pods de dues versions. Això no és un efecte secundari, és el mecanisme amb què Rutas Norte aconseguirà per fi el seu objectiu de zero talls als desplegaments, davant dels un o dos minuts d'aturada dels dimarts de matinada. A la lliçó següent, Actualitzacions, Rollbacks i Estratègies de Desplegament, controlarem aquell traspàs pod a pod amb maxSurge i maxUnavailable, veurem per què postgres-reserves no admetria una actualització progressiva, i aprendrem a diagnosticar i desfer un desplegament trencat amb kubectl rollout undo.

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