46  35. Necesitamos cambiar producción sin tumbarla

Deploy a las 4 PM del viernes, con migración. ¿Qué puede salir mal?

NotaEn una frase

Deployar es reemplazar el avión en vuelo: la versión vieja y la nueva conviven unos minutos — y la BD es una sola para las dos. Por eso los deploys sin downtime no son un flag de la plataforma: son una disciplina — migraciones compatibles hacia atrás (expand/contract), rollback que sabe qué se revierte y qué no, y ojos en los logs después del deploy.

46.1 El problema

git push a prod a las 4 PM del viernes: la plataforma levanta la versión nueva mientras apaga la vieja. En esos 90 segundos, la v1.4 y la v1.5 conviven — y ambas hablan con la misma base de datos. Si la migración de la v1.5 renombró name → full_name, la v1.4 que sigue viva escribe name en una columna que ya no existe: errores por todas partes, en el mejor momento posible.

Producción (prod) es el entorno real: donde están los usuarios, los datos que importan y las consecuencias. Todo lo demás — local, staging — existe para que los errores ocurran antes de llegar ahí.

Una base de datos es el programa que guarda datos de forma permanente y los responde rápido. Relacional (Postgres): tablas con relaciones. No relacional: documentos, clave-valor, grafos — cada una para una forma de dato distinta.

Una migración es un cambio versionado del esquema: un archivo que dice “crea esta columna” (up) y “bórrala” (down). Son el historial Git de la estructura de la BD — se aplican en orden y no se editan una vez aplicadas.

46.2 Cómo lo resuelve un equipo

Un equipo serio asume la coexistencia: durante el deploy, dos versiones del código comparten una BD — por tanto la migración debe ser segura para las dos. La técnica se llama expand/contract:

Un deploy es poner tu código a correr en el servidor: copiar la imagen, iniciar el proceso, verificar que responde. El objetivo es que sea aburrido — automático, repetible, con rollback.

  1. Expand: agregar lo nuevo sin tocar lo viejo (columna nueva, tabla nueva — nunca renombrar ni borrar).
  2. Código que escribe en ambos (o migra datos en segundo plano).
  3. Contract: cuando todo usa lo nuevo, otra migración retira lo viejo — un deploy después, no en el mismo.

Y el seguro: el rollback de código es cambiar el tag de imagen; el rollback de datos no existe gratis — los datos que la v1.5 escribió en full_name no se des-escriben al volver a v1.4.

Un rollback es volver atrás: la migración se revierte, el deploy regresa a la versión anterior. Todo cambio serio tiene plan de rollback antes de ejecutarse — si no, el plan es rezar.

Una imagen es la plantilla inmutable de la que nacen contenedores: la foto del disco + cómo arrancar. Se construye en capas (Dockerfile), se versiona con tags, se publica en un registry.

46.3 Conceptos nuevos

  • Ops Deploys sin downtime (concepto): rolling, qué exige de tu app y de tus migraciones
  • D Migraciones compatibles: agregar columna sí, renombrar con datos en prod no — expand/contract
  • Ops Rollback: qué se revierte (código) y qué no se revierte gratis (datos)
  • Ops Post-deploy: logs, errores, latencia — observabilidad mínima viable

46.4 La explicación visual

sequenceDiagram
  participant LB as Balanceador
  participant V1 as v1.4 (vieja)
  participant V2 as v1.5 (nueva)
  participant DB as Postgres
  Note over LB,V2: Rolling deploy: las dos conviven
  LB->>V1: tráfico
  LB->>V2: tráfico (salud ok)
  V1->>DB: escribe columnas VIEJAS
  V2->>DB: escribe columnas VIEJAS + NUEVAS
  Note over V1: apaga cuando V2 está sana
  Note over DB: expand/contract = la BD<br/>sirve a las dos versiones

La regla de oro: la BD debe entender a la versión vieja y a la nueva a la vez — toda migración se evalúa contra esa pregunta.

46.5 Implementación

Capítulo agnóstico — la receta expand/contract del cambio clásico “name → first_name + last_name”:

-- Paso 1 (deploy N): EXPAND — add only, nothing removed
ALTER TABLE users ADD COLUMN first_name TEXT;
ALTER TABLE users ADD COLUMN last_name  TEXT;

-- Paso 2 (mismo deploy N): el código escribe las TRES columnas
-- (name sigue poblada para la v1.4 que aún vive)
-- + backfill en segundo plano:
UPDATE users SET first_name = split_part(name, ' ', 1),
                 last_name  = split_part(name, ' ', 2)
WHERE first_name IS NULL;

-- Paso 3 (deploy N+2, cuando TODO lee las nuevas): CONTRACT
ALTER TABLE users ALTER COLUMN first_name SET NOT NULL;
ALTER TABLE users DROP COLUMN name;

Tres deploys para “renombrar una columna” — parece excesivo hasta que comparas con la alternativa: el deploy que la renombra de golpe tumba la versión vieja en plena rotación.

Diseña el plan de deploy sin downtime para este cambio: renombrar
users.name → users.first_name + last_name en mi API [stack] con Postgres
y rolling deploys. Dame el plan expand/contract completo: qué
migraciones en qué orden, qué escribe cada versión del código mientras
conviven, cómo se hace el backfill sin bloquear la tabla (batches),
cuándo es seguro el DROP COLUMN, y qué se revierte (y qué NO) si hay
que hacer rollback en cada etapa.

46.6 ¿Por qué así y no “apagar, migrar, encender”?

Lo único que necesitas llevarte: el downtime programado funciona mientras tienes 40 usuarios; con usuarios reales el deploy es en caliente — y en caliente la BD es el punto único compartido, así que toda la disciplina es mantener la BD compatible con dos versiones.

Estrategia Qué es Qué te cuesta Cuándo
Rolling Reemplaza instancias de a una Convivencia de versiones (este capítulo) El default en orquestadores
Blue-green Dos entornos completos; el balanceador cambia de golpe Doble infra mientras dura Rollback instantáneo crítico
Canary La nueva versión recibe 1% del tráfico primero Más plomería de routing Cambios riesgosos a escala
Recreate Apagar todo, subir nuevo Downtime real Dev/staging, apps internas

46.7 Errores comunes

Error Por qué pasa Fix
Migración destructiva en el deploy DROP/RENAME con v1.4 viva Expand/contract — destructivo solo cuando nada viejo corre
Migración que bloquea la tabla ALTER pesado en horario Operaciones baratas en el hot path (ADD COLUMN es instantáneo en PG 11+); lo pesado, por pasos/ventana
Rollback = “deployar la imagen vieja” Olvida que los datos ya cambiaron Saber qué se revierte: código sí, datos escritos no — por eso expand/contract también es rollback-friendly
Deployar y mirar el techo “Salió verde el pipeline” Post-deploy: logs, tasa de errores, latencia los primeros 15 min (cap. 23)
Viernes 4 PM sin poder revertir Ritmo Deployar cuando hay equipo despierto — la valentía no es un plan

46.8 Buenas prácticas

  • Toda migración pasa el test: “¿la versión anterior del código sigue funcionando con este esquema?” — si no, va por pasos.

El esquema es el plano de la base de datos: qué tablas hay, qué columnas tiene cada una y de qué tipo. Es contrato: una fila que no cumple el esquema no entra. Se cambia con migraciones, no a mano.

  • Backfill en batches (UPDATE ... WHERE id BETWEEN o por lotes) — un UPDATE de millones de filas de golpe bloquea la tabla que atiende tráfico.
  • El deploy taggeado = el rollback: registry/app:sha-viejo — revertir código es cambiar una etiqueta, no rebuildar nada.
  • Post-deploy checklist: error rate, p95, logs de excepciones nuevas — los 15 minutos después del deploy son donde vive el downtime real.

46.9 Ejercicio

  1. ¿Por qué RENAME COLUMN name TO full_name en un solo deploy tumba la versión vieja aunque la nueva esté lista?
  2. Diseña el expand/contract para eliminar la columna legacy_flag — ¿qué va primero, el código o la migración?
  3. La v1.5 lleva 20 minutos en prod escribiendo en first_name cuando hay que hacer rollback a v1.4. ¿Qué se pierde y qué no?

Un array es una colección ordenada de elementos accedidos por posición: ["a","b","c"][0] es "a" (se cuenta desde 0). Python las llama listas, Go slices — misma idea, distinto acento.

46.10 Mini reto

Planear “dividir name en first_name/last_name” sin downtime.

Ejercicio.

  1. Porque durante el rolling, la v1.4 (que consulta name) sigue viva mientras la migración ya la renombró → sus queries fallan. Renombrar es destructivo para la otra versión — siempre hay dos versiones durante la ventana del deploy.
  2. Código primero: deploy que deja de leer legacy_flag (la columna queda huérfana pero presente — la v1.4 sigue feliz). Cuando nada la lee, la migración DROP COLUMN en el deploy siguiente — contract es el último paso, nunca el primero.
  3. Se revierte el código (imagen vieja, un tag) — los datos escritos en first_name durante esos 20 min no se revierten: v1.4 no los lee (lee name, que v1.5 también mantuvo escrita por diseño). Si la migración hubiera sido destructiva, el rollback sería una restauración de backup, no un tag.

Mini reto. El plan de la sección Implementación de arriba: expand (agregar columnas) → dual-write + backfill en batches → todo el código lee lo nuevo → contract (NOT NULL + DROP). Cada paso es deploy seguro porque cada esquema intermedio sirve a ambas versiones — esa es la frase que se responde en la entrevista.

46.11 Vocabulario técnico del capítulo

Una clase es el molde de un objeto: define qué datos y qué métodos tiene. class Task es el molde; new Task() es una instancia concreta. Go no tiene clases — usa struct + métodos sueltos.

Latencia = cuánto tarda UNA operación (ms). Throughput = cuántas haces por segundo. Se pelean: bajar latencia no siempre sube throughput. La latencia la siente el usuario; el throughput lo paga tu factura.

Vertical: máquina más gorda (más CPU/RAM) — fácil, tiene techo. Horizontal: más máquinas — el camino de internet, pero requiere que la app no guarde estado local (por eso todo va a BD/Redis).

Staging es el ensayo general: una copia de producción (misma config, datos falsos o anonimizados) donde pruebas el deploy antes de hacerlo de verdad. Si falla ahí, falló gratis.

Un load balancer reparte el tráfico entre tus réplicas: request entra → lo manda a la instancia menos ocupada. Además detecta cuál está caída (health checks) y deja de enviarle.

Kubernetes es el orquestador de contenedores: le dices “quiero 3 réplicas de esta imagen” y él las distribuye, reinicia las que mueren y las expone. Poderoso y complejo — se usa cuando las máquinas ya son manada.

Un backup es una copia de los datos en otro lugar, hecha seguido y probada (un backup que nunca se restauró no es backup, es esperanza). Regla 3-2-1: 3 copias, 2 medios, 1 fuera del sitio.

Un entorno es una instancia completa donde corre tu app con su propia config y datos: local (tu máquina), staging (réplica de prueba), producción (la real). Cada entorno tiene sus propias llaves y su propia base de datos.

Una query es la pregunta que le haces a la base de datos en SQL: SELECT * FROM tasks WHERE done = false. La BD traduce la pregunta a un plan de búsqueda — por eso los índices importan.

Un log es el diario del programa: qué pasó, cuándo, con qué request. Estructurado = en JSON con campos (request_id, user_id), para filtrar por máquina y no con los ojos.

Un registry es el almacén de imágenes (Docker Hub, ECR, Artifact Registry): el CI empuja nexus:v42, el servidor la jala. Es el intermediario entre “se construyó” y “está corriendo”.

CI (integración continua): cada push corre tests y build automático. CD (despliegue continuo): si todo pasa, se despliega solo. El pipeline es la cadena de pasos — GitHub Actions es el motor típico.

46.12 Lo que deberías saber hacer ahora