MÓDULO 18 · CI/CD + MONITORING EN PRODUCCIÓN · 35 MIN

Desplegar sin que se note

En cualquier despliegue gradual conviven dos versiones del código contra una sola base de datos. Ahí está todo el problema: la estrategia de despliegue y la migración de esquema no son dos decisiones, son la misma. Rolling, blue-green y canary solo funcionan si el esquema es compatible con las dos versiones a la vez — y hacerlo compatible es el patrón expand-contract.
01 — Las tres estrategias
Y la fila que las une
RollingBlue-greenCanary
CómoSustituye instancias por tandasDos entornos, se conmuta el tráficoUn % pequeño a la versión nueva
RollbackGradualInstantáneo: volver a blueAfecta solo al canary
CosteLos mismos recursosEl doble durante el despliegueLos mismos + enrutado
CuándoEl caso por defectoCambios grandes, rollback críticoCambios arriesgados, A/B
No usar siv1 y v2 son incompatiblesLos recursos están justosEl cambio de esquema es incompatible

La última fila es la que importa, y las tres celdas dicen lo mismo desde ángulos distintos: ninguna de las tres sirve si las dos versiones no pueden coexistir. Blue-green parece la excepción porque conmuta de golpe — pero la base de datos no conmuta con él, y ahí es donde la estrategia se rompe.

02 — El par que más se confunde
Liveness y readiness

Liveness probe

¿Está vivo el proceso?

Si falla: reinicia el contenedor.

Debe comprobar solo que el proceso responde. Se mantiene tonto a propósito.

Readiness probe

¿Puede atender tráfico?

Si falla: lo saca del balanceo, sin reiniciarlo.

Aquí sí se comprueban las dependencias: base de datos, caché, carga inicial.

Confundirlos tiene una consecuencia muy concreta y muy fea: si el liveness comprueba la base de datos, una caída de la base de datos reinicia todas tus instancias. Te quedas sin base de datos y sin aplicación, y el reinicio en bucle además impide que el servicio se recupere solo cuando la base vuelve.

app.get('/health', (req, res) => res.json({ status: 'ok' }));   // liveness: tonto

app.get('/ready', async (req, res) => {                         // readiness: comprueba
  try {
    await db.query('SELECT 1');
    await redis.ping();
    res.status(200).json({ status: 'ready' });
  } catch (e) {
    res.status(503).json({ status: 'not ready' });
  }
});
03 — El pico de 5xx que nadie explica
Graceful shutdown

Un despliegue rolling apaga instancias que están sirviendo peticiones. Sin un apagado ordenado, esas peticiones se pierden — y en el panel eso aparece como un pico de errores 5xx en cada despliegue que el equipo acaba dando por normal.

  1. Llega SIGTERM.
  2. Dejar de aceptar conexiones nuevas (server.close()).
  3. Esperar a que terminen las peticiones en curso.
  4. Cerrar las conexiones a la base de datos.
  5. Salir.

En Kubernetes lo acompañan terminationGracePeriodSeconds —cuánto se espera antes de matar el proceso por las bravas— y un preStop con una espera corta, para dar tiempo al balanceador a dejar de mandar tráfico antes de que el proceso empiece a cerrarse. Ese desfase de unos segundos es la causa habitual de que el apagado ordenado esté implementado y aun así se pierdan peticiones.

04 — Compatibilidad de esquema
Expand-contract, fase a fase
1 · Expand
Esquema: añadir la columna nueva sin tocar la vieja rollback → eliminarla
2 · Expand
Código: escribir en las dos columnas rollback → desplegar la versión anterior
3 · Migrate
Datos: rellenar la nueva por lotes rollback → poner la columna nueva a nulo
4 · Migrate
Código: leer solo de la nueva rollback → desplegar la versión anterior
5 · Contract
Esquema: eliminar la columna vieja rollback → no existe
// el rollback de la fase 5 no existe: la columna ya no está, y los datos tampoco

Por eso la quinta se ejecuta días o semanas después del resto, cuando ya se sabe que nada la usa. Y por eso el backfill de la fase 3 va por lotes con una pausa entre ellos: un UPDATE masivo sobre millones de filas bloquea la tabla justo el rato que se intentaba evitar.

La regla que ordena todo lo demás:

OperaciónOrdenPor qué
Añadir columnaEsquema primeroEl código nuevo necesita algo que aún no existe
Eliminar columnaCódigo primeroHay que dejar de usarla antes de quitarla
RenombrarExpand-contractEs añadir y eliminar a la vez
Auditoría Dos cosas de esta parte. La guía propone SELECT * FROM pg_stat_activity WHERE waiting; para ver bloqueos durante una migración: ¿existe todavía la columna waiting en esa vista, o fue sustituida hace ya varias versiones mayores por wait_event_type y wait_event? Si es lo segundo, la consulta no devuelve un resultado vacío — falla, y falla justo en el momento en que se está mirando una migración en producción. Y la segunda: el capítulo abre con «ejecutan ALTER TABLE users RENAME COLUMN en producción y la tabla se bloquea durante 20 minutos». ¿Sobre qué motor? Conviene comprobar si un renombrado de columna no es una operación de metadatos —casi instantánea, aunque tome un bloqueo exclusivo— en el mismo motor que la guía usa en todos sus ejemplos. Si lo es, la anécdota que justifica el capítulo entero está describiendo un problema que ese motor no tiene, y el capítulo sigue siendo útil por otras razones.
Para hacer
Escribir los cinco rollbacks

Dibuja las cinco fases de un renombrado de columna sobre una tabla tuya y escribe al lado el rollback de cada una. Cuando llegues a la quinta y no puedas escribir nada, ya has entendido la lección.

La única fase irreversible es la última, y por eso es la única que no tiene prisa. Ahora las dos preguntas que decides tú y no el patrón: ¿cuánto tiempo dejarías pasar entre la cuarta y la quinta? ¿Y qué comprobarías antes de ejecutarla — una búsqueda en el código, una consulta de uso real de la columna, ambas?