Aurora Libros escala sola i aguanta el pic. Queda el moment més delicat de tots: publicar una versió nova. Entre la vella i la nova sempre hi ha un període en què conviuen, i decidir com es gestiona aquell període és la diferència entre una publicació que ningú no nota i un quart d'hora sense poder comprar un llibre.

Contingut

  1. El problema: la finestra de coexistència
  2. Les cinc estratègies, comparades
  3. Rolling update a Kubernetes: maxSurge i maxUnavailable
  4. update_config a Swarm i la taula d'equivalències
  5. La readiness i minReadySeconds
  6. Veure el desplegament en marxa
  7. Rollback: undo, revisions i revisionHistoryLimit
  8. Rollback a Swarm
  9. Assaig d'un desplegament fallit: aurora-api:2.1.0
  10. Blue-green amb dos Deployments
  11. Canari per rèpliques i per Ingress
  12. Què es vigila durant el canari
  13. Argo Rollouts, Flagger i GitOps
  14. Migracions de base de dades: expand/contract
  15. Feature flags

  1. El problema: la finestra de coexistència

Durant un desplegament progressiu, el trànsit es reparteix entre versions diferents. Això obliga que la versió 2.1.0 sigui compatible amb la 2.0.0 en tres fronts:

Front Què exigeix
Esquema de la base de dades Que totes dues versions puguin llegir i escriure la mateixa taula
Contracte de l'API Que un client que va començar amb la vella pugui acabar amb la nova
Memòria cau compartida Que una entrada escrita per una versió l'entengui l'altra

El tercer és el que més sorprèn i el més fàcil de provocar: si la 2.1.0 canvia el format del que desa a aurora-cache amb la mateixa clau, les rèpliques antigues llegiran objectes que no entenen i retornaran errors intermitents que apareixen i desapareixen segons a quin Pod caigui cada petició. La solució és tan simple com versionar la clau: llibres:v2:*.

  1. Les cinc estratègies, comparades

Estratègia Com Recursos extra Tall Rollback Risc Quan
Recreate Mata tot i arrenca el nou Cap , total Redesplegar (lent) Alt Canvis incompatibles, entorns interns
Rolling update Substitueix de N en N +maxSurge No Rodant cap enrere Mitjà El valor per defecte assenyat
Blue-green Dos entorns complets, es commuta ×2 No Instantani Baix Canvis grans, es pot pagar el doble
Canari Un % del trànsit a la nova +1 rèplica No Ràpid El més baix Canvis sensibles amb mètriques fiables
Shadow Còpia del trànsit real sense retornar resposta ×2 en còmput No No aplica Nul per a l'usuari Validar rendiment abans d'exposar
flowchart TB
    subgraph R[Rolling update]
        R1["v1 v1 v1"] --> R2["v2 v1 v1"] --> R3["v2 v2 v1"] --> R4["v2 v2 v2"]
    end
    subgraph B[Blue-green]
        B1["blau v1 ← 100%"] --> B2["blau v1 + verd v2 (0%)"] --> B3["verd v2 ← 100%"]
    end
    subgraph C[Canari]
        C1["v1 100%"] --> C2["v1 90% / v2 10%"] --> C3["v1 50% / v2 50%"] --> C4["v2 100%"]
    end

El shadow mereix una nota: duplica el trànsit real cap a la versió nova però descarta la seva resposta, així que l'usuari no la veu mai. És magnífic per validar rendiment amb càrrega autèntica, i té un parany perillós: si la petició duplicada escriu a la base de dades o cobra una targeta, l'operació passa dues vegades. Només serveix amb trànsit de lectura o amb dependències aïllades.

  1. Rolling update a Kubernetes: maxSurge i maxUnavailable

spec:
  replicas: 6
  minReadySeconds: 10               # 10 s sa abans de comptar com a disponible
  progressDeadlineSeconds: 300      # si en 5 min no avança, es marca com a fallit
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2                   # fins a 8 Pods alhora (6 + 2)
      maxUnavailable: 0             # mai menys de 6 disponibles
maxSurge maxUnavailable Capacitat durant el desplegament Velocitat Recursos extra
0 1 5 de 6 (83 %) Lenta Cap
1 1 6 de 6 Mitjana +1 Pod
2 0 6 de 6 sempre Mitjana +2 Pods
100% 0 Doble durant la transició Màxima ×2 (blue-green de facto)
25% 25% 75 % mínim Mitjana +25 %

La combinació maxSurge: 2 / maxUnavailable: 0 és la correcta per a Aurora Libros: mai hi ha menys de sis Pods servint, així que la capacitat no baixa ni un instant durant la publicació. Costa dos Pods de més durant uns minuts, que és un preu ridícul. maxUnavailable: 0 és també una assegurança: si els Pods nous no arriben a estar preparats, el desplegament no pot avançar, perquè avançar exigiria treure Pods vells.

Compte amb un cas concret: maxSurge: 0 i maxUnavailable: 0 alhora és una configuració invàlida —el desplegament no podria fer ni un pas—, i Kubernetes la rebutja.

  1. update_config a Swarm i la taula d'equivalències

    deploy:
      replicas: 6
      update_config:
        parallelism: 2          # 2 tasques alhora
        delay: 15s              # espera entre lots
        order: start-first      # arrencar la nova abans d'aturar la vella
        failure_action: rollback
        monitor: 30s            # finestra de vigilància després de cada tasca
        max_failure_ratio: 0.2
      rollback_config:
        parallelism: 3
        order: stop-first
Concepte Kubernetes Swarm
Quantes alhora maxSurge / maxUnavailable parallelism
Pausa entre lots minReadySeconds delay
Ordre maxSurge > 0 = start-first order: start-first / stop-first
Què fer si falla S'atura (amb maxUnavailable: 0) failure_action: rollback
Finestra de vigilància progressDeadlineSeconds monitor
Llindar de fallada Implícit a les sondes max_failure_ratio
Desfer kubectl rollout undo docker service rollback
Historial revisionHistoryLimit Només la versió anterior

Dues diferències importants. Swarm pot desfer sol amb failure_action: rollback, mentre que Kubernetes es limita a aturar-se i esperar que decideixis —cosa defensable: aturar deixa el sistema en un estat conegut i no amaga el problema—. En canvi, Swarm guarda únicament la versió immediatament anterior, mentre que Kubernetes conserva tantes revisions com diguis.

  1. La readiness i minReadySeconds

Aquí és on el que vas fer a 06-01 rendeix de debò. Sense sonda de readiness, un rolling update és una ruleta: Kubernetes considera "preparat" un Pod tan bon punt el seu contenidor arrenca, li envia trànsit i la teva aplicació encara està establint el pool de PostgreSQL. Cada rèplica substituïda produeix uns segons d'errors.

Sense readiness:  el contenidor arrenca → rep trànsit → encara connectant → 502
Amb readiness:    el contenidor arrenca → /salut/preparat 503 → connecta → 200 → rep trànsit

minReadySeconds: 10 hi afegeix una segona assegurança: exigeix que el Pod porti deu segons seguits preparat abans de comptar-lo com a disponible i passar al següent. Sense això, un Pod que passa la readiness i cau dos segons després faria avançar el desplegament igualment, i acabaries amb sis rèpliques trencades. Amb això, el desplegament s'atura al primer Pod inestable.

I el preStop amb sleep 5 de 06-05 tanca l'altre extrem: els Pods que se'n van deixen de rebre trànsit abans de tancar les seves connexions. Readiness en entrar, preStop en sortir: entre totes dues, zero errors durant la publicació.

  1. Veure el desplegament en marxa

kubectl set image deploy/aurora-api api=ghcr.io/auroralibros/aurora-api:2.1.0 -n aurora
# o, amb Kustomize: editar l'overlay i kubectl apply -k

kubectl rollout status deploy/aurora-api -n aurora --timeout=300s
kubectl get pods -n aurora -l app.kubernetes.io/name=aurora-api -w
kubectl get rs -n aurora -l app.kubernetes.io/name=aurora-api
# Waiting for deployment "aurora-api" rollout to finish: 4 of 6 updated replicas are available...
# deployment "aurora-api" successfully rolled out
# NAME                  DESIRED  CURRENT  READY  AGE
# aurora-api-6d4f8b7c9  0        0        0      3d   ← v2.0.0, conservat
# aurora-api-8f2a1c5e7  6        6        6      2m   ← v2.1.0, actiu

Els dos ReplicaSets són la clau de tot el que ve després. L'antic es queda a zero rèpliques, no s'esborra: és l'historial que fa possible l'undo. El rollout status retorna codi 0 si acaba i diferent de zero si falla, cosa que el converteix en la porta natural de l'últim pas del pipeline de 06-02.

  1. Rollback: undo, revisions i revisionHistoryLimit

kubectl rollout history deploy/aurora-api -n aurora --revision=4   # veure una revisió
kubectl rollout undo deploy/aurora-api -n aurora                   # a l'anterior
kubectl rollout undo deploy/aurora-api -n aurora --to-revision=3   # a una de concreta
kubectl rollout pause deploy/aurora-api -n aurora                  # congelar a mitges
kubectl rollout resume deploy/aurora-api -n aurora
# REVISION  CHANGE-CAUSE
# 3         kubectl apply -k k8s/overlays/produccio (2.0.0)
# 4         kubectl set image ... aurora-api:2.1.0
# 5         kubectl rollout undo (tornada a 2.0.0)

Observa la revisió 5: el rollback no esborra la 4, crea una revisió nova amb el contingut de la 3. L'historial sempre va cap endavant, així que pots tornar a avançar quan corregeixis el problema.

revisionHistoryLimit: 5 al teu manifest controla quants ReplicaSets antics es conserven. Un valor de 0 fa impossible qualsevol undo; un valor enorme omple el namespace d'objectes buits. Entre 5 i 10 és el raonable.

I la regla d'or, que no és una comanda: el rollback ha d'estar provat abans de necessitar-lo. Un undo que ningú no ha executat mai és una suposició, no un pla. Assaja'l a staging cada vegada que canviïs res rellevant del desplegament, perquè el dia que calgui serà a les tres de la matinada i amb clients esperant.

  1. Rollback a Swarm

docker service update --image ghcr.io/auroralibros/aurora-api:2.1.0 aurora_aurora-api
docker service rollback aurora_aurora-api
docker service inspect aurora_aurora-api --format '{{.UpdateStatus.State}} {{.UpdateStatus.Message}}'
# rollback_completed  rollback: service rolled back to previous specification

Amb failure_action: rollback, Swarm ho fa sol: si durant la finestra de monitor una tasca nova falla i se supera max_failure_ratio, reverteix sense que ningú hi intervingui. És còmode i té un límit clar: només guarda l'especificació anterior, així que un rollback sobre un altre rollback et torna a on vas començar.

  1. Assaig d'un desplegament fallit: aurora-api:2.1.0

Simulem el cas real: la 2.1.0 té una errada d'escriptura al nom de la variable de la memòria cau, així que la readiness no passa mai.

kubectl set image deploy/aurora-api api=ghcr.io/auroralibros/aurora-api:2.1.0 -n aurora
kubectl rollout status deploy/aurora-api -n aurora --timeout=120s
# Waiting for deployment "aurora-api" rollout to finish: 2 out of 6 new replicas updated...
# error: deployment "aurora-api" exceeded its progress deadline
kubectl get pods -n aurora -l app.kubernetes.io/name=aurora-api
# NAME                        READY  STATUS   RESTARTS  AGE
# aurora-api-6d4f8b7c9-2xkpq  1/1    Running  0         3d   ← v2.0.0 servint (×6)
# aurora-api-8f2a1c5e7-b7kmd  0/1    Running  0         2m   ← v2.1.0 mai preparada
# aurora-api-8f2a1c5e7-n9xwp  0/1    Running  0         2m

Aquí hi ha el mecanisme complet: sis Pods vells servint i dos de nous que no arriben mai a 1/1. El desplegament es va aturar sol al segon Pod perquè maxUnavailable: 0 prohibeix retirar cap dels antics mentre els nous no estiguin preparats. Cap client no ha notat res; els dos Pods trencats no reben trànsit perquè la seva readiness retorna 503 i kube-proxy no els té als endpoints.

kubectl logs -n aurora aurora-api-8f2a1c5e7-b7kmd --tail=2
kubectl rollout undo deploy/aurora-api -n aurora
kubectl rollout status deploy/aurora-api -n aurora

Aquest és l'escenari que justifica totes les decisions del mòdul: la sonda de readiness va detectar el problema, maxUnavailable: 0 va impedir que es propagués i l'historial de revisions va permetre desfer-ho en una comanda.

  1. Blue-green amb dos Deployments

La idea és tenir dos Deployments complets i un Service el selector del qual decideix quin rep el trànsit.

# Dos Deployments idèntics llevat de l'etiqueta version i la imatge
metadata: { name: aurora-api-blau }
  template:
    metadata: { labels: { app.kubernetes.io/name: aurora-api, version: "2.0.0" } }
---
metadata: { name: aurora-api-verd }
  template:
    metadata: { labels: { app.kubernetes.io/name: aurora-api, version: "2.1.0" } }
---
apiVersion: v1
kind: Service
metadata: { name: aurora-api }
spec:
  selector:
    app.kubernetes.io/name: aurora-api
    version: "2.0.0"            # ← el commutador
  ports: [{ port: 3000, targetPort: http }]
# 1. Desplegar el verd i validar-lo sense exposar-lo, per un Service secundari
kubectl apply -f k8s/blue-green/verd.yaml
kubectl port-forward -n aurora deploy/aurora-api-verd 8081:3000 &
curl -s localhost:8081/salut/preparat && curl -s localhost:8081/llibres | jq '.llibres|length'

# 2. Commutar: una sola comanda, efecte immediat
kubectl patch svc aurora-api -n aurora -p '{"spec":{"selector":{"version":"2.1.0"}}}'

# 3. Si alguna cosa va malament, tornar: la mateixa comanda a l'inrevés
kubectl patch svc aurora-api -n aurora -p '{"spec":{"selector":{"version":"2.0.0"}}}'

Això és el que permetia l'acoblament per etiquetes de 06-04. El rollback és instantani —un canvi de selector, sense arrencar res— i aquesta és la seva gran virtut. El cost també és evident: durant la transició pagues el doble de recursos, i amb dotze rèpliques d'API no és trivial. A més, les connexions ja obertes contra els Pods blaus continuen allà fins que acabin; el tall no és tan atòmic com sembla.

Regla pràctica: mantén l'entorn blau viu almenys un cicle complet de negoci —una hora punta, un dia— abans d'eliminar-lo. El rollback barat només existeix mentre l'altre entorn continua dret.

  1. Canari per rèpliques i per Ingress

Per rèpliques, sense eines: dos Deployments amb la mateixa etiqueta de selector i el Service repartint per nombre de Pods.

aurora-api-estable aurora-api-canari Trànsit al canari
9 1 ~10 %
8 2 ~20 %
5 5 ~50 %
0 10 100 % (promocionat)

És aproximat —el repartiment de kube-proxy és aleatori— i de granularitat gruixuda: per servir un 1 % caldrien 99 rèpliques estables. Però no requereix instal·lar res.

Per Ingress, amb precisió real:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: aurora-canari
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"        # 10 % exacte
    nginx.ingress.kubernetes.io/canary-by-header: "x-aurora-canari"
spec:
  ingressClassName: nginx
  rules:
    - host: llibres.aurora.example
      http:
        paths:
          - { path: /llibres, pathType: Prefix, backend: { service: { name: aurora-api-canari, port: { number: 3000 } } } }

L'anotació canary-by-header és més útil del que sembla: permet que l'equip forci el seu propi trànsit cap al canari amb una capçalera, i validar la versió nova amb usuaris reals coneguts abans d'obrir el percentatge al públic.

  1. Què es vigila durant el canari

Un canari sense mètriques és només un desplegament lent. El que el converteix en una estratègia és comparar les dues versions amb els mateixos indicadors i a la mateixa finestra.

Mètrica Llindar típic d'avortament D'on surt (05-06)
Taxa d'error 5xx > 1 %, o el doble que l'estable Prometheus sobre els logs amb req_id
Latència p95 > 1,2 × l'estable Histograma de l'API
Latència p99 > 1,5 × l'estable Histograma de l'API
Reinicis de Pods Qualsevol kube_pod_container_status_restarts
Ús de memòria > 1,3 × l'estable cAdvisor
Errors de l'aplicació Qualsevol tipus nou Logs estructurats
Progressió típica:  10 % → 25 % → 50 % → 100 %
Espera a cada pas: almenys 10 min o 1 000 peticions (el que arribi més tard)

El criteri d'espera importa tant com els llindars. A les tres de la matinada, un 10 % de trànsit durant cinc minuts són trenta peticions: no diuen absolutament res. Per això la condició ha de ser doble, de temps i de volum. I la promoció, tan bon punt sigui possible, automàtica amb aquests llindars: si depèn que algú miri un panell, acabarà promocionant-se sense mirar.

  1. Argo Rollouts, Flagger i GitOps

Eina Què aporta
Argo Rollouts Substitueix el Deployment per un Rollout amb passos de canari i blue-green declaratius, i AnalysisTemplate per promoure o avortar segons Prometheus
Flagger Automatitza el canari sobre el Deployment existent, integrat amb malles de serveis i Ingress
Argo CD / Flux GitOps: el clúster se sincronitza amb el que hi ha a Git; desplegar és un merge
# Argo Rollouts: la progressió del canari, declarada
  strategy:
    canary:
      steps:
        - setWeight: 10
        - pause: { duration: 10m }
        - analysis: { templates: [{ templateName: taxa-d-exit }] }
        - setWeight: 50
        - pause: { duration: 10m }

El model GitOps canvia la direcció del desplegament i mereix entendre's: en comptes que el pipeline faci kubectl apply contra el clúster (push), un agent dins del clúster observa el repositori i aplica el que hi troba (pull). Els avantatges són concrets: CI no necessita credencials del clúster —desapareix el risc de l'últim pas de 06-02—, l'estat desitjat és a Git amb el seu historial i les seves revisions, i qualsevol canvi manual es detecta com a drift i es corregeix. El rollback passa a ser git revert.

  1. Migracions de base de dades: expand/contract

Cap estratègia de desplegament no resol això tota sola, perquè l'esquema és compartit per totes les versions alhora.

Imagina't que la 2.1.0 afegeix idioma a la taula llibres. L'enfocament ingenu —desplegar codi i esquema junts— falla en les dues direccions: si migres primer, les rèpliques velles no coneixen la columna; si desplegues primer, les noves fallen perquè la columna no existeix. I si fas rollback del codi, què fas amb l'esquema?

La resposta és el patró expand/contract, en tres desplegaments separats:

-- FASE 1 — EXPAND: additiu i compatible cap enrere
ALTER TABLE llibres ADD COLUMN idioma VARCHAR(8) DEFAULT 'es';

La 2.0.0 continua funcionant: ignora una columna que no coneix. Es desplega només l'esquema, sense tocar el codi.

-- FASE 2 — MIGRATE: la 2.1.0 escriu als dos llocs i llegeix del nou
UPDATE llibres SET idioma = 'es' WHERE idioma IS NULL;

Ara sí que es desplega la 2.1.0, que ja fa servir idioma. Durant el rolling update conviuen les dues versions sense problema: la vella no toca la columna, la nova l'omple. I el rollback és segur, perquè tornar a la 2.0.0 no trenca res.

-- FASE 3 — CONTRACT: només quan ja ningú no fa servir el que és vell
ALTER TABLE llibres ALTER COLUMN idioma SET NOT NULL;
ALTER TABLE llibres DROP COLUMN idioma_antic;
Canvi Compatible cap enrere? Com fer-ho
Afegir columna amb DEFAULT Directe
Afegir columna NOT NULL sense valor per defecte No Expand amb defecte → omplir → SET NOT NULL
Reanomenar columna No Afegir la nova → escriure a totes dues → migrar → esborrar
Esborrar columna No Deixar de fer-la servir → esperar un cicle → esborrar
Canviar el tipus No Columna nova, escriptura doble, migrar, esborrar
Afegir índex CREATE INDEX CONCURRENTLY (no bloqueja)

La regla que se'n dedueix: l'esquema i el codi no es despleguen mai acoblats. L'esquema va sempre al davant i de manera additiva; la retirada del que és vell va sempre al darrere i amb un cicle de retard. Així, en tot moment, la base de dades és compatible amb la versió anterior i amb la següent, i qualsevol rollback de codi és segur.

Advertència. Una migració sobre dades reals pot bloquejar taules, esgotar el disc pel WAL o fer irreversible un desplegament. Un ALTER TABLE que reescriu una taula gran deixa el servei bloquejat mentre dura. Valida sempre el pla de migració, la finestra i el procediment de tornada enrere amb el responsable de dades de la teva organització, i assaja'l abans sobre una còpia del volum de producció.

  1. Feature flags

L'última peça separa dues coses que solem confondre: desplegar codi i activar una funcionalitat.

// api/src/flags.js — la bandera arriba per configuració, com tota la resta (06-01)
const actives = new Set((config.flags ?? '').split(',').filter(Boolean));
const activa = (nom) => actives.has(nom);

app.get('/llibres', async (req, res) => {
  const llibres = await cataleg.llistar();
  if (activa('recomanacions')) llibres.recomanats = await recomanador.per(req);
  res.json(llibres);
});

Amb la bandera apagada, el codi de la 2.1.0 es desplega fins a producció i no fa res. S'activa després, quan vulguis, canviant un ConfigMap i sense desplegar res; i si va malament, s'apaga en segons, cosa infinitament més ràpida que qualsevol rollback d'imatge.

Aspecte Rollback de desplegament Apagar una bandera
Temps 1-5 min Segons
Abast Tota la versió Només aquella funcionalitat
Risc Recrea Pods Cap
Segmentació No Per usuari, percentatge o regió

El preu és el deute tècnic: cada bandera és una branca viva al codi i dos camins per provar. Es posen amb data de caducitat i es retiren tan bon punt la funcionalitat està consolidada, o acabes amb vint banderes i cap combinació provada.

Errors Habituals i Consells

  • Desplegar sense sonda de readiness. Cada rèplica substituïda produeix uns segons d'errors. És el requisit de tota la resta.
  • maxUnavailable > 0 amb capacitat justa. Publiques justament quan menys capacitat tens. Fes servir maxSurge i maxUnavailable: 0.
  • Esquema i codi al mateix desplegament. Bloqueja el rollback: pots tornar el codi, però no l'esquema. Expand/contract, sempre.
  • Canari sense mètriques ni criteri. És un desplegament lent amb més passos. Defineix llindars i finestra abans de començar.
  • revisionHistoryLimit: 0. Deixa kubectl rollout undo sense res a què tornar.
  • Blue-green i esborrar el blau de seguida. Perds l'única cosa que feia barat el rollback. Espera un cicle complet.
  • Canviar el format de la memòria cau sense versionar la clau. Errors intermitents impossibles de reproduir durant la coexistència.
  • Consell: assaja el rollback a staging periòdicament. Un procediment no provat no és un procediment.
  • Consell: fes que el pipeline falli si kubectl rollout status falla. Un desplegament en verd amb Pods trencats és el pitjor dels dos mons.

Exercicis

Exercici 1. Assaja un desplegament fallit d'aurora-api:2.1.0 que no passi la readiness: observa com s'atura sol, comprova que cap client no rep errors durant tot el procés i desfés-lo verificant l'historial de revisions.

Exercici 2. Implementa un blue-green complet: desplega la versió verda, valida-la sense exposar-la, commuta el Service, mesura el temps de commutació i torna enrere.

Exercici 3. Aplica expand/contract per afegir la columna idioma a llibres sense tall i demostrant que el rollback del codi continua sent segur a cada fase.

Solucions

Solució 1.

# Càrrega contínua en segon pla durant tot l'exercici
kubectl run vigia --image=curlimages/curl -n aurora --restart=Never -- \
  sh -c 'while true; do curl -s -o /dev/null -w "%{http_code}\n" http://aurora-api:3000/llibres; sleep 0.2; done' &

kubectl set image deploy/aurora-api api=ghcr.io/auroralibros/aurora-api:2.1.0 -n aurora
kubectl rollout status deploy/aurora-api -n aurora --timeout=180s; echo "codi: $?"
# error: deployment "aurora-api" exceeded its progress deadline
# codi: 1

kubectl get pods -n aurora -l app.kubernetes.io/name=aurora-api \
  -o custom-columns=POD:.metadata.name,PREPARAT:.status.containerStatuses[0].ready | sort -k2
kubectl get endpoints aurora-api -n aurora -o jsonpath='{.subsets[0].addresses[*].ip}' | wc -w
kubectl logs -n aurora vigia --tail=2000 | sort | uniq -c
aurora-api-8f2a1c5e7-b7kmd   false        ← v2.1.0
aurora-api-8f2a1c5e7-n9xwp   false        ← v2.1.0
aurora-api-6d4f8b7c9-2xkpq   true         ← v2.0.0 (×6)
6
   1983 200

Tres números ho demostren tot. Hi ha vuit Pods en marxa però només sis endpoints: els dos Pods de la 2.1.0 existeixen i no van entrar mai al balanceig perquè la seva readiness no va retornar mai 200. I el vigia va registrar 1 983 respostes, totes 200, durant un desplegament que ha fallat completament.

El mecanisme té tres peces encadenades, i totes tres es van decidir en lliçons anteriors. La readiness de 06-01 va detectar que la 2.1.0 no podia servir; kube-proxy la va excloure dels endpoints, així que no va rebre ni una petició; i maxUnavailable: 0 va impedir retirar cap Pod vell, perquè fer-ho hauria baixat de sis disponibles. El desplegament va quedar congelat en un estat segur.

kubectl rollout undo deploy/aurora-api -n aurora
kubectl rollout status deploy/aurora-api -n aurora
kubectl rollout history deploy/aurora-api -n aurora
kubectl logs -n aurora vigia --tail=3000 | sort | uniq -c
# deployment "aurora-api" successfully rolled out
# REVISION  CHANGE-CAUSE
# 3         kubectl apply -k k8s/overlays/produccio (2.0.0)
# 4         kubectl set image ... aurora-api:2.1.0
# 5         kubectl rollout undo
#    2874 200

Zero errors en tot l'exercici: desplegament fallit, espera i rollback inclosos. Fixa't que l'undo va ser pràcticament instantani, i la raó és que no va arrencar res: els sis Pods de la 2.0.0 no van deixar d'existir mai, així que desfer va consistir a esborrar els dos Pods trencats i tornar el ReplicaSet antic a sis rèpliques desitjades. Un rollback després d'un desplegament reeixit sí que triga, perquè cal recrear els Pods vells.

I el codi: 1 del rollout status és la peça que faltava al pipeline de 06-02: n'hi ha prou de no ignorar aquest codi de sortida perquè un desplegament així posi el job en vermell i dispari l'undo automàticament.

Solució 2.

kubectl apply -f k8s/blue-green/verd.yaml
kubectl wait --for=condition=available deploy/aurora-api-verd -n aurora --timeout=120s
kubectl get endpoints aurora-api -n aurora -o jsonpath='{.subsets[0].addresses[*].ip}' | wc -w
kubectl port-forward -n aurora deploy/aurora-api-verd 8081:3000 &
curl -s localhost:8081/llibres | jq -r '"\(.origen) \(.llibres|length)"'
# 6
# db 9

Detall important del primer número: encara que el Deployment verd ja està preparat, el Service continua tenint sis endpoints, els del blau. La versió nova és viva, validada per port-forward i servint els nou títols, i no rep ni una petició d'usuari. Aquesta separació entre "desplegat" i "exposat" és l'essència del blue-green.

inici=$(date +%s%3N)
kubectl patch svc aurora-api -n aurora -p '{"spec":{"selector":{"version":"2.1.0"}}}'
kubectl get endpoints aurora-api -n aurora -o jsonpath='{.subsets[0].addresses[*].ip}' | wc -w
echo "commutacio: $(( $(date +%s%3N) - inici )) ms"
kubectl logs -n aurora vigia --tail=200 | sort | uniq -c
# 6
# commutacio: 412 ms
#     200 200
kubectl patch svc aurora-api -n aurora -p '{"spec":{"selector":{"version":"2.0.0"}}}'   # tornada enrere
Estratègia Temps de commutació Temps de rollback Recursos durant
Rolling update (6 rèpliques) ~90 s ~90 s 6 + 2 Pods
Blue-green 0,4 s 0,4 s 12 Pods

Els 412 mil·lisegons són l'argument sencer del blue-green, i el 12 de l'última columna és la seva factura. La commutació és tan ràpida perquè no arrenca ni atura res: només reescriu un selector, i el controlador d'endpoints recalcula quins Pods pertanyen al Service.

Dos matisos que la taula no mostra. El primer: les connexions HTTP keep-alive ja establertes contra Pods blaus continuen servint-se des de la versió vella fins que es tanquen, així que la commutació no és atòmica des del punt de vista de l'usuari. El segon, més seriós: mentre els dos entorns conviuen, totes dues versions estan escrivint a la mateixa base de dades i a la mateixa memòria cau, amb la qual cosa la compatibilitat de la secció 1 continua sent obligatòria. Blue-green resol l'encaminament, no la compatibilitat de dades.

Solució 3.

-- FASE 1 (EXPAND) — es desplega SOLA, sense tocar el codi
ALTER TABLE llibres ADD COLUMN idioma VARCHAR(8) DEFAULT 'es';
kubectl exec -n aurora aurora-db-0 -- psql -U aurora -d aurora_llibres -f /tmp/expand.sql
curl -s localhost:8080/llibres | jq -r '"\(.origen) \(.llibres|length)"'   # amb la 2.0.0 encara
# db 9

La 2.0.0 continua retornant els nou títols amb una columna nova que ignora completament, perquè el seu SELECT anomena les columnes que necessita. Aquesta és la propietat que fa segura la fase 1: un canvi additiu amb valor per defecte és invisible per al codi antic.

# FASE 2 (MIGRATE) — ara sí, el codi nou, amb rolling update normal
kubectl set image deploy/aurora-api api=ghcr.io/auroralibros/aurora-api:2.1.0 -n aurora
kubectl rollout status deploy/aurora-api -n aurora
curl -s localhost:8080/llibres | jq -r '.llibres[1] | "\(.titol) [\(.idioma)]"'
kubectl rollout undo deploy/aurora-api -n aurora     # rollback de prova, a mitges
curl -s localhost:8080/llibres | jq -r '.llibres[1].titol'
# Rayuela [es]      ← amb la 2.1.0
# Rayuela           ← després del rollback a la 2.0.0

Aquestes dues línies són la demostració demanada. Amb la 2.1.0, /llibres inclou l'idioma; després del rollback a la 2.0.0, el camp desapareix de la resposta i tota la resta continua funcionant. La base de dades no ha hagut de canviar en cap moment, perquè la columna era compatible amb les dues versions.

-- FASE 3 (CONTRACT) — dies després, quan cap versió antiga no queda viva
ALTER TABLE llibres ALTER COLUMN idioma SET NOT NULL;
Fase Què es desplega Rollback del codi segur? Cal rollback de l'esquema?
1. Expand Només esquema (additiu) Sí (no ha canviat) No
2. Migrate Només codi (2.1.0) No
3. Contract Només esquema (restrictiu) Sí, si ja no hi ha versions velles No

La columna decisiva és la tercera: en cap de les tres fases no cal desfer l'esquema, i aquest és exactament l'objectiu del patró. Comparat amb l'enfocament acoblat, la diferència és abismal: si haguessis desplegat l'ALTER TABLE ... NOT NULL juntament amb la 2.1.0, el rollback del codi hauria deixat la 2.0.0 inserint files sense idioma contra una columna obligatòria, i cada alta hauria fallat.

La fase 3 exigeix una disciplina que s'oblida amb facilitat: no es pot executar fins a estar segur que cap versió antiga continua viva, i això inclou treballs per lots, processos d'informes o integracions de tercers que potser ningú no recorda. Per això el prudent és deixar passar un cicle complet —dies, no minuts— entre la fase 2 i la 3. I per això el pla de migració, la finestra i el procediment de tornada enrere s'acorden amb el responsable de dades abans de tocar producció.

Conclusió

Ja saps publicar sense que ningú deixi de poder comprar un llibre. Tens clar el problema de fons —durant tot desplegament conviuen dues versions sobre la mateixa base de dades i la mateixa memòria cau— i les cinc estratègies amb el seu cost real: recreate i el seu tall, el rolling update com a valor per defecte assenyat, blue-green amb la seva commutació de 412 mil·lisegons a canvi del doble de recursos, el canari com l'opció de menor risc quan tens mètriques fiables, i shadow amb la seva advertència sobre els efectes secundaris duplicats.

Domines el rolling update per dins: maxSurge: 2 amb maxUnavailable: 0 per no perdre capacitat ni un instant, minReadySeconds per no donar per bo un Pod que cau dos segons després, progressDeadlineSeconds com a límit, i la taula d'equivalències amb l'update_config de Swarm. Sobretot, has comprovat per què la sonda de readiness de 06-01 és la peça que sosté tota la resta: l'assaig del desplegament fallit d'aurora-api:2.1.0 va acabar amb vuit Pods en marxa, només sis endpoints i 1 983 respostes, totes 200, amb el desplegament congelat en un estat segur i un rollout status retornant codi 1 perquè el pipeline ho detecti. L'undo va ser instantani perquè no hi havia res a arrencar, i l'historial de revisions sempre avança, mai no esborra.

Has muntat el blue-green commutant un selector —l'acoblament per etiquetes de 06-04 rendint—, el canari per rèpliques i per anotacions de l'Ingress amb la seva capçalera per a l'equip, i saps què es vigila durant el canari i amb quin criteri doble de temps i volum es promociona o s'avorta. Coneixes Argo Rollouts i Flagger per automatitzar-ho, i GitOps amb Argo CD o Flux invertint la direcció del desplegament perquè CI no necessiti credencials del clúster. I has resolt el que cap estratègia no arregla tota sola: el patró expand/contract en tres desplegaments, aplicat a afegir idioma a llibres, amb la demostració que el rollback del codi és segur a totes les fases perquè l'esquema no es desplega mai acoblat. Tanquen el quadre els feature flags, que separen desplegar d'activar i converteixen una tornada enrere de minuts en una de segons.

I amb això es tanca el mòdul 6. Aurora Libros hi va entrar sent una pila de Compose al teu portàtil i en surt convertida en una plataforma: una imatge 2.0.0 apta per a producció, amb la configuració fora, validació en arrencar que falla en 0,4 segons, aturada ordenada sense perdre ni una petició i tres sondes amb semàntiques separades; un pipeline que a cada git push prova contra PostgreSQL i Redis reals, construeix per a dues arquitectures en 41 segons gràcies a la memòria cau remota, escaneja, signa amb Cosign i publica; un clúster —primer Swarm amb les seves xarxes overlay i la seva malla d'encaminament, després Kubernetes amb els quatre serveis en manifestos reals, StatefulSet per a les dades, Kustomize per entorn i /llibres retornant els nou títols—; un HorizontalPodAutoscaler que va portar el p95 de 1 840 ms a 88 ms sense que ningú toqués res, amb PgBouncer resolent el coll d'ampolla que era on no miraves; i, ara, publicacions sense tall amb tornada enrere provada. Aurora Libros ja no viu a la teva màquina: viu en un clúster, es desplega sola des d'un commit, creix amb la càrrega i s'actualitza sense que cap client ho noti.

Al mòdul 7 aixequem la vista del projecte per mirar al voltant. Veuràs com s'aprovisionen els hosts que sostenen tot això, quan convé Compose i quan Kubernetes amb una comparació honesta de tots dos, què aporta i què costa Docker Desktop, quines eines i plugins de tercers mereixen un lloc al teu flux de treball, com hi encaixen Podman, containerd i l'estàndard OCI —aquestes imatges teves que ja saps que no són "de Docker"— i cap a on va l'ecosistema de contenidors.

Docker: De Principiant a Avançat

Mòdul 1: Introducció a Docker

Mòdul 2: Treballant amb Imatges Docker

Mòdul 3: Contenidors Docker

Mòdul 4: Docker Compose

Mòdul 5: Conceptes Avançats de Docker

Mòdul 6: Docker en Producció

Mòdul 7: Ecosistema i Eines de Docker

© Copyright 2026. Tots els drets reservats