Saltar al contenido
HA_

← Todos los artículos

· 4 min de lectura

Migraciones que fallan de forma segura

Cómo ClickFortify ejecuta las migraciones de Prisma como un hook PreSync de ArgoCD, de modo que una migración rota detiene el despliegue antes de que arranque cualquier pod nuevo.

devopskubernetesarchitecture

Traducción automática. Leer el original

Un cambio de esquema es la parte más arriesgada de la mayoría de los despliegues. El código nuevo se puede revertir en segundos. Una base de datos que se ha quedado a medio camino entre dos estructuras, no. En ClickFortify, donde soy el arquitecto principal, los servicios se ejecutan en Kubernetes y ArgoCD los despliega mediante GitOps. Quería una sola propiedad del paso de migración: si falla, no se ejecuta nada nuevo.

A eso me refiero con fallar de forma segura. Una migración fallida debe detener el despliegue, no permitir que arranquen pods nuevos contra un esquema que no esperan.

Dónde se ejecuta la migración

Los servicios NestJS de ClickFortify usan PostgreSQL a través de Prisma. Las migraciones se aplican con prisma migrate deploy, que ejecuta en orden todas las migraciones pendientes y no cambia nada más.

La pregunta era dónde ejecutarlo. Ejecutarlo dentro del contenedor de la aplicación al arrancar significa que todas las réplicas compiten por migrar, y un fallo aparece como pods en un bucle de reinicios. Ejecutarlo a mano desde un portátil significa que depende de que alguien se acuerde. Ninguna de las dos opciones falla de forma segura.

En su lugar, la migración es un Job de Kubernetes que ArgoCD ejecuta como un hook PreSync. ArgoCD ejecuta los hooks PreSync antes de aplicar el resto de los manifiestos. Si el Job del hook falla, la sincronización se aborta y el nuevo Deployment nunca se aplica.

apiVersion: batch/v1
kind: Job
metadata:
  name: migrate
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
  backoffLimit: 0
  activeDeadlineSeconds: 600
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migrate
          image: registry.example.com/api:sha-abc1234
          command: ['npx', 'prisma', 'migrate', 'deploy']

Este manifiesto es ilustrativo, no está copiado de uno real.

La misma imagen que el despliegue

El Job ejecuta la misma imagen que el despliegue que protege. Desplegar significa confirmar una nueva etiqueta de imagen en el repositorio que vigila ArgoCD, y esa etiqueta se aplica tanto al Job de migración como al Deployment de la API. Así, las migraciones que se ejecutan son exactamente las que se entregan con el código que está a punto de entrar en producción. No hay una imagen de migraciones aparte que pueda desincronizarse.

El estado del despliegue vive en su propio repositorio, separado del código de la aplicación. Eso evita que ArgoCD vuelva a clonar un monorepo grande en cada actualización, y significa que lo que despliega ArgoCD es un historial pequeño y revisable de etiquetas y manifiestos.

Sin reintentos a ciegas

backoffLimit: 0 es deliberado. Kubernetes reintentaría sin problema un Job fallido, pero una migración que falló a medias puede haber dejado el esquema aplicado a medias. Volver a ejecutarla sin mirar puede empeorarlo. Un intento, luego parar y dejar que una persona lea los logs.

activeDeadlineSeconds pone un límite al tiempo que el Job puede quedarse bloqueado, por ejemplo en un lock, antes de contar como fallido.

Logs que sobreviven

La política de borrado del hook es BeforeHookCreation. ArgoCD borra el Job anterior solo cuando está a punto de crear el siguiente. Hasta entonces, el Job fallido y los logs de su pod permanecen en el clúster, que es justo cuando los necesitas. Una política que borrara el hook al fallar tiraría las pruebas.

Una ejecución correcta es fácil de reconocer en esos logs: Prisma informa de las migraciones que encontró, después la lista de las que aplicó o que no había ninguna pendiente, y el Job termina.

El orden puede importar

Ejecutar las migraciones antes de los pods nuevos es el comportamiento por defecto correcto, y a veces es obligatorio. Pensemos en un despliegue en el que el código nuevo deja de escribir una columna, y el valor por defecto de esa columna ya no es correcto en la nueva situación. La migración que corrige las filas existentes y el valor por defecto tiene que aplicarse antes de que arranque el código nuevo, o las filas nuevas recibirán el valor por defecto obsoleto sin nada que lo corrija.

Un hook PreSync ofrece ese orden sin coste adicional.

Lo que le diría a alguien que vaya a configurar esto

  1. Ejecuta las migraciones como un hook previo al despliegue que pueda abortarlo, no al arrancar la aplicación.
  2. Construye el Job de migración a partir de la misma etiqueta de imagen que el despliegue, para que código y esquema no puedan desincronizarse.
  3. Configura backoffLimit: 0 y conserva los logs de los Jobs fallidos; un esquema aplicado a medias necesita una persona, no un reintento.
  4. Despliega solo mediante sincronización, porque un reinicio se salta los hooks.