Rollback y deploys sin downtime

Guarda cada release como un artefacto versionado e inmutable, así el rollback es solo volver a desplegar el anterior. Para cero downtime, arranca la versión nueva, espera su health check y luego mueve el tráfico.

Actualizado el 6 min de lectura

Objetivos de aprendizaje

Después de leer este artículo podrás:

  • Dejar cada release listo para rollback con imágenes versionadas y un deshacer de un comando
  • Elegir entre deploys rolling, blue-green y canary
  • Evitar peticiones perdidas y rollbacks rotos por apagados bruscos y migraciones de base de datos
En esta página

Cada deploy es una pequeña apuesta a que la versión nueva funciona. El rollback es la forma de cancelar la apuesta en segundos, y el deploy sin downtime es la forma de hacerla sin que los usuarios lo noten. Resuelven problemas distintos, pero dependen del mismo hábito: nunca sobrescribir lo que está corriendo y tener siempre lista la versión anterior.

Tres estrategias de release cubren casi todos los casos: rolling, blue-green y canary. Y dos fallas pueden romper cualquiera de ellas: el apagado brusco del proceso y las migraciones de base de datos.

v1viejav2nuevatráficoen esperainiciandotráficohealth check okrollbackdeploy de v2cambio de tráficotiempo
La v2 arranca junto a la v1 y solo recibe tráfico cuando pasa el health check; la v1 queda lista para el rollback.

¿Qué hace posible un rollback?#

Un rollback solo es rápido cuando la versión anterior todavía existe como un artefacto completo y ejecutable. Eso exige tres reglas:

  • Builds inmutables y versionados. Etiqueta las imágenes Docker con una versión o el SHA del commit, nunca solo con latest. A una etiqueta que se mueve no se puede volver.
  • Configuración separada del código. Las variables de entorno viven fuera del build, así revertir el código no revierte (ni conserva) en silencio la configuración equivocada. Mira variables de entorno.
  • Release viejo guardado. Conserva al menos las últimas imágenes o carpetas de release. Kubernetes, por ejemplo, guarda ReplicaSets antiguos para rollback, controlados por revisionHistoryLimit (si lo pones en 0, pierdes la posibilidad de volver atrás).

Con esas reglas en marcha, un rollback es volver a desplegar una versión anterior, no compilar de nuevo.

¿Cómo se hace un rollback, paso a paso?#

Docker Compose. Fija la imagen por etiqueta con una variable:

yaml
services:
  app:
    image: registry.example.com/myapp:${APP_VERSION}
    restart: unless-stopped

Despliega y vuelve atrás cambiando un solo valor:

bash
APP_VERSION=1.4.2 docker compose up -d   # despliegue
APP_VERSION=1.4.1 docker compose up -d   # rollback

Kubernetes. Los Deployments registran las revisiones por ti:

bash
kubectl rollout history deployment/myapp
kubectl rollout undo deployment/myapp                  # revisión anterior
kubectl rollout undo deployment/myapp --to-revision=2  # una revisión concreta

Solo se crea una revisión nueva cuando cambia la plantilla del pod (imagen, labels, env), así que escalar réplicas no ensucia el historial.

Servidor tradicional. Despliega cada release en su propia carpeta y apunta un symlink current a ella. Volver atrás es mover el enlace y recargar el proceso:

bash
ln -sfn /srv/myapp/releases/1.4.1 /srv/myapp/current

Después de cualquier rollback, comprueba que funcionó: llama al endpoint de health y revisa los logs de la aplicación para confirmar que la versión vieja es la que responde.

¿Qué estrategia sin downtime conviene usar?#

Estrategia Cómo funciona Costo Velocidad del rollback
Rolling Reemplaza las instancias de a pocas Sin entorno extra Otro rolling update
Blue-green Corre la vieja y la nueva lado a lado y cambia todo el tráfico El doble de capacidad durante el cambio Inmediata: volver a cambiar
Canary Envía primero una parte pequeña del tráfico a la versión nueva Requiere reparto de tráfico y métricas Inmediata: enviar 0%

Rolling es el modo por defecto en Kubernetes (maxSurge y maxUnavailable valen 25% por defecto). No necesita un entorno extra, pero durante un rato las dos versiones atienden tráfico a la vez, así que deben ser compatibles entre sí.

Blue-green deja intacto el entorno viejo (blue) mientras el nuevo (green) arranca y se prueba. Cuando cambias el router, todos los usuarios pasan a la vez, y volver a cambiarlo es tu rollback. El precio es mantener dos copias.

Canary limita el daño: 1% del tráfico, luego 10%, luego todos, vigilando la tasa de errores y la latencia entre cada paso. Vale la pena cuando tienes tráfico suficiente para que los números signifiquen algo.

Las tres necesitan los mismos dos ingredientes: al menos dos instancias detrás de un router (mira balanceadores de carga) y un health check que le diga al router cuándo la instancia nueva está lista.

¿Cómo recargar sin perder peticiones?#

No necesitas un orquestador para una versión básica de esto.

Nginx. nginx -s reload valida primero la configuración nueva. Si no es válida, Nginx sigue con la vieja. Si es válida, arrancan workers nuevos y los viejos terminan sus peticiones en curso antes de salir. Prueba siempre antes de recargar:

bash
nginx -t && nginx -s reload

PM2 (Node.js). En modo cluster, pm2 reload recarga los procesos sin downtime, a diferencia de pm2 restart, que mata y reinicia el proceso:

bash
pm2 start app.js -i 2     # dos instancias en modo cluster
pm2 reload app            # recarga sin downtime

Esto solo ayuda con dos instancias o más, y tu aplicación tiene que ser stateless: cada proceso tiene su propia memoria, así que guarda las sesiones en Redis o en una base de datos.

Health checks. Un router solo debe enviar tráfico a una instancia que demuestre que está lista. En Compose:

yaml
services:
  app:
    image: registry.example.com/myapp:${APP_VERSION}
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/healthz"]
      interval: 5s
      timeout: 3s
      retries: 3
      start_period: 10s

Haz que /healthz compruebe lo que la aplicación realmente necesita (por ejemplo, un ping a la base de datos), no que devuelva 200 en cuanto arranca el proceso. El ejemplo supone que tu imagen incluye wget; usa curl o un script pequeño si no lo tiene. Compose solo registra el resultado (el estado aparece en docker compose ps); tu router todavía tiene que actuar sobre él, leyendo ese estado o ejecutando su propia comprobación contra /healthz.

¿Por qué un deploy sigue perdiendo peticiones?#

Cuando un orquestador o un gestor de procesos detiene una instancia vieja, primero envía una señal de terminación (SIGTERM, o SIGINT con PM2). Si tu aplicación la ignora, las peticiones en curso se cortan. Manéjala dejando de aceptar conexiones nuevas y terminando las que están en marcha:

js
// server.js (Node.js, ES modules)
import http from "node:http";

const server = http.createServer((req, res) => {
  if (req.url === "/healthz") return res.end("ok");
  res.end("hello");
});
server.listen(3000);

let shuttingDown = false;
function shutdown() {
  if (shuttingDown) return;
  shuttingDown = true;
  // Deja de aceptar conexiones nuevas; el callback corre cuando terminan las abiertas.
  server.close(() => process.exit(0));
  // Red de seguridad: no quedarse colgado para siempre en una conexión trabada.
  setTimeout(() => process.exit(1), 10_000).unref();
}
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);

Otras causas habituales:

  • El router sigue enviando tráfico a una instancia que se está apagando. Se arregla con health checks y una pequeña espera para drenar las conexiones.
  • La instancia nueva recibe tráfico antes de terminar de calentar. Se arregla con una comprobación de disponibilidad y con start_period, para que un arranque lento no se marque como unhealthy.
  • Las conexiones largas, como los WebSockets. Un reinicio siempre las interrumpe, así que los clientes tienen que reconectarse solos.

¿Cómo rompen el rollback las migraciones de base de datos?#

El código vuelve atrás fácilmente. Los datos no. Si la versión 2 borra una columna que la versión 1 lee, volver a la versión 1 la hace fallar. En un release rolling o canary el problema es peor, porque las dos versiones corren a la vez contra la misma base de datos.

La solución habitual es el patrón expand and contract (expandir y contraer), repartido en releases separados:

  1. Expandir. Añade la columna o tabla nueva. Conserva la vieja. Las dos versiones funcionan.
  2. Migrar. Despliega código que escribe en ambas y lee de la nueva. Rellena las filas antiguas.
  3. Contraer. Cuando ninguna versión en ejecución use la columna vieja, y ya no necesites volver antes de ese punto, bórrala.

Reglas prácticas: haz que las migraciones sean compatibles con el release anterior, nunca renombres una columna en un solo paso (crea el nombre nuevo, copia, elimina el viejo más tarde) y saca un dump de la base de datos antes de cualquier migración destructiva. Mira cómo hacer un backup de una base de datos.

¿Qué revisar antes y después de cada release?#

Antes: los tests pasan, la versión anterior está etiquetada y sigue disponible, y la migración es compatible con la versión anterior. Despliega mediante un pipeline para que los pasos se repitan siempre igual, como en deploys automáticos con GitHub Actions.

Después: el endpoint de health responde desde la versión nueva, la tasa de errores y la latencia coinciden con la referencia de la versión vieja, y los logs no muestran excepciones nuevas. Decide de antemano qué dispara el rollback (por ejemplo, una tasa de errores por encima de un umbral fijo durante cinco minutos) para que nadie lo discuta en medio de un incidente.

Preguntas frecuentes

¿Cuál es la diferencia entre rollback y roll-forward?

Un rollback vuelve a desplegar la versión anterior que funcionaba. Un roll-forward publica un arreglo nuevo encima del release roto. El rollback suele ser más rápido, pero no es posible cuando una migración de base de datos no se puede deshacer.

¿Necesito Kubernetes para desplegar sin downtime?

No. Cualquier montaje con dos copias de la aplicación y algo que reparta el tráfico entre ellas sirve, por ejemplo Nginx delante de dos procesos, PM2 en modo cluster o Docker Compose con un proxy inverso.

¿Cuál es la diferencia entre blue-green y canary?

Blue-green mantiene dos entornos completos y cambia todo el tráfico de una vez. Un canary envía primero una parte pequeña del tráfico a la versión nueva y la amplía solo si las métricas siguen sanas.

¿Por qué un deploy pierde peticiones aunque tenga dos instancias?

Normalmente el proceso viejo se mata sin drenar conexiones. Las peticiones en curso se cortan, o el balanceador sigue enviando tráfico a una instancia que ya se está apagando.

Siguiente artículoQué son las variables de entornoLas variables de entorno son pares clave-valor que el sistema operativo entrega a un proceso al iniciar. Aprende a leerlas en Node.js y Python sin exponer tokens.

Más en Deploy

Ver los 33 artículos