Ir al contenido
Eitan FeldmanBA, ARGCVenes
Notas

Siete deploys en verde que nunca entraron

La API de Restoman sirvió durante un día el build de la noche anterior mientras siete deploys daban éxito. Una migración trabada y un comando que salía con 0 ante un fallo se ocultaban entre sí.

Durante unas veinticuatro horas, nada de lo que mergeamos llegó a producción. Corrieron siete deploys, los siete quedaron en verde y la API siguió sirviendo el build de la noche anterior. Nadie se dio cuenta porque nada estaba caído: el contenedor viejo seguía respondiendo. Lo encontramos de casualidad mientras aplicábamos migraciones en producción.

Dos bugs, cada uno tapando al otro

El contenedor nuevo no podía arrancar

El contenedor corre ./migrate && exec ./server. El migrador de Drizzle decide qué está pendiente por nombre, y saltea las filas que no tienen nombre.

Una migración se había aplicado por fuera con drizzle-kit, que la registró sin nombre. En cada arranque, el migrador la creía pendiente y volvía a correr su ALTER TABLE ... ADD COLUMN. Postgres contestaba que la columna ya existía, migrate salía con 1, el && cortaba ahí y el servidor nunca llegaba a escuchar. El health check fallaba, así que la herramienta de deploy conservaba el contenedor anterior — exactamente lo que tenía que hacer.

El rollout fallido pasaba el CI

haloy deploy sale con 0 aunque un rollout falle el health check. El paso de GitHub Actions era un run: haloy deploy pelado, así que el job quedaba en verde. Siete veces.

El primer bug frenaba cada versión nueva. El segundo reportaba que había entrado. Juntos dejaron a producción lo bastante sana como para ocultar que llevaba un día de atraso.

¿Por qué no borrar la fila?

Borrar la fila sin nombre, o reescribir la migración con IF NOT EXISTS, también habría dejado arrancar al contenedor. Las dos opciones contestan la única pregunta que importa —¿esta migración se aplicó?— con una suposición sobre la base de producción.

En cambio, reparamos el estado:

  • Antes de migrar, el script ahora relaciona las filas sin nombre con nuestras migraciones por hash y les devuelve el nombre. Un hash que coincide es evidencia de que la migración ya corrió; registrar ese dato evita volver a ejecutar el DDL o modificar datos de la aplicación.
  • El workflow de deploy lee la salida de haloy y falla cuando el rollout falla o nunca reporta un health check sano.

Verificamos el arreglo reproduciendo el estado exacto en desarrollo — una fila sin nombre para una migración que ya había corrido — y ejecutando el mismo binario que corre el contenedor. Salió con 0, reparó la fila y no ejecutó ningún DDL dos veces.

Lo que me llevo

  • El estado de las migraciones se verifica por hash, nunca por nombre. El nombre es una etiqueta; el hash es la evidencia.
  • Un deploy que no puede hacer fallar el CI no es un deploy verificado. Si la herramienta sale con 0 ante un rollout fallido, hay que leer su salida y hacer fallar el job.
  • Un health check en verde no prueba que esté corriendo el código nuevo. La confirmación barata es pedirle a la API algo que solo la versión nueva sepa contestar.