A la lliçó anterior vam canviar la imatge de botiga-web i vam observar una cosa que vam deixar deliberadament sense explicar: va aparèixer un segon ReplicaSet, el vell es va quedar a zero rèpliques sense esborrar-se, i durant uns segons hi van conviure pods de dues versions. Aquell mecanisme és la raó de ser del Deployment i la resposta directa al primer dels quatre objectius de Rutas Norte: zero talls als desplegaments, davant del minut o dos d'aturada que avui obliga a desplegar els dimarts de matinada. En aquesta lliçó prenem el control d'aquell traspàs. Veuràs les dues estratègies natives del Deployment i quan exigeix cadascuna, calcularàs pod a pod el que passa durant una actualització d'api-reserves, ajustaràs el ritme amb maxSurge i maxUnavailable, distingiràs quins canvis generen una revisió nova i quins no, i faràs servir el joc complet de kubectl rollout: status, history, undo, pause i resume. I acabaràs trencant un desplegament a propòsit amb una imatge inexistent, diagnosticant-lo i desfent-lo.

Contingut

  1. Les dues estratègies natives: RollingUpdate i Recreate
  2. Per què postgres-reserves no admet RollingUpdate
  3. maxSurge i maxUnavailable, pas a pas amb api-reserves
  4. Ritme i tolerància: minReadySeconds, progressDeadlineSeconds, revisionHistoryLimit
  5. Què dispara una revisió nova i què no
  6. El joc complet de kubectl rollout
  7. L'anotació kubernetes.io/change-cause
  8. Desplegament sense tall: què cal de debò
  9. Diagnòstic i rollback d'un desplegament encallat

  1. Les dues estratègies natives: RollingUpdate i Recreate

spec.strategy.type només admet dos valors. No hi ha més estratègies natives a Kubernetes: blue-green i canary, que veuràs a 11-04, es construeixen combinant aquests maons amb Serveis i Ingress.

RollingUpdate (per defecte)

Substitueix els pods de forma progressiva: aixeca pods de la versió nova i retira els de la vella a poc a poc, mantenint sempre un nombre mínim de pods servint.

flowchart LR
    subgraph T0["Inici"]
        A1["v1"]; A2["v1"]; A3["v1"]; A4["v1"]
    end
    subgraph T1["A mitges"]
        B1["v1"]; B2["v1"]; B3["v2"]; B4["v2"]
    end
    subgraph T2["Final"]
        C1["v2"]; C2["v2"]; C3["v2"]; C4["v2"]
    end
    T0 --> T1 --> T2

Recreate

Mata tots els pods vells, espera que desapareguin, i només llavors crea els nous. Hi ha un interval, normalment de segons a un parell de minuts, sense cap pod servint.

flowchart LR
    subgraph R0["Inici"]
        D1["v1"]; D2["v1"]
    end
    subgraph R1["TALL DE SERVEI"]
        X["Cap pod"]
    end
    subgraph R2["Final"]
        E1["v2"]; E2["v2"]
    end
    R0 --> R1 --> R2

Comparativa

Aspecte RollingUpdate Recreate
Tall de servei No , mentre dura la substitució
Conviuen dues versions Sí, és inevitable No, mai
Capacitat extra necessària Sí, si maxSurge > 0 No
Velocitat Més lenta (progressiva) Més ràpida
Consum de recursos al pic Fins a replicas + maxSurge Mai no supera replicas
Rollback Progressiu, també sense tall Amb un altre tall
Quan fer-la servir Càrregues sense estat: botiga-web, api-reserves, worker-notificacions Quan dues versions no poden coexistir

Com es declara explícitament:

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
spec:
  strategy:
    type: Recreate      # sense subcamps: Recreate no admet maxSurge ni maxUnavailable

  1. Per què postgres-reserves no admet RollingUpdate

És la pregunta clau d'aquest apartat, i la resposta ensenya més que l'estratègia en si. Imagina que postgres-reserves fos un Deployment d'1 rèplica amb RollingUpdate i maxSurge: 1. Durant l'actualització, això és el que passaria:

  1. Kubernetes crea el pod nou abans de retirar el vell, perquè maxSurge: 1 ho permet.
  2. Durant uns segons hi ha dos processos PostgreSQL simultanis.
  3. Tots dos intenten muntar i escriure al mateix directori de dades.

Conseqüències reals, en ordre de gravetat:

  • Bloqueig de l'arrencada: PostgreSQL detecta el fitxer postmaster.pid de la instància viva i es nega a arrencar. El desplegament s'encalla (el cas benigne).
  • Corrupció de dades si el bloqueig falla o l'emmagatzematge no ho garanteix: dos processos escrivint als mateixos fitxers WAL.
  • Escriptures perdudes: reserves confirmades al client que no queden registrades.

I hi ha un segon motiu, independent de l'emmagatzematge: la migració d'esquema. Si la versió nova d'api-reserves requereix una columna que la vella no coneix, tenir les dues versions parlant alhora amb la mateixa base de dades trenca una de les dues. Amb RollingUpdate aquella coexistència dura minuts; amb Recreate, no existeix.

Component Estratègia Motiu
botiga-web RollingUpdate Sense estat; dues versions d'una SPA conviuen sense problema
api-reserves RollingUpdate Sense estat; el contracte de l'API es manté compatible entre versions consecutives
worker-notificacions RollingUpdate Sense estat; consumeix d'una cua, dues versions poden consumir alhora
redis-cache Recreate Una sola rèplica amb un volum; a més la memòria cau es repobla sola
postgres-reserves Ni l'una ni l'altra: StatefulSet Necessita identitat i emmagatzematge estables (06-01)

La conclusió honesta: per a postgres-reserves, la resposta correcta no és Recreate, és no fer servir un Deployment. Recreate és l'estratègia adequada per a càrregues d'una sola rèplica que no toleren duplicitat però tampoc no necessiten identitat estable, com el nostre redis-cache.

Aplica-ho a redis-cache, que ja vas desplegar a la lliçó anterior:

kubectl patch deployment redis-cache --type=merge \
  -p '{"spec":{"strategy":{"type":"Recreate","rollingUpdate":null}}}'
kubectl get deploy redis-cache -o jsonpath='{.spec.strategy.type}{"\n"}'
deployment.apps/redis-cache patched
Recreate

Recorda portar també aquell canvi al manifest a Git.

  1. maxSurge i maxUnavailable, pas a pas amb api-reserves

Aquests dos paràmetres governen completament el ritme d'una actualització progressiva.

Paràmetre Què limita Valor per defecte Efecte d'apujar-lo
maxSurge Quants pods de més hi pot haver per sobre de replicas 25 % Actualització més ràpida, més recursos al pic
maxUnavailable Quants pods poden estar no disponibles per sota de replicas 25 % Actualització més ràpida, menys capacitat durant el procés

Tots dos admeten un nombre enter o un percentatge. Els percentatges s'arrodoneixen així: maxSurge cap amunt i maxUnavailable cap avall, per pecar sempre per excés de capacitat. I hi ha una restricció: no poden valer 0 tots dos alhora, perquè aleshores el desplegament no podria fer ni un pas.

L'escenari

api-reserves amb 4 rèpliques, maxSurge: 1 i maxUnavailable: 1. Actualitzem de la versió 2.4.0 a la 2.5.0. Els números que regeixen tot el procés:

  • Màxim de pods simultanis = replicas + maxSurge = 4 + 1 = 5
  • Mínim de pods disponibles = replicas - maxUnavailable = 4 − 1 = 3

Prepara l'escenari:

kubectl scale deployment api-reserves --replicas=4
kubectl patch deployment api-reserves --type=merge \
  -p '{"spec":{"strategy":{"type":"RollingUpdate","rollingUpdate":{"maxSurge":1,"maxUnavailable":1}}}}'
kubectl get deploy api-reserves
NAME           READY   UP-TO-DATE   AVAILABLE   AGE
api-reserves   4/4     4            4           41m

El recompte exacte, pas a pas

Pas Acció del controlador Pods v2.4.0 Pods v2.5.0 Total Disponibles Respecta els límits?
0 Estat inicial 4 llestos 0 4 4
1 Crea 1 pod nou (fa servir el maxSurge) 4 llestos 1 creant-se 5 4 Total = 5 = màxim
2 Retira 1 pod vell (fa servir el maxUnavailable) 3 llestos 1 creant-se 4 3 Disponibles = 3 = mínim
3 El pod nou passa a Ready 3 llestos 1 llest 4 4
4 Crea un altre pod nou 3 llestos 1 llest + 1 creant-se 5 4 Total = 5
5 Retira un altre de vell 2 llestos 1 llest + 1 creant-se 4 3 Disponibles = 3
6 El segon nou passa a Ready 2 llestos 2 llestos 4 4
7 Crea el tercer 2 llestos 2 llestos + 1 creant-se 5 4 Total = 5
8 Retira el tercer vell 1 llest 2 llestos + 1 creant-se 4 3 Disponibles = 3
9 El tercer passa a Ready 1 llest 3 llestos 4 4
10 Crea el quart 1 llest 3 llestos + 1 creant-se 5 4 Total = 5
11 Retira l'últim vell 0 3 llestos + 1 creant-se 4 3 Disponibles = 3
12 El quart passa a Ready. Final 0 4 llestos 4 4 Completat

Les dues lectures importants d'aquella taula:

  • En cap moment no hi ha menys de 3 pods servint. Rutas Norte no perd mai capacitat total, només el 25 % durant uns segons.
  • En cap moment no hi ha més de 5 pods. El clúster necessita lloc per a un pod extra, no per a vuit.
flowchart TD
    P0["Pas 0<br/>4 vells · 0 nous<br/>total 4"] --> P1["Pas 1<br/>4 vells · 1 nou<br/>total 5 ← sostre de maxSurge"]
    P1 --> P2["Pas 2<br/>3 vells · 1 nou<br/>disponibles 3 ← terra de maxUnavailable"]
    P2 --> P3["Pas 3<br/>el nou passa a Ready<br/>disponibles 4"]
    P3 --> PN["Es repeteix el cicle<br/>crear · retirar · esperar Ready"]
    PN --> PF["Pas 12<br/>0 vells · 4 nous<br/>desplegament completat"]

Veure-ho en directe

En una terminal:

kubectl get pods -l app=api-reserves -w

En una altra, llança l'actualització:

kubectl set image deployment/api-reserves api=node:20.15-alpine
NAME                     READY   STATUS              RESTARTS   AGE
api-reserves-6b4c9d7f5-x2jkp   1/1   Running             0        41m
api-reserves-6b4c9d7f5-k7wpd   1/1   Running             0        41m
api-reserves-6b4c9d7f5-m4rzt   1/1   Running             0        41m
api-reserves-6b4c9d7f5-q8vnc   1/1   Running             0        41m
api-reserves-9f2a7c531-t3xkb   0/1   Pending             0        0s
api-reserves-9f2a7c531-t3xkb   0/1   ContainerCreating   0        0s
api-reserves-6b4c9d7f5-x2jkp   1/1   Terminating         0        41m
api-reserves-9f2a7c531-t3xkb   1/1   Running             0        3s
api-reserves-9f2a7c531-w9djm   0/1   ContainerCreating   0        0s
api-reserves-6b4c9d7f5-k7wpd   1/1   Terminating         0        41m
...

I el recompte de rèpliques de cada ReplicaSet durant el procés:

kubectl get rs -l app=api-reserves
NAME                     DESIRED   CURRENT   READY   AGE
api-reserves-6b4c9d7f5   2         2         2       42m
api-reserves-9f2a7c531   3         3         2       25s

El Deployment està traspassant rèpliques d'un ReplicaSet a un altre. Això és, literalment, tot el que fa un desplegament progressiu.

Combinacions habituals

maxSurge maxUnavailable Comportament Quan fer-la servir
25% 25% Per defecte: equilibri entre velocitat i seguretat La majoria dels casos
1 0 Mai no baixa de la capacitat nominal. Primer aixeca, després retira Producció amb capacitat crítica: api-reserves a rutas-norte-pro
0 1 Mai no supera la capacitat nominal. Primer retira, després aixeca Clústers amb recursos molt justos o quotes ajustades
100% 0 Aixeca tots els nous alhora i després retira tots els vells Desplegament molt ràpid, exigeix el doble de recursos
0 0 Invàlid: l'API ho rebutja

Per a producció de Rutas Norte, l'elecció recomanada és maxSurge: 1 i maxUnavailable: 0: en un pont amb el trànsit multiplicat per sis, perdre un 25 % de capacitat durant un desplegament no és acceptable.

  1. Ritme i tolerància: minReadySeconds, progressDeadlineSeconds, revisionHistoryLimit

Tres camps que completen el control del desplegament.

Camp Per defecte Què fa Per què importa
minReadySeconds 0 Segons que un pod ha de portar llest sense caure abans de comptar-se com a disponible Evita que el desplegament avanci sobre pods que arrenquen i moren als 5 segons
progressDeadlineSeconds 600 Segons sense progrés després dels quals el desplegament es declara fallit Converteix un encallament silenciós en una condició Progressing: False detectable
revisionHistoryLimit 10 Quants ReplicaSets antics (a 0 rèpliques) es conserven És la teva capacitat de rollback; a 0, no pots desfer res

minReadySeconds mereix una explicació a part perquè el seu efecte és subtil però molt valuós. Amb el valor per defecte de 0, tan bon punt un pod nou diu "estic llest", el controlador retira un altre de vell i tira endavant. Si aquell pod cau dos segons després, el dany ja està fet: el desplegament avança sobre una versió trencada. Amb minReadySeconds: 15, cada pod nou ha de sobreviure 15 segons llest abans de comptar. Si cau abans, el desplegament es frena.

progressDeadlineSeconds és la diferència entre un desplegament que falla en silenci i un que avisa. Sense ell, un desplegament encallat per una imatge inexistent es quedaria intentant-ho eternament sense que cap sistema d'alertes se n'assabentés.

Manifest d'api-reserves amb els tres camps i l'estratègia de producció:

# k8s/base/api-reserves-deployment.yaml (fragment actualitzat)
spec:
  replicas: 4
  revisionHistoryLimit: 5
  minReadySeconds: 15
  progressDeadlineSeconds: 300
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0

Amb aquesta configuració, una actualització d'api-reserves a rutas-norte-pro triga com a mínim 4 × 15 = 60 segons, i si alguna cosa falla es declara fallida als 5 minuts en lloc d'als 10. Lentitud a canvi de seguretat: en un component que gestiona pagaments de bitllets, és un canvi excel·lent.

  1. Què dispara una revisió nova i què no

Aquesta és la regla més important de la lliçó i la més oblidada:

Només els canvis sota spec.template disparen una revisió nova. Tota la resta, no.

La lògica és transparent: una revisió és un ReplicaSet, i un ReplicaSet es distingeix d'un altre pel hash de la seva plantilla de pod. Si la plantilla no canvia, el hash no canvia, i no hi ha cap ReplicaSet nou a crear.

Canvi Crea revisió? Què passa
spec.template.spec.containers[].image ReplicaSet nou; substitució progressiva de pods
spec.template.spec.containers[].env Ídem: els pods es recreen amb les variables noves
spec.template.spec.containers[].resources Ídem
spec.template.metadata.labels o annotations Ídem, encara que el contenidor sigui idèntic
spec.template.spec.terminationGracePeriodSeconds Ídem
spec.replicas No El ReplicaSet actual apuja o abaixa el seu nombre de pods
spec.strategy No S'aplicarà a la pròxima actualització
spec.minReadySeconds No Ídem
spec.revisionHistoryLimit No Només neteja ReplicaSets antics
metadata.annotations del Deployment No Inclosa change-cause: és metadada del Deployment, no del pod

Comprova-ho tu mateix:

kubectl rollout history deployment/api-reserves | tail -3
kubectl scale deployment api-reserves --replicas=5
kubectl rollout history deployment/api-reserves | tail -3
REVISION  CHANGE-CAUSE
1         Desplegament inicial d'api-reserves 2.4.0
2         <none>

deployment.apps/api-reserves scaled

REVISION  CHANGE-CAUSE
1         Desplegament inicial d'api-reserves 2.4.0
2         <none>

Mateix historial: escalar no és desplegar.

Una conseqüència pràctica que sorprèn molta gent: canviar un ConfigMap no reinicia els pods que el consumeixen, perquè el ConfigMap no és al template, només s'hi referencia des d'ell. El truc habitual, que veuràs al mòdul 3, és forçar un canvi al template amb una anotació calculada, o fer servir directament:

kubectl rollout restart deployment/api-reserves

rollout restart no canvia res funcional: escriu una anotació kubectl.kubernetes.io/restartedAt dins del template, cosa que altera el hash i provoca una substitució progressiva de tots els pods. És la manera correcta i sense tall de "reiniciar" un Deployment.

  1. El joc complet de kubectl rollout

kubectl set image: actualitzar la imatge

kubectl set image deployment/api-reserves api=node:20.16-alpine
deployment.apps/api-reserves image updated

La sintaxi és <nom-del-contenidor>=<imatge>. El nom del contenidor és el de spec.template.spec.containers[].name, no el del pod. Amb diversos contenidors es poden actualitzar alhora separant-los per espais.

Advertiment de disciplina: en un projecte declaratiu, la via correcta és editar el manifest i aplicar-lo. set image és per a emergències, i el seu canvi es perd al següent apply.

kubectl rollout status: esperar

kubectl rollout status deployment/api-reserves
Waiting for deployment "api-reserves" rollout to finish: 1 out of 4 new replicas have been updated...
Waiting for deployment "api-reserves" rollout to finish: 2 out of 4 new replicas have been updated...
Waiting for deployment "api-reserves" rollout to finish: 3 out of 4 new replicas have been updated...
Waiting for deployment "api-reserves" rollout to finish: 1 old replicas are pending termination...
deployment "api-reserves" successfully rolled out

kubectl rollout history: l'historial

kubectl rollout history deployment/api-reserves
deployment.apps/api-reserves
REVISION  CHANGE-CAUSE
1         Desplegament inicial d'api-reserves 2.4.0
2         Actualitzacio a node 20.15 per correccio de seguretat
3         Actualitzacio a node 20.16

I el detall complet d'una revisió concreta:

kubectl rollout history deployment/api-reserves --revision=2
deployment.apps/api-reserves with revision #2
Pod Template:
  Labels:  app=api-reserves
           app.kubernetes.io/part-of=rutas-norte
           entorn=dev
           pod-template-hash=9f2a7c531
  Annotations:  kubernetes.io/change-cause: Actualitzacio a node 20.15 per correccio de seguretat
  Containers:
   api:
    Image:  node:20.15-alpine
    Port:   3000/TCP
    Limits:  cpu: 500m, memory: 256Mi
    Requests: cpu: 100m, memory: 128Mi

Aquella informació surt íntegrament del ReplicaSet a 0 rèpliques que va quedar guardat. Per això revisionHistoryLimit: 0 et deixaria sense historial i sense rollback.

kubectl rollout undo: desfer

kubectl rollout undo deployment/api-reserves
deployment.apps/api-reserves rolled back

Torna a la revisió immediatament anterior. Per anar a una de concreta:

kubectl rollout undo deployment/api-reserves --to-revision=1

Dues precisions importants:

  1. Un rollback és un desplegament més: fa servir la mateixa estratègia, respecta maxSurge i maxUnavailable, i per tant tampoc no talla el servei.
  2. Desfer no esborra la revisió dolenta: crea una revisió nova amb el contingut de l'antiga. Si eres a la 3 i fas undo, apareixes a la 4, el contingut de la qual és el de la 2. Els números no retrocedeixen mai.
kubectl rollout history deployment/api-reserves
REVISION  CHANGE-CAUSE
1         Desplegament inicial d'api-reserves 2.4.0
3         Actualitzacio a node 20.16
4         Actualitzacio a node 20.15 per correccio de seguretat

Fixa't que la revisió 2 ha desaparegut de la llista: el seu ReplicaSet s'ha reaprofitat com a revisió 4.

kubectl rollout pause i resume: congelar a mitges

kubectl rollout pause deployment/api-reserves

Amb el Deployment pausat, els canvis que facis no s'apliquen. És l'eina per agrupar diverses modificacions en un sol desplegament:

kubectl rollout pause deployment/api-reserves
kubectl set image deployment/api-reserves api=node:20.17-alpine
kubectl set resources deployment/api-reserves -c=api --limits=cpu=600m,memory=384Mi
kubectl scale deployment api-reserves --replicas=5
# fins aqui no ha passat RES al cluster
kubectl rollout resume deployment/api-reserves
kubectl rollout status deployment/api-reserves
deployment.apps/api-reserves paused
deployment.apps/api-reserves image updated
deployment.apps/api-reserves resource requirements updated
deployment.apps/api-reserves scaled
deployment.apps/api-reserves resumed
Waiting for deployment "api-reserves" rollout to finish: 3 of 5 updated replicas are available...
deployment "api-reserves" successfully rolled out

Sense pause, aquelles tres ordres haurien provocat tres desplegaments encadenats, cadascun interrompent l'anterior. Amb pause, un de sol i una sola revisió.

L'altre ús de pause és d'emergència: si veus que un desplegament està traient pods trencats, kubectl rollout pause el congela a l'acte, deixant-te els pods vells que queden servint mentre decideixes si arreglar o desfer.

Taula resum del joc complet:

Ordre Per a què
rollout status Esperar i verificar; retorna codi != 0 si falla
rollout history Veure revisions; amb --revision=N, el detall
rollout undo Tornar enrere, amb o sense --to-revision
rollout pause / resume Congelar i reprendre; agrupar canvis
rollout restart Recrear tots els pods sense canviar res

  1. L'anotació kubernetes.io/change-cause

Hauràs notat els <none> a la columna CHANGE-CAUSE. Un historial de revisions sense motius és gairebé inútil: al cap de tres setmanes ningú no recorda què era la revisió 7.

kubernetes.io/change-cause és una anotació del Deployment el valor de la qual es copia al ReplicaSet d'aquella revisió i es mostra a l'historial. Tres maneres d'omplir-la, de pitjor a millor:

Amb kubectl annotate (ràpida, imperativa):

kubectl annotate deployment/api-reserves \
  kubernetes.io/change-cause="Actualitzacio a node 20.17 i pujada de limits pels pics d'agost" \
  --overwrite

Al manifest (la correcta segons les convencions del projecte):

metadata:
  name: api-reserves
  annotations:
    kubernetes.io/change-cause: "v2.5.0 - cache de disponibilitat per expedicio (RN-482)"

Automatitzada a CI/CD, que és l'ideal:

kubectl annotate deployment/api-reserves \
  kubernetes.io/change-cause="${CI_COMMIT_SHA} · ${CI_COMMIT_MESSAGE} · ${CI_USER}" \
  --overwrite

Un detall que ja coneixes per l'apartat 5, però que convé repetir perquè genera confusió: canviar només aquesta anotació no dispara una revisió nova, perquè viu al metadata del Deployment i no al template. Anota-la juntament amb el canvi real, no després.

Bones pràctiques per al text de Rutas Norte: versió de la imatge, resum del canvi en llenguatge planer i referència al tiquet. Exemple real del projecte:

REVISION  CHANGE-CAUSE
1         v2.4.0 - desplegament inicial
2         v2.4.1 - correccio de calcul de places lliures (RN-455)
3         v2.5.0 - cache de disponibilitat per expedicio (RN-482)
4         v2.4.1 - ROLLBACK: la 2.5.0 retornava 500 en reservar (RN-489)

  1. Desplegament sense tall: què cal de debò

Aquí arriba l'advertiment més important de la lliçó. Podries pensar que amb RollingUpdate ben configurat ja tens desplegaments sense tall de servei. No és així, i aquesta és la causa número u d'errors 502 durant desplegaments suposadament perfectes.

El problema és que, amb el que sabem fins ara, Kubernetes considera que un pod està "llest" tan bon punt el procés arrenca. Però un procés de Node.js triga uns segons a carregar dependències, connectar a PostgreSQL i quedar operatiu. Durant aquella finestra:

  1. El pod ja compta com a Ready.
  2. El Deployment retira un pod vell, confiat.
  3. El Service comença a enviar-li trànsit real.
  4. El pod nou encara no pot respondre: errors 502.
flowchart TD
    A["El pod nou arrenca"] --> B["Kubernetes el marca Ready<br/>només perquè el procés viu"]
    B --> C["El Service li envia transit"]
    C --> D{"L'aplicacio esta<br/>realment operativa?"}
    D -->|Sí| E["Tot correcte"]
    D -->|No: encara connectant a PostgreSQL| F["502 a clients reals<br/>durant diversos segons"]

La peça que falta s'anomena sonda de disponibilitat (readinessProbe): una comprovació que el kubelet executa contra la teva aplicació i que decideix si el pod està realment llest per rebre trànsit. Amb ella, el pas 1 de l'esquema no passa fins que l'aplicació respon de debò.

La llista de comprovació completa d'un desplegament realment sense tall a Rutas Norte:

Requisit Estat en aquest moment On es resol
Estratègia RollingUpdate ben parametritzada Fet en aquesta lliçó 02-04
Més d'una rèplica Fet 02-03
minReadySeconds com a coixí Fet 02-04
readinessProbe que reflecteixi la disponibilitat real Pendent 07-01
livenessProbe per detectar pods penjats Pendent 07-01
Aturada ordenada que capturi SIGTERM Fet 02-01
Un Service que enruti als pods llestos Pendent 02-05
Compatibilitat entre versions consecutives de l'API Responsabilitat del desenvolupament

És a dir: RollingUpdate és necessari però no suficient. Sense sondes, el desplegament progressiu avança a cegues. Fins que arribem al mòdul 7, minReadySeconds és un substitut pobre però útil.

  1. Diagnòstic i rollback d'un desplegament encallat

Exercici final i el més realista de tots. Un divendres a la tarda, algú desplega a rutas-norte-dev una versió que no existeix al registre.

El desastre

kubectl annotate deployment/api-reserves \
  kubernetes.io/change-cause="v2.6.0 - versio que no existeix (RN-501)" --overwrite
kubectl set image deployment/api-reserves api=node:99.99-inexistent
kubectl rollout status deployment/api-reserves --timeout=90s
deployment.apps/api-reserves annotated
deployment.apps/api-reserves image updated
Waiting for deployment "api-reserves" rollout to finish: 1 out of 5 new replicas have been updated...
error: timed out waiting for the condition

Pas 1: l'estat general

kubectl get deploy api-reserves
NAME           READY   UP-TO-DATE   AVAILABLE   AGE
api-reserves   5/5     1            5           1h

Lectura: hi ha 5 pods llestos i disponibles —el servei continua funcionant amb la versió anterior— però només 1 té la versió nova. El desplegament ha fet un pas i s'ha aturat. Això és el sistema funcionant bé: maxUnavailable: 0 ha impedit que es retirés cap pod vell fins que el nou estigués llest, i com que no ho estarà mai, no se n'ha retirat cap.

Pas 2: els ReplicaSets

kubectl get rs -l app=api-reserves
NAME                     DESIRED   CURRENT   READY   AGE
api-reserves-3d8e1f602   1         1         0       2m
api-reserves-c5a9b47d8   5         5         5       18m

El ReplicaSet nou té 1 pod que no arriba a READY.

Pas 3: el pod culpable

kubectl get pods -l app=api-reserves | grep -v Running
NAME                           READY   STATUS             RESTARTS   AGE
api-reserves-3d8e1f602-h4tqm   0/1     ImagePullBackOff   0          2m
kubectl describe pod api-reserves-3d8e1f602-h4tqm | tail -6
Events:
  Type     Reason     Age                 From               Message
  ----     ------     ----                ----               -------
  Normal   Pulling    2m                  kubelet            Pulling image "node:99.99-inexistent"
  Warning  Failed     2m                  kubelet            Failed to pull image: manifest for node:99.99-inexistent not found
  Warning  Failed     2m                  kubelet            Error: ErrImagePull
  Normal   BackOff    30s (x6 over 2m)    kubelet            Back-off pulling image "node:99.99-inexistent"

Causa arrel identificada, exactament amb el procediment de la lliçó de Pods.

Pas 4: les conditions

kubectl describe deployment api-reserves | grep -A4 Conditions
Conditions:
  Type           Status  Reason
  ----           ------  ------
  Available      True    MinimumReplicasAvailable
  Progressing    False   ProgressDeadlineExceeded

Available: True (continuem servint) + Progressing: False amb ProgressDeadlineExceeded (el desplegament és mort) és la signatura exacta d'un desplegament encallat. En un clúster monitorat, aquella combinació ha de generar una alerta.

Pas 5: decidir i actuar

Dues opcions, segons el moment:

# Opcio A: congelar mentre s'investiga (no reverteix, nomes atura)
kubectl rollout pause deployment/api-reserves

# Opcio B: desfer ja (el normal un divendres a la tarda)
kubectl rollout undo deployment/api-reserves
kubectl rollout status deployment/api-reserves
deployment.apps/api-reserves rolled back
deployment "api-reserves" successfully rolled out

Advertiment important: si el Deployment està pausat, undo no té cap efecte. Cal fer resume abans.

Pas 6: verificar i documentar

kubectl get deploy,rs -l app=api-reserves
kubectl annotate deployment/api-reserves \
  kubernetes.io/change-cause="ROLLBACK a v2.5.0: la imatge de v2.6.0 no existeix al registre (RN-501)" \
  --overwrite
kubectl rollout history deployment/api-reserves
NAME                           READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/api-reserves   5/5     5            5           1h

NAME                                     DESIRED   CURRENT   READY   AGE
replicaset.apps/api-reserves-3d8e1f602   0         0         0       6m
replicaset.apps/api-reserves-c5a9b47d8   5         5         5       22m

REVISION  CHANGE-CAUSE
1         Desplegament inicial d'api-reserves 2.4.0
4         v2.6.0 - versio que no existeix (RN-501)
5         ROLLBACK a v2.5.0: la imatge de v2.6.0 no existeix al registre (RN-501)

El ReplicaSet trencat es queda a 0 rèpliques, sense consumir res, com a testimoni de l'incident. I l'historial explica la història completa a qui la llegeixi d'aquí a sis mesos.

La moralitat de tot l'apartat: durant els sis passos anteriors, api-reserves no va deixar d'atendre ni una sola reserva. Amb el Docker Compose de partida, aquell mateix error hauria deixat la plataforma caiguda fins que algú se n'adonés. Això és el que compra un Deployment ben configurat.

Deixa el clúster endreçat abans de continuar:

kubectl scale deployment api-reserves --replicas=2
kubectl get deploy
NAME                   READY   UP-TO-DATE   AVAILABLE   AGE
api-reserves           2/2     2            2           1h
botiga-web             3/3     3            3           1h
redis-cache            1/1     1            1           50m
worker-notificacions   2/2     2            2           45m

Errors Comuns i Consells

  • Creure que RollingUpdate garanteix zero talls per si sol. Sense readinessProbe, el trànsit arriba a pods que encara no poden respondre. És la causa número u de 502 durant desplegaments.
  • Posar maxSurge: 0 i maxUnavailable: 0. L'API ho rebutja: el desplegament no podria avançar.
  • Fer servir RollingUpdate amb una càrrega que no tolera dues versions simultànies. Bases de dades d'una rèplica, processos amb bloqueig exclusiu o migracions d'esquema incompatibles: allà hi va Recreate, o directament un StatefulSet.
  • Posar revisionHistoryLimit: 0 per "estalviar". Els ReplicaSets a 0 rèpliques no consumeixen ni CPU ni memòria, només uns quilobytes a etcd. A canvi, perds completament la capacitat de desfer.
  • Esperar que canviar spec.strategy o spec.replicas dispari un desplegament. No són al template. Només s'apliquen a l'actualització real següent.
  • Esperar que modificar un ConfigMap reiniciï els pods. No ho fa. Fes servir kubectl rollout restart.
  • Oblidar el change-cause. Un historial de <none> no serveix de res quan cal decidir a quina revisió tornar a les tres de la matinada.
  • Anotar el change-cause després del desplegament. Com que no dispara revisió, queda associat al Deployment però pot no reflectir el que va fer aquella revisió. Anota'l al mateix apply.
  • Fer undo sobre un Deployment pausat. No passa res fins que facis resume. És un clàssic de la depuració a cegues.
  • Confiar en kubectl apply com a verificació. apply només diu que l'API ha acceptat l'objecte. Encadena sempre kubectl rollout status --timeout=... als teus scripts.
  • Consell: en producció, maxSurge: 1 i maxUnavailable: 0. Costa un pod extra de capacitat i garanteix que mai no baixes de la capacitat nominal.
  • Consell: kubectl rollout status retorna codi de sortida diferent de 0 si el desplegament falla. És el que converteix una canalització de CI/CD en una cosa fiable: kubectl apply -f k8s/ && kubectl rollout status deploy/api-reserves --timeout=300s || kubectl rollout undo deploy/api-reserves.

Exercicis

Exercici 1: Calcular un desplegament sobre el paper

botiga-web és a rutas-norte-pro amb 6 rèpliques, maxSurge: 2 i maxUnavailable: 1.

  1. Quin és el nombre màxim de pods simultanis i el mínim de pods disponibles?
  2. Construeix una taula amb els primers cinc passos de l'actualització, indicant a cadascun: pods vells, pods nous, total i disponibles.
  3. Si l'equip de plataforma exigeix que mai no es baixi de 6 pods servint, quins valors hi posaries i quins recursos extra necessita el clúster?
  4. Amb la configuració del punt 3 i minReadySeconds: 20, quina és la durada mínima teòrica del desplegament si cada pod triga 5 segons a arrencar?

Exercici 2: Cicle complet d'actualització i rollback

Sobre botiga-web a rutas-norte-dev:

  1. Configura RollingUpdate amb maxSurge: 1, maxUnavailable: 0, minReadySeconds: 10 i revisionHistoryLimit: 5, aplicant-ho des del manifest.
  2. Actualitza a nginx:1.27.1-alpine amb el seu change-cause corresponent i espera que acabi.
  3. Actualitza a nginx:1.26-alpine amb un altre change-cause.
  4. Mostra l'historial i el detall de la revisió 2.
  5. Simula que la 1.26 té una fallada greu: torna a la 1.27.1 i verifica que les tres rèpliques la serveixen.
  6. Explica per què l'historial no mostra la numeració que esperaries.

Exercici 3: Diagnosticar un desplegament encallat a worker-notificacions

  1. Trenca worker-notificacions a propòsit desplegant la imatge busybox:9.9.9-inexistent, amb change-cause inclòs.
  2. Amb quatre ordres, diagnostica el problema seguint l'ordre correcte: estat del Deployment, ReplicaSets, pod culpable i conditions.
  3. Respon: continuen enviant-se els correus de confirmació durant l'encallament? Justifica-ho amb la sortida de les ordres.
  4. Congela el desplegament, comprova que undo no fa res mentre està pausat, reprèn-lo i desfés.
  5. Documenta el rollback amb una anotació i mostra l'historial final.

Solucions

Solució 1

  1. Màxim de pods = replicas + maxSurge = 6 + 2 = 8. Mínim disponibles = replicas - maxUnavailable = 6 − 1 = 5.

  2. Primers cinc passos:

Pas Acció Vells Nous Total Disponibles
0 Estat inicial 6 llestos 0 6 6
1 Crea 2 pods nous (sostre de maxSurge) 6 llestos 2 creant-se 8 6
2 Retira 1 de vell (sostre de maxUnavailable) 5 llestos 2 creant-se 7 5
3 Els 2 nous passen a Ready 5 llestos 2 llestos 7 7
4 Crea 1 nou més (7 + 1 = 8, sostre) 5 llestos 2 llestos + 1 creant-se 8 7
5 Retira 2 de vells (disponibles 7 − 2 = 5, terra) 3 llestos 2 llestos + 1 creant-se 6 5
  1. Per no baixar mai de 6 servint: maxUnavailable: 0, i maxSurge a 1 o 2 segons com de ràpid es vulgui anar. Amb maxSurge: 2, el clúster ha de tenir lloc per a 8 pods de botiga-web alhora: amb requests de 50m de CPU i 64Mi per pod, això són 100m de CPU i 128Mi de memòria lliures de més durant el desplegament. Amb maxUnavailable: 0 el clúster ha de tenir aquella capacitat; si no la té, els pods nous es queden en Pending i el desplegament no avança mai.

  2. Amb maxSurge: 2 i maxUnavailable: 0, els pods se substitueixen en tandes de 2. Cada tanda triga 5 s (arrencada) + 20 s (minReadySeconds) = 25 s. Sis pods en tandes de dos són 3 tandes: 75 segons com a mínim teòric, sense comptar descàrregues d'imatge ni el temps de terminació dels vells.

Solució 2

# k8s/base/botiga-web-deployment.yaml (fragment)
spec:
  replicas: 3
  revisionHistoryLimit: 5
  minReadySeconds: 10
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
kubectl apply -f k8s/base/botiga-web-deployment.yaml

# 2. Actualitzacio a 1.27.1
kubectl annotate deployment/botiga-web \
  kubernetes.io/change-cause="v1.27.1 - correccio de seguretat de nginx (RN-470)" --overwrite
kubectl set image deployment/botiga-web nginx=nginx:1.27.1-alpine
kubectl rollout status deployment/botiga-web

# 3. Actualitzacio a 1.26
kubectl annotate deployment/botiga-web \
  kubernetes.io/change-cause="v1.26 - tornada a branca anterior per compatibilitat (RN-473)" --overwrite
kubectl set image deployment/botiga-web nginx=nginx:1.26-alpine
kubectl rollout status deployment/botiga-web
deployment "botiga-web" successfully rolled out
# 4. Historial i detall
kubectl rollout history deployment/botiga-web
kubectl rollout history deployment/botiga-web --revision=2
deployment.apps/botiga-web
REVISION  CHANGE-CAUSE
1         Desplegament inicial de botiga-web 1.27.0
2         v1.27.1 - correccio de seguretat de nginx (RN-470)
3         v1.26 - tornada a branca anterior per compatibilitat (RN-473)

deployment.apps/botiga-web with revision #2
Pod Template:
  Labels:  app=botiga-web
           pod-template-hash=6f9c4b8d7
  Annotations:  kubernetes.io/change-cause: v1.27.1 - correccio de seguretat de nginx (RN-470)
  Containers:
   nginx:
    Image:  nginx:1.27.1-alpine
# 5. Rollback a la revisio 2
kubectl rollout undo deployment/botiga-web --to-revision=2
kubectl rollout status deployment/botiga-web
kubectl get pods -l app=botiga-web \
  -o custom-columns=NOM:.metadata.name,IMATGE:.spec.containers[0].image
deployment.apps/botiga-web rolled back
deployment "botiga-web" successfully rolled out

NOM                           IMATGE
botiga-web-6f9c4b8d7-42kxr    nginx:1.27.1-alpine
botiga-web-6f9c4b8d7-8vnwq    nginx:1.27.1-alpine
botiga-web-6f9c4b8d7-t9mzd    nginx:1.27.1-alpine
# 6. Historial final
kubectl rollout history deployment/botiga-web
REVISION  CHANGE-CAUSE
1         Desplegament inicial de botiga-web 1.27.0
3         v1.26 - tornada a branca anterior per compatibilitat (RN-473)
4         v1.27.1 - correccio de seguretat de nginx (RN-470)
  1. La numeració sorprèn perquè les revisions no retrocedeixen mai. En desfer cap a la revisió 2, Kubernetes no hi "torna": reaprofita el seu ReplicaSet i el renumera com a revisió 4, la més recent. Per això la 2 desapareix de la llista i n'apareix una 4 amb el seu mateix change-cause. El número de revisió identifica l'ordre en què es van aplicar els canvis, no el contingut: un rollback és un canvi més.

Solució 3

# 1. Trencar-lo
kubectl annotate deployment/worker-notificacions \
  kubernetes.io/change-cause="v1.3.0 - imatge que no existeix (RN-512)" --overwrite
kubectl set image deployment/worker-notificacions worker=busybox:9.9.9-inexistent
# 2. Diagnostic en quatre passos
kubectl get deploy worker-notificacions
kubectl get rs -l app=worker-notificacions
kubectl get pods -l app=worker-notificacions
kubectl describe pod <pod-en-ImagePullBackOff> | tail -6
kubectl describe deployment worker-notificacions | grep -A4 Conditions
NAME                   READY   UP-TO-DATE   AVAILABLE   AGE
worker-notificacions   2/2     1            2           1h

NAME                              DESIRED   CURRENT   READY   AGE
worker-notificacions-5a3f9d21c    1         1         0       90s
worker-notificacions-8c7b5d94f    2         2         2       1h

NAME                                    READY   STATUS             RESTARTS   AGE
worker-notificacions-5a3f9d21c-r6bkt    0/1     ImagePullBackOff   0          90s
worker-notificacions-8c7b5d94f-k2xrt    1/1     Running            0          1h
worker-notificacions-8c7b5d94f-p9mzq    1/1     Running            0          1h

  Warning  Failed   80s   kubelet   Failed to pull image "busybox:9.9.9-inexistent": manifest unknown

Conditions:
  Type           Status  Reason
  ----           ------  ------
  Available      True    MinimumReplicasAvailable
  Progressing    False   ProgressDeadlineExceeded
  1. Sí, els correus se segueixen enviant. La prova és a tres llocs: READY 2/2 i AVAILABLE 2 al Deployment, els 2 pods de la revisió anterior en Running, i sobretot Available: True amb MinimumReplicasAvailable a les conditions. Només UP-TO-DATE 1 delata que hi ha una versió nova intentant entrar sense aconseguir-ho. Confirmació directa:
kubectl logs -l app=worker-notificacions --tail=1 --prefix | grep enviant
[pod/worker-notificacions-8c7b5d94f-k2xrt/worker] enviant lot de confirmacions des de worker-notificacions-8c7b5d94f-k2xrt
# 4. Pausar, comprovar que undo no fa res, reprendre i desfer
kubectl rollout pause deployment/worker-notificacions
kubectl rollout undo deployment/worker-notificacions
kubectl get rs -l app=worker-notificacions
deployment.apps/worker-notificacions paused
deployment.apps/worker-notificacions rolled back

NAME                              DESIRED   CURRENT   READY   AGE
worker-notificacions-5a3f9d21c    1         1         0       4m
worker-notificacions-8c7b5d94f    2         2         2       1h

El ReplicaSet trencat continua amb 1 rèplica: l'undo s'ha registrat però no s'ha executat, perquè el Deployment està pausat.

kubectl rollout resume deployment/worker-notificacions
kubectl rollout status deployment/worker-notificacions
kubectl get rs -l app=worker-notificacions
deployment.apps/worker-notificacions resumed
deployment "worker-notificacions" successfully rolled out

NAME                              DESIRED   CURRENT   READY   AGE
worker-notificacions-5a3f9d21c    0         0         0       5m
worker-notificacions-8c7b5d94f    2         2         2       1h
# 5. Documentar
kubectl annotate deployment/worker-notificacions \
  kubernetes.io/change-cause="ROLLBACK a v1.2.0: la imatge de v1.3.0 no existeix al registre (RN-512)" \
  --overwrite
kubectl rollout history deployment/worker-notificacions
REVISION  CHANGE-CAUSE
1         Desplegament inicial de worker-notificacions 1.2.0
2         v1.3.0 - imatge que no existeix (RN-512)
3         ROLLBACK a v1.2.0: la imatge de v1.3.0 no existeix al registre (RN-512)

Conclusió

Has tancat el cercle que vam obrir a la lliçó anterior amb aquell segon ReplicaSet que va aparèixer sense explicació. Ara saps que les dues estratègies natives del Deployment són RollingUpdate i Recreate, i per què l'elecció no és de gust sinó de naturalesa de la càrrega: botiga-web, api-reserves i worker-notificacions toleren dues versions convivint i van amb RollingUpdate; redis-cache no, i va amb Recreate; i postgres-reserves no admet cap de les dues perquè el Deployment no és el seu lloc: dos processos PostgreSQL sobre el mateix directori de dades signifiquen bloqueig en el millor cas i corrupció en el pitjor.

Saps calcular un desplegament sobre el paper abans d'executar-lo: replicas + maxSurge és el sostre de pods i replicas - maxUnavailable el terra de disponibles, i amb aquests dos números pots reconstruir pas a pas el que farà el controlador amb les 4 rèpliques d'api-reserves. Coneixes les combinacions útils i la recomanada per a producció, maxSurge: 1 amb maxUnavailable: 0, que protegeix la capacitat nominal en un pont d'agost. Ajustes el ritme amb minReadySeconds, detectes els encallaments amb progressDeadlineSeconds i conserves la teva capacitat de desfer amb revisionHistoryLimit. Tens clara la regla que ho governa tot: només els canvis sota spec.template creen una revisió, i per això escalar no desplega, canviar un ConfigMap no reinicia res i existeix kubectl rollout restart. I fas servir el joc complet —status, history, undo, pause, resume— amb change-cause documentant cada pas, inclòs el rollback d'un desplegament encallat per una imatge inexistent, resolt sense que un sol client deixés de comprar el seu bitllet.

Queda un advertiment pendent i una mancança grossa. L'advertiment: RollingUpdate és necessari però no suficient per desplegar sense tall real; sense una readinessProbe, Kubernetes marca com a llest un pod el procés del qual acaba d'arrencar i li envia trànsit abans que el pugui atendre. Aquella peça arriba a Verificacions de Salut i Sondes. I la mancança: tot el trànsit del qual portem dues lliçons parlant encara no existeix. Els nostres pods tenen IPs que canvien a cada desplegament, i per parlar amb api-reserves hem hagut de buscar a mà la IP d'un pod concret. A la lliçó següent, Serveis, donarem a cada component de Rutas Norte una adreça estable que sobrevisqui als desplegaments, amb balanceig de càrrega entre rèpliques inclòs.

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