La migración que corría con una imagen distinta a la de la app
El servicio que aplica migraciones usaba el mismo Dockerfile que la API, y por eso parecía la misma imagen. No lo era. Reconstruir sólo una dejó la base migrada por código viejo y la API devolviendo errores.
Un despliegue de rutina: reconstruir la imagen de la API, levantar, listo. La API arrancó devolviendo errores en todos los endpoints que tocaban la base.
La causa: el servicio que aplica las migraciones tiene su propia imagen, aunque comparta el Dockerfile con la API. Reconstruir una no reconstruye la otra, y la base quedó migrada por la versión anterior del código.
Por qué son dos imágenes si el Dockerfile es uno
En una herramienta de composición, cada servicio que declara una construcción produce su propia imagen etiquetada con el nombre de ese servicio, aunque el contexto y el archivo sean idénticos.
| Lo que uno piensa | Lo que pasa |
|---|---|
| Mismo Dockerfile, misma imagen | Una imagen por servicio, cada una con su etiqueta |
| Reconstruir la API alcanza | El servicio de migraciones sigue con la imagen construida la vez anterior |
| Si algo estuviera mal, fallaría el arranque | La migración corre bien: es código viejo aplicándose sin error |
Y hay un agravante de tiempos: la migración corre antes, termina bien, y la API arranca después contra un esquema que no es el que su código espera. El error aparece en la primera petición, no en el despliegue.
Cómo se ve desde afuera
La secuencia del incidente
- Se reconstruye y levanta sólo el servicio de la API.
- El servicio de migraciones se ejecuta con su imagen vieja y aplica el esquema de la versión anterior.
- La API nueva arranca, se conecta, y falla al consultar columnas o tablas que no existen.
- Los registros muestran errores de base, no de despliegue, y la primera hipótesis es un problema de datos.
Lo que quedó como regla
Cuatro cosas que cambiaron
- Construir siempre el conjunto completo de servicios que comparten código: migraciones, API y cualquier trabajador. Nunca uno solo.
- Levantar reconstruyendo, en vez de levantar sin construir después de haber construido a mano. Un solo comando que no permite el desfasaje.
- Verificar la versión después de desplegar, comparando lo que reporta la aplicación con el commit que se quería desplegar.
- Que el arranque verifique el esquema, cuando la herramienta lo permita: fallar al iniciar es mucho mejor que fallar en la primera petición de un usuario.
Antes de seguir, predecí
Lo que este incidente enseña sobre migraciones
Más allá de las imágenes, el episodio expone algo estructural: durante un despliegue conviven, por unos segundos o por horas, el esquema viejo y el código nuevo, o al revés.
Las reglas que hacen esa convivencia segura
- Migraciones compatibles hacia atrás. Agregar columnas y tablas antes de usarlas; el código viejo tiene que seguir funcionando con el esquema nuevo.
- Borrar en un despliegue posterior. Quitar una columna en el mismo despliegue que deja de usarla rompe cualquier instancia vieja que siga viva.
- Nunca renombrar en un paso. Agregar, escribir en las dos, migrar datos, dejar de leer la vieja, borrar. Cuatro despliegues, cero caídas.
- Migración idempotente y verificable, porque va a correr de nuevo cuando un despliegue falle por la mitad.
Más a fondo · nivel seniorQuién aplica las migraciones
Hacerlas correr al arrancar la aplicación es cómodo y peligroso cuando hay varias instancias: dos arrancando a la vez aplican lo mismo en paralelo. Un servicio dedicado que corre una sola vez, con un bloqueo en la base, es lo que evita la carrera. Y tiene una ventaja operativa: se puede ejecutar y verificar antes de levantar lo demás, en lugar de descubrir el resultado cuando la aplicación ya está sirviendo tráfico.
Las dos imágenes, versión por versión
Estado inicial: las dos imágenes construidas de la misma versión del código. Todo consistente.
Migraciones que se pueden desplegar
Cierre
Autoevaluación
¿Lo entendiste?
Práctica