Aurora Libros escala sola y aguanta el pico. Queda el momento más delicado de todos: publicar una versión nueva. Entre la vieja y la nueva hay siempre un periodo en el que conviven, y decidir cómo se gestiona ese periodo es la diferencia entre una publicación que nadie nota y un cuarto de hora sin poder comprar un libro.

Contenido

  1. El problema: la ventana de coexistencia
  2. Las cinco estrategias, comparadas
  3. Rolling update en Kubernetes: maxSurge y maxUnavailable
  4. update_config en Swarm y la tabla de equivalencias
  5. La readiness y minReadySeconds
  6. Ver el despliegue en marcha
  7. Rollback: undo, revisiones y revisionHistoryLimit
  8. Rollback en Swarm
  9. Ensayo de un despliegue fallido: aurora-api:2.1.0
  10. Blue-green con dos Deployments
  11. Canario por réplicas y por Ingress
  12. Qué se vigila durante el canario
  13. Argo Rollouts, Flagger y GitOps
  14. Migraciones de base de datos: expand/contract
  15. Feature flags

  1. El problema: la ventana de coexistencia

Durante un despliegue progresivo, el tráfico se reparte entre versiones distintas. Eso obliga a que la versión 2.1.0 sea compatible con la 2.0.0 en tres frentes:

Frente Qué exige
Esquema de la base de datos Que ambas versiones puedan leer y escribir la misma tabla
Contrato de la API Que un cliente que empezó con la vieja pueda terminar con la nueva
Caché compartida Que una entrada escrita por una versión la entienda la otra

El tercero es el que más sorprende y el más fácil de provocar: si la 2.1.0 cambia el formato de lo que guarda en aurora-cache con la misma clave, las réplicas antiguas leerán objetos que no entienden y devolverán errores intermitentes que aparecen y desaparecen según a qué Pod caiga cada petición. La solución es tan simple como versionar la clave: libros:v2:*.

  1. Las cinco estrategias, comparadas

Estrategia Cómo Recursos extra Corte Rollback Riesgo Cuándo
Recreate Mata todo y arranca lo nuevo Ninguno , total Redesplegar (lento) Alto Cambios incompatibles, entornos internos
Rolling update Sustituye de N en N +maxSurge No Rodando hacia atrás Medio El defecto sensato
Blue-green Dos entornos completos, se conmuta ×2 No Instantáneo Bajo Cambios grandes, se puede pagar el doble
Canario Un % del tráfico a la nueva +1 réplica No Rápido El más bajo Cambios sensibles con métricas fiables
Shadow Copia del tráfico real sin devolver respuesta ×2 en cómputo No No aplica Nulo para el usuario Validar rendimiento antes de exponer
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["azul v1 ← 100%"] --> B2["azul v1 + verde v2 (0%)"] --> B3["verde v2 ← 100%"]
    end
    subgraph C[Canario]
        C1["v1 100%"] --> C2["v1 90% / v2 10%"] --> C3["v1 50% / v2 50%"] --> C4["v2 100%"]
    end

El shadow merece una nota: duplica el tráfico real hacia la versión nueva pero descarta su respuesta, así que el usuario nunca la ve. Es magnífico para validar rendimiento con carga auténtica, y tiene una trampa peligrosa: si la petición duplicada escribe en la base de datos o cobra una tarjeta, la operación ocurre dos veces. Solo sirve con tráfico de lectura o con dependencias aisladas.

  1. Rolling update en Kubernetes: maxSurge y maxUnavailable

spec:
  replicas: 6
  minReadySeconds: 10               # 10 s sano antes de contar como disponible
  progressDeadlineSeconds: 300      # si en 5 min no avanza, se marca como fallido
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2                   # hasta 8 Pods a la vez (6 + 2)
      maxUnavailable: 0             # nunca menos de 6 disponibles
maxSurge maxUnavailable Capacidad durante el despliegue Velocidad Recursos extra
0 1 5 de 6 (83 %) Lenta Ninguno
1 1 6 de 6 Media +1 Pod
2 0 6 de 6 siempre Media +2 Pods
100% 0 Doble durante la transición Máxima ×2 (blue-green de facto)
25% 25% 75 % mínimo Media +25 %

La combinación maxSurge: 2 / maxUnavailable: 0 es la correcta para Aurora Libros: nunca hay menos de seis Pods sirviendo, así que la capacidad no baja ni un instante durante la publicación. Cuesta dos Pods de más durante unos minutos, que es un precio ridículo. maxUnavailable: 0 es también un seguro: si los Pods nuevos no llegan a estar listos, el despliegue no puede avanzar, porque avanzar exigiría quitar Pods viejos.

Ojo con un caso concreto: maxSurge: 0 y maxUnavailable: 0 a la vez es una configuración inválida —el despliegue no podría dar ni un paso—, y Kubernetes la rechaza.

  1. update_config en Swarm y la tabla de equivalencias

    deploy:
      replicas: 6
      update_config:
        parallelism: 2          # 2 tareas a la vez
        delay: 15s              # espera entre lotes
        order: start-first      # arrancar la nueva antes de parar la vieja
        failure_action: rollback
        monitor: 30s            # ventana de vigilancia tras cada tarea
        max_failure_ratio: 0.2
      rollback_config:
        parallelism: 3
        order: stop-first
Concepto Kubernetes Swarm
Cuántas a la vez maxSurge / maxUnavailable parallelism
Pausa entre lotes minReadySeconds delay
Orden maxSurge > 0 = start-first order: start-first / stop-first
Qué hacer si falla Se detiene (con maxUnavailable: 0) failure_action: rollback
Ventana de vigilancia progressDeadlineSeconds monitor
Umbral de fallo Implícito en las sondas max_failure_ratio
Deshacer kubectl rollout undo docker service rollback
Historial revisionHistoryLimit Solo la versión anterior

Dos diferencias importantes. Swarm puede deshacer solo con failure_action: rollback, mientras que Kubernetes se limita a detenerse y esperar a que decidas —lo cual es defendible: parar deja el sistema en un estado conocido y no oculta el problema—. En cambio, Swarm guarda únicamente la versión inmediatamente anterior, mientras que Kubernetes conserva tantas revisiones como digas.

  1. La readiness y minReadySeconds

Aquí es donde lo que hiciste en 06-01 rinde de verdad. Sin sonda de readiness, un rolling update es una ruleta: Kubernetes considera "listo" a un Pod en cuanto su contenedor arranca, le manda tráfico y tu aplicación aún está estableciendo el pool de PostgreSQL. Cada réplica sustituida produce unos segundos de errores.

Sin readiness:  contenedor arranca → recibe tráfico → aún conectando → 502
Con readiness:  contenedor arranca → /salud/listo 503 → conecta → 200 → recibe tráfico

minReadySeconds: 10 añade un segundo seguro: exige que el Pod lleve diez segundos seguidos listo antes de contarlo como disponible y pasar al siguiente. Sin él, un Pod que pasa la readiness y se cae dos segundos después haría avanzar el despliegue igualmente, y acabarías con seis réplicas rotas. Con él, el despliegue se detiene en el primer Pod inestable.

Y el preStop con sleep 5 de 06-05 cierra el otro extremo: los Pods que se van dejan de recibir tráfico antes de cerrar sus conexiones. Readiness al entrar, preStop al salir: entre las dos, cero errores durante la publicación.

  1. Ver el despliegue en marcha

kubectl set image deploy/aurora-api api=ghcr.io/auroralibros/aurora-api:2.1.0 -n aurora
# o, con Kustomize: editar el overlay y 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, conservado
# aurora-api-8f2a1c5e7  6        6        6      2m   ← v2.1.0, activo

Los dos ReplicaSets son la clave de todo lo que viene después. El antiguo se queda a cero réplicas, no se borra: es el historial que hace posible el undo. El rollout status devuelve código 0 si termina y distinto de cero si falla, lo que lo convierte en la puerta natural del último paso del pipeline de 06-02.

  1. Rollback: undo, revisiones y revisionHistoryLimit

kubectl rollout history deploy/aurora-api -n aurora --revision=4   # ver una revisión
kubectl rollout undo deploy/aurora-api -n aurora                   # a la anterior
kubectl rollout undo deploy/aurora-api -n aurora --to-revision=3   # a una concreta
kubectl rollout pause deploy/aurora-api -n aurora                  # congelar a mitad
kubectl rollout resume deploy/aurora-api -n aurora
# REVISION  CHANGE-CAUSE
# 3         kubectl apply -k k8s/overlays/produccion (2.0.0)
# 4         kubectl set image ... aurora-api:2.1.0
# 5         kubectl rollout undo (vuelta a 2.0.0)

Observa la revisión 5: el rollback no borra la 4, crea una revisión nueva con el contenido de la 3. El historial es siempre hacia delante, así que puedes volver a avanzar cuando corrijas el problema.

revisionHistoryLimit: 5 en tu manifiesto controla cuántos ReplicaSets antiguos se conservan. Un valor de 0 hace imposible cualquier undo; un valor enorme llena el namespace de objetos vacíos. Entre 5 y 10 es lo razonable.

Y la regla de oro, que no es un comando: el rollback debe estar probado antes de necesitarlo. Un undo que nadie ha ejecutado nunca es una suposición, no un plan. Ensáyalo en staging cada vez que cambies algo relevante del despliegue, porque el día que haga falta será a las tres de la madrugada y con clientes esperando.

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

Con failure_action: rollback, Swarm lo hace solo: si durante la ventana de monitor una tarea nueva falla y se supera max_failure_ratio, revierte sin que nadie intervenga. Es cómodo y tiene un límite claro: solo guarda la especificación anterior, así que un rollback sobre otro rollback te devuelve a donde empezaste.

  1. Ensayo de un despliegue fallido: aurora-api:2.1.0

Simulemos el caso real: la 2.1.0 tiene una errata en el nombre de la variable de la caché, así que la readiness nunca pasa.

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 sirviendo (×6)
# aurora-api-8f2a1c5e7-b7kmd  0/1    Running  0         2m   ← v2.1.0 nunca lista
# aurora-api-8f2a1c5e7-n9xwp  0/1    Running  0         2m

Ahí está el mecanismo completo: seis Pods viejos sirviendo y dos nuevos que nunca alcanzan 1/1. El despliegue se paró solo en el segundo Pod porque maxUnavailable: 0 prohíbe retirar ninguno de los antiguos mientras los nuevos no estén listos. Ningún cliente ha notado nada; los dos Pods rotos no reciben tráfico porque su readiness devuelve 503 y kube-proxy no los tiene en los 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

Este es el escenario que justifica todas las decisiones del módulo: la sonda de readiness detectó el problema, maxUnavailable: 0 impidió que se propagara y el historial de revisiones permitió deshacerlo en un comando.

  1. Blue-green con dos Deployments

La idea es tener dos Deployments completos y un Service cuyo selector decide cuál recibe el tráfico.

# Dos Deployments idénticos salvo por la etiqueta version y la imagen
metadata: { name: aurora-api-azul }
  template:
    metadata: { labels: { app.kubernetes.io/name: aurora-api, version: "2.0.0" } }
---
metadata: { name: aurora-api-verde }
  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 conmutador
  ports: [{ port: 3000, targetPort: http }]
# 1. Desplegar verde y validarlo sin exponerlo, por un Service secundario
kubectl apply -f k8s/blue-green/verde.yaml
kubectl port-forward -n aurora deploy/aurora-api-verde 8081:3000 &
curl -s localhost:8081/salud/listo && curl -s localhost:8081/libros | jq '.libros|length'

# 2. Conmutar: un solo comando, efecto inmediato
kubectl patch svc aurora-api -n aurora -p '{"spec":{"selector":{"version":"2.1.0"}}}'

# 3. Si algo va mal, volver: el mismo comando al revés
kubectl patch svc aurora-api -n aurora -p '{"spec":{"selector":{"version":"2.0.0"}}}'

Esto es lo que permitía el acoplamiento por etiquetas de 06-04. El rollback es instantáneo —un cambio de selector, sin arrancar nada— y esa es su gran virtud. El coste también es evidente: durante la transición pagas el doble de recursos, y con doce réplicas de API no es trivial. Además, las conexiones ya abiertas contra los Pods azules siguen ahí hasta que terminen; el corte no es tan atómico como parece.

Regla práctica: mantén el entorno azul vivo al menos un ciclo completo de negocio —una hora punta, un día— antes de eliminarlo. El rollback barato solo existe mientras el otro entorno esté en pie.

  1. Canario por réplicas y por Ingress

Por réplicas, sin herramientas: dos Deployments con la misma etiqueta de selector y el Service repartiendo por número de Pods.

aurora-api-estable aurora-api-canario Tráfico al canario
9 1 ~10 %
8 2 ~20 %
5 5 ~50 %
0 10 100 % (promocionado)

Es aproximado —el reparto de kube-proxy es aleatorio— y de granularidad gruesa: para servir un 1 % harían falta 99 réplicas estables. Pero no requiere instalar nada.

Por Ingress, con precisión real:

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

La anotación canary-by-header es más útil de lo que parece: permite que el equipo fuerce su propio tráfico hacia el canario con una cabecera, y validar la versión nueva con usuarios reales conocidos antes de abrir el porcentaje al público.

  1. Qué se vigila durante el canario

Un canario sin métricas es solo un despliegue lento. Lo que lo convierte en una estrategia es comparar las dos versiones con los mismos indicadores y en la misma ventana.

Métrica Umbral típico de aborto De dónde sale (05-06)
Tasa de error 5xx > 1 %, o el doble que la estable Prometheus sobre los logs con req_id
Latencia p95 > 1,2 × la estable Histograma de la API
Latencia p99 > 1,5 × la estable Histograma de la API
Reinicios de Pods Cualquiera kube_pod_container_status_restarts
Uso de memoria > 1,3 × la estable cAdvisor
Errores de la aplicación Cualquier tipo nuevo Logs estructurados
Progresión típica:  10 % → 25 % → 50 % → 100 %
Espera en cada paso: al menos 10 min o 1 000 peticiones (lo que llegue más tarde)

El criterio de espera importa tanto como los umbrales. A las tres de la madrugada, un 10 % de tráfico durante cinco minutos son treinta peticiones: no dicen absolutamente nada. Por eso la condición debe ser doble, de tiempo y de volumen. Y la promoción, en cuanto sea posible, automática con esos umbrales: si depende de que alguien mire un panel, acabará promocionándose sin mirar.

  1. Argo Rollouts, Flagger y GitOps

Herramienta Qué aporta
Argo Rollouts Sustituye el Deployment por un Rollout con pasos de canario y blue-green declarativos, y AnalysisTemplate para promover o abortar según Prometheus
Flagger Automatiza el canario sobre el Deployment existente, integrado con mallas de servicios e Ingress
Argo CD / Flux GitOps: el clúster se sincroniza con lo que hay en Git; desplegar es un merge
# Argo Rollouts: la progresión del canario, declarada
  strategy:
    canary:
      steps:
        - setWeight: 10
        - pause: { duration: 10m }
        - analysis: { templates: [{ templateName: tasa-de-exito }] }
        - setWeight: 50
        - pause: { duration: 10m }

El modelo GitOps cambia la dirección del despliegue y merece entenderse: en vez de que el pipeline haga kubectl apply contra el clúster (push), un agente dentro del clúster observa el repositorio y aplica lo que encuentra (pull). Las ventajas son concretas: CI no necesita credenciales del clúster —desaparece el riesgo del último paso de 06-02—, el estado deseado está en Git con su historial y sus revisiones, y cualquier cambio manual se detecta como drift y se corrige. El rollback pasa a ser git revert.

  1. Migraciones de base de datos: expand/contract

Ninguna estrategia de despliegue resuelve esto sola, porque el esquema es compartido por todas las versiones a la vez.

Imagina que la 2.1.0 añade idioma a la tabla libros. El enfoque ingenuo —desplegar código y esquema juntos— falla en las dos direcciones: si migras primero, las réplicas viejas no conocen la columna; si despliegas primero, las nuevas fallan porque la columna no existe. Y si haces rollback del código, ¿qué haces con el esquema?

La respuesta es el patrón expand/contract, en tres despliegues separados:

-- FASE 1 — EXPAND: aditivo y compatible hacia atrás
ALTER TABLE libros ADD COLUMN idioma VARCHAR(8) DEFAULT 'es';

La 2.0.0 sigue funcionando: ignora una columna que no conoce. Se despliega solo el esquema, sin tocar el código.

-- FASE 2 — MIGRATE: la 2.1.0 escribe en ambos sitios y lee del nuevo
UPDATE libros SET idioma = 'es' WHERE idioma IS NULL;

Ahora sí se despliega la 2.1.0, que ya usa idioma. Durante el rolling update conviven las dos versiones sin problema: la vieja no toca la columna, la nueva la rellena. Y el rollback es seguro, porque volver a la 2.0.0 no rompe nada.

-- FASE 3 — CONTRACT: solo cuando ya nadie usa lo viejo
ALTER TABLE libros ALTER COLUMN idioma SET NOT NULL;
ALTER TABLE libros DROP COLUMN idioma_antiguo;
Cambio ¿Compatible hacia atrás? Cómo hacerlo
Añadir columna con DEFAULT Directo
Añadir columna NOT NULL sin defecto No Expand con defecto → rellenar → SET NOT NULL
Renombrar columna No Añadir la nueva → escribir en ambas → migrar → borrar
Borrar columna No Dejar de usarla → esperar un ciclo → borrar
Cambiar el tipo No Columna nueva, doble escritura, migrar, borrar
Añadir índice CREATE INDEX CONCURRENTLY (no bloquea)

La regla que se deduce: el esquema y el código nunca se despliegan acoplados. El esquema va siempre por delante y de forma aditiva; la retirada de lo viejo va siempre por detrás y con un ciclo de retraso. Así, en todo momento, la base de datos es compatible con la versión anterior y con la siguiente, y cualquier rollback de código es seguro.

Advertencia. Una migración sobre datos reales puede bloquear tablas, agotar el disco por el WAL o hacer irreversible un despliegue. Un ALTER TABLE que reescribe una tabla grande deja el servicio bloqueado mientras dura. Valida siempre el plan de migración, la ventana y el procedimiento de vuelta atrás con el responsable de datos de tu organización, y ensáyalo antes sobre una copia del volumen de producción.

  1. Feature flags

La última pieza separa dos cosas que solemos confundir: desplegar código y activar una funcionalidad.

// api/src/flags.js — la bandera llega por configuración, como todo lo demás (06-01)
const activas = new Set((config.flags ?? '').split(',').filter(Boolean));
const activa = (nombre) => activas.has(nombre);

app.get('/libros', async (req, res) => {
  const libros = await catalogo.listar();
  if (activa('recomendaciones')) libros.recomendados = await recomendador.para(req);
  res.json(libros);
});

Con la bandera apagada, el código de la 2.1.0 se despliega hasta producción y no hace nada. Se activa después, cuando quieras, cambiando un ConfigMap y sin desplegar nada; y si va mal, se apaga en segundos, que es infinitamente más rápido que cualquier rollback de imagen.

Aspecto Rollback de despliegue Apagar una bandera
Tiempo 1-5 min Segundos
Alcance Toda la versión Solo esa funcionalidad
Riesgo Recrea Pods Ninguno
Segmentación No Por usuario, porcentaje o región

El precio es la deuda técnica: cada bandera es una rama viva en el código y dos caminos que probar. Se ponen con fecha de caducidad y se retiran en cuanto la funcionalidad está consolidada, o acabas con veinte banderas y ninguna combinación probada.

Errores Comunes y Consejos

  • Desplegar sin sonda de readiness. Cada réplica sustituida produce unos segundos de errores. Es el requisito de todo lo demás.
  • maxUnavailable > 0 con capacidad justa. Publicas justo cuando menos capacidad tienes. Usa maxSurge y maxUnavailable: 0.
  • Esquema y código en el mismo despliegue. Bloquea el rollback: puedes volver el código, pero no el esquema. Expand/contract, siempre.
  • Canario sin métricas ni criterio. Es un despliegue lento con más pasos. Define umbrales y ventana antes de empezar.
  • revisionHistoryLimit: 0. Deja kubectl rollout undo sin nada a lo que volver.
  • Blue-green y borrar el azul enseguida. Pierdes lo único que hacía barato el rollback. Espera un ciclo completo.
  • Cambiar el formato de la caché sin versionar la clave. Errores intermitentes imposibles de reproducir durante la coexistencia.
  • Consejo: ensaya el rollback en staging periódicamente. Un procedimiento no probado no es un procedimiento.
  • Consejo: haz que el pipeline falle si kubectl rollout status falla. Un despliegue en verde con Pods rotos es lo peor de ambos mundos.

Ejercicios

Ejercicio 1. Ensaya un despliegue fallido de aurora-api:2.1.0 que no pase la readiness: observa cómo se detiene solo, comprueba que ningún cliente recibe errores durante todo el proceso y deshazlo verificando el historial de revisiones.

Ejercicio 2. Implementa un blue-green completo: despliega la versión verde, valídala sin exponerla, conmuta el Service, mide el tiempo de conmutación y vuelve atrás.

Ejercicio 3. Aplica expand/contract para añadir la columna idioma a libros sin corte y demostrando que el rollback del código sigue siendo seguro en cada fase.

Soluciones

Solución 1.

# Carga continua en segundo plano durante todo el ejercicio
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/libros; 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 "codigo: $?"
# error: deployment "aurora-api" exceeded its progress deadline
# codigo: 1

kubectl get pods -n aurora -l app.kubernetes.io/name=aurora-api \
  -o custom-columns=POD:.metadata.name,LISTO:.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 lo demuestran todo. Hay ocho Pods en marcha pero solo seis endpoints: los dos Pods de la 2.1.0 existen y nunca entraron en el balanceo porque su readiness nunca devolvió 200. Y el vigía registró 1 983 respuestas, todas 200, durante un despliegue que ha fallado por completo.

El mecanismo tiene tres piezas encadenadas, y las tres se decidieron en lecciones anteriores. La readiness de 06-01 detectó que la 2.1.0 no podía servir; kube-proxy la excluyó de los endpoints, así que no recibió ni una petición; y maxUnavailable: 0 impidió retirar ningún Pod viejo, porque hacerlo habría bajado de seis disponibles. El despliegue quedó congelado en un estado seguro.

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/produccion (2.0.0)
# 4         kubectl set image ... aurora-api:2.1.0
# 5         kubectl rollout undo
#    2874 200

Cero errores en todo el ejercicio: despliegue fallido, espera y rollback incluidos. Fíjate en que el undo fue prácticamente instantáneo, y la razón es que no arrancó nada: los seis Pods de la 2.0.0 nunca dejaron de existir, así que deshacer consistió en borrar los dos Pods rotos y devolver el ReplicaSet antiguo a seis réplicas deseadas. Un rollback después de un despliegue exitoso sí tarda, porque hay que recrear los Pods viejos.

Y el codigo: 1 del rollout status es la pieza que faltaba en el pipeline de 06-02: basta con no ignorar ese código de salida para que un despliegue así ponga el job en rojo y dispare el undo automáticamente.

Solución 2.

kubectl apply -f k8s/blue-green/verde.yaml
kubectl wait --for=condition=available deploy/aurora-api-verde -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-verde 8081:3000 &
curl -s localhost:8081/libros | jq -r '"\(.origen) \(.libros|length)"'
# 6
# db 9

Detalle importante del primer número: aunque el Deployment verde ya está listo, el Service sigue teniendo seis endpoints, los del azul. La versión nueva está viva, validada por port-forward y sirviendo los nueve títulos, y no recibe ni una petición de usuario. Esa separación entre "desplegado" y "expuesto" es la esencia del blue-green.

inicio=$(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 "conmutacion: $(( $(date +%s%3N) - inicio )) ms"
kubectl logs -n aurora vigia --tail=200 | sort | uniq -c
# 6
# conmutacion: 412 ms
#     200 200
kubectl patch svc aurora-api -n aurora -p '{"spec":{"selector":{"version":"2.0.0"}}}'   # vuelta atrás
Estrategia Tiempo de conmutación Tiempo de rollback Recursos durante
Rolling update (6 réplicas) ~90 s ~90 s 6 + 2 Pods
Blue-green 0,4 s 0,4 s 12 Pods

Los 412 milisegundos son el argumento entero del blue-green, y el 12 de la última columna es su factura. La conmutación es tan rápida porque no arranca ni para nada: solo reescribe un selector, y el controlador de endpoints recalcula qué Pods pertenecen al Service.

Dos matices que la tabla no muestra. El primero: las conexiones HTTP keep-alive ya establecidas contra Pods azules siguen sirviéndose desde la versión vieja hasta que se cierran, así que la conmutación no es atómica desde el punto de vista del usuario. El segundo, más serio: mientras los dos entornos conviven, ambas versiones están escribiendo en la misma base de datos y en la misma caché, con lo que la compatibilidad de la sección 1 sigue siendo obligatoria. Blue-green resuelve el enrutado, no la compatibilidad de datos.

Solución 3.

-- FASE 1 (EXPAND) — se despliega SOLA, sin tocar el código
ALTER TABLE libros ADD COLUMN idioma VARCHAR(8) DEFAULT 'es';
kubectl exec -n aurora aurora-db-0 -- psql -U aurora -d aurora_libros -f /tmp/expand.sql
curl -s localhost:8080/libros | jq -r '"\(.origen) \(.libros|length)"'   # con la 2.0.0 aún
# db 9

La 2.0.0 sigue devolviendo los nueve títulos con una columna nueva que ignora por completo, porque su SELECT nombra las columnas que necesita. Esa es la propiedad que hace segura la fase 1: un cambio aditivo con valor por defecto es invisible para el código antiguo.

# FASE 2 (MIGRATE) — ahora sí, el código nuevo, con 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/libros | jq -r '.libros[1] | "\(.titulo) [\(.idioma)]"'
kubectl rollout undo deploy/aurora-api -n aurora     # rollback de prueba, a mitad
curl -s localhost:8080/libros | jq -r '.libros[1].titulo'
# Rayuela [es]      ← con la 2.1.0
# Rayuela           ← tras el rollback a la 2.0.0

Esas dos líneas son la demostración pedida. Con la 2.1.0, /libros incluye el idioma; tras el rollback a la 2.0.0, el campo desaparece de la respuesta y todo lo demás sigue funcionando. La base de datos no ha tenido que cambiar en ningún momento, porque la columna era compatible con las dos versiones.

-- FASE 3 (CONTRACT) — días después, cuando ninguna versión antigua queda viva
ALTER TABLE libros ALTER COLUMN idioma SET NOT NULL;
Fase Qué se despliega ¿Rollback del código seguro? ¿Rollback del esquema necesario?
1. Expand Solo esquema (aditivo) Sí (no ha cambiado) No
2. Migrate Solo código (2.1.0) No
3. Contract Solo esquema (restrictivo) Sí, si ya no hay versiones viejas No

La columna decisiva es la tercera: en ninguna de las tres fases hace falta deshacer el esquema, y ese es exactamente el objetivo del patrón. Comparado con el enfoque acoplado, la diferencia es abismal: si hubieras desplegado el ALTER TABLE ... NOT NULL junto con la 2.1.0, el rollback del código habría dejado a la 2.0.0 insertando filas sin idioma contra una columna obligatoria, y cada alta habría fallado.

La fase 3 exige una disciplina que se olvida con facilidad: no puede ejecutarse hasta estar seguro de que ninguna versión antigua sigue viva, y eso incluye trabajos por lotes, procesos de informes o integraciones de terceros que quizá nadie recuerde. Por eso lo prudente es dejar pasar un ciclo completo —días, no minutos— entre la fase 2 y la 3. Y por eso el plan de migración, la ventana y el procedimiento de vuelta atrás se acuerdan con el responsable de datos antes de tocar producción.

Conclusión

Ya sabes publicar sin que nadie deje de poder comprar un libro. Tienes claro el problema de fondo —durante todo despliegue conviven dos versiones sobre la misma base de datos y la misma caché— y las cinco estrategias con su coste real: recreate y su corte, el rolling update como defecto sensato, blue-green con su conmutación de 412 milisegundos a cambio del doble de recursos, el canario como la opción de menor riesgo cuando tienes métricas fiables, y shadow con su advertencia sobre los efectos secundarios duplicados.

Dominas el rolling update por dentro: maxSurge: 2 con maxUnavailable: 0 para no perder capacidad ni un instante, minReadySeconds para no dar por bueno un Pod que se cae dos segundos después, progressDeadlineSeconds como límite, y la tabla de equivalencias con el update_config de Swarm. Sobre todo, has comprobado por qué la sonda de readiness de 06-01 es la pieza que sostiene todo lo demás: el ensayo del despliegue fallido de aurora-api:2.1.0 terminó con ocho Pods en marcha, solo seis endpoints y 1 983 respuestas, todas 200, con el despliegue congelado en un estado seguro y un rollout status devolviendo código 1 para que el pipeline lo detecte. El undo fue instantáneo porque no había nada que arrancar, y el historial de revisiones siempre avanza, nunca borra.

Has montado el blue-green conmutando un selector —el acoplamiento por etiquetas de 06-04 rindiendo—, el canario por réplicas y por anotaciones del Ingress con su cabecera para el equipo, y sabes qué se vigila durante el canario y con qué criterio doble de tiempo y volumen se promociona o se aborta. Conoces Argo Rollouts y Flagger para automatizarlo, y GitOps con Argo CD o Flux invirtiendo la dirección del despliegue para que CI no necesite credenciales del clúster. Y has resuelto lo que ninguna estrategia arregla sola: el patrón expand/contract en tres despliegues, aplicado a añadir idioma a libros, con la demostración de que el rollback del código es seguro en todas las fases porque el esquema nunca se despliega acoplado. Cierran el cuadro los feature flags, que separan desplegar de activar y convierten una vuelta atrás de minutos en una de segundos.

Y con esto se cierra el módulo 6. Aurora Libros entró siendo una pila de Compose en tu portátil y sale convertida en una plataforma: una imagen 2.0.0 apta para producción, con la configuración fuera, validación al arrancar que falla en 0,4 segundos, apagado ordenado sin perder una sola petición y tres sondas con semánticas separadas; un pipeline que en cada git push prueba contra PostgreSQL y Redis reales, construye para dos arquitecturas en 41 segundos gracias a la caché remota, escanea, firma con Cosign y publica; un clúster —primero Swarm con sus redes overlay y su malla de enrutamiento, después Kubernetes con los cuatro servicios en manifiestos reales, StatefulSet para los datos, Kustomize por entorno y /libros devolviendo los nueve títulos—; un HorizontalPodAutoscaler que llevó el p95 de 1 840 ms a 88 ms sin que nadie tocara nada, con PgBouncer resolviendo el cuello de botella que estaba donde no mirabas; y, ahora, publicaciones sin corte con vuelta atrás probada. Aurora Libros ya no vive en tu máquina: vive en un clúster, se despliega sola desde un commit, crece con la carga y se actualiza sin que ningún cliente lo note.

En el módulo 7 levantamos la vista del proyecto para mirar alrededor. Verás cómo se aprovisionan los hosts que sostienen todo esto, cuándo conviene Compose y cuándo Kubernetes con una comparación honesta de ambos, qué aporta y qué cuesta Docker Desktop, qué herramientas y plugins de terceros merecen un hueco en tu flujo de trabajo, cómo encajan Podman, containerd y el estándar OCI —esas imágenes tuyas que ya sabes que no son "de Docker"— y hacia dónde va el ecosistema de contenedores.

Docker: De Principiante a Avanzado

Módulo 1: Introducción a Docker

Módulo 2: Trabajando con Imágenes Docker

Módulo 3: Contenedores Docker

Módulo 4: Docker Compose

Módulo 5: Conceptos Avanzados de Docker

Módulo 6: Docker en Producción

Módulo 7: Ecosistema y Herramientas de Docker

© Copyright 2026. Todos los derechos reservados