Aller au contenu
HA_

← Tous les articles

· 3 min de lecture

Des migrations qui échouent en sécurité

Comment ClickFortify exécute ses migrations Prisma sous forme de hook PreSync ArgoCD, afin qu’une migration défaillante arrête la release avant le démarrage du moindre nouveau pod.

devopskubernetesarchitecture

Traduction automatique. Lire l’original

Un changement de schéma est la partie la plus risquée de la plupart des déploiements. Un nouveau code peut être annulé en quelques secondes. Une base de données restée à mi-chemin entre deux structures, non. Chez ClickFortify, dont je suis l’architecte principal, les services tournent sur Kubernetes et ArgoCD les déploie en GitOps. J’attendais une seule propriété de l’étape de migration : si elle échoue, rien de nouveau ne tourne.

C’est ce que j’entends par « échouer en sécurité » (fail closed). Une migration en échec doit arrêter la release, et non laisser de nouveaux pods démarrer face à un schéma qu’ils n’attendent pas.

Où la migration s’exécute

Les services NestJS de ClickFortify utilisent PostgreSQL via Prisma. Les migrations sont appliquées avec prisma migrate deploy, qui exécute dans l’ordre toutes les migrations en attente et ne modifie rien d’autre.

La question était de savoir où l’exécuter. Le lancer dans le conteneur applicatif au démarrage, c’est laisser chaque réplica se précipiter pour migrer, et un échec se traduit par des pods en boucle de crash. Le lancer à la main depuis un ordinateur portable, c’est dépendre de la mémoire de quelqu’un. Aucune des deux options n’échoue en sécurité.

La migration est donc un Job Kubernetes qu’ArgoCD exécute comme hook PreSync. ArgoCD lance les hooks PreSync avant d’appliquer le reste des manifestes. Si le Job du hook échoue, la synchronisation s’interrompt et le nouveau Deployment n’est jamais appliqué.

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']

Ce manifeste est illustratif et n’est pas copié d’un manifeste réel.

La même image que la release

Le Job utilise la même image que la release qu’il protège. Déployer consiste à committer un nouveau tag d’image dans le dépôt surveillé par ArgoCD, et ce tag est appliqué au Job de migration comme au Deployment api. Les migrations exécutées sont donc exactement celles livrées avec le code sur le point de passer en production. Il n’existe pas d’image de migration séparée susceptible de se désynchroniser.

L’état de déploiement vit dans son propre dépôt, séparé du code applicatif. Cela évite à ArgoCD de recloner un gros monorepo à chaque rafraîchissement, et ce qu’ArgoCD déploie reste un historique court et relisible de tags et de manifestes.

Pas de nouvelles tentatives à l’aveugle

backoffLimit: 0 est volontaire. Kubernetes relancerait volontiers un Job en échec, mais une migration interrompue en cours de route a pu laisser le schéma à moitié appliqué. La relancer sans regarder peut aggraver la situation. Une seule tentative, puis on s’arrête et on laisse une personne lire les logs.

activeDeadlineSeconds fixe une limite à la durée pendant laquelle le Job peut rester bloqué, par exemple sur un verrou, avant d’être considéré comme en échec.

Des logs qui restent

La politique de suppression du hook est BeforeHookCreation. ArgoCD ne supprime l’ancien Job qu’au moment de créer le suivant. D’ici là, le Job en échec et les logs de son pod restent dans le cluster, précisément quand on en a besoin. Une politique qui supprime le hook en cas d’échec jetterait les preuves.

Une exécution saine se reconnaît facilement dans ces logs : Prisma indique les migrations trouvées, puis soit la liste de celles appliquées, soit qu’aucune n’était en attente, et le Job se termine.

L’ordre peut compter

Exécuter les migrations avant les nouveaux pods est le bon comportement par défaut, et c’est parfois indispensable. Prenons une release où le nouveau code cesse d’écrire une colonne, et où la valeur par défaut de cette colonne n’est plus adaptée. La migration qui corrige les lignes existantes et la valeur par défaut doit arriver avant le démarrage du nouveau code, sinon les nouvelles lignes reçoivent l’ancienne valeur par défaut sans que rien ne la corrige.

Un hook PreSync garantit cet ordre sans effort supplémentaire.

Ce que je conseillerais à quelqu’un qui met cela en place

  1. Exécutez les migrations dans un hook pré-déploiement capable d’interrompre la release, pas au démarrage de l’application.
  2. Construisez le Job de migration à partir du même tag d’image que la release, pour que le code et le schéma ne divergent pas.
  3. Définissez backoffLimit: 0 et conservez les logs des Jobs en échec ; un schéma à moitié appliqué a besoin d’une personne, pas d’une nouvelle tentative.
  4. Déployez uniquement par synchronisation, car un redémarrage saute les hooks.