Zum Inhalt springen
HA_

← Alle Artikel

· 3 Min. Lesezeit

Migrationen, die sicher scheitern

Wie ClickFortify Prisma-Migrationen als ArgoCD-PreSync-Hook ausführt, damit eine fehlerhafte Migration das Release stoppt, bevor ein neuer Pod startet.

devopskubernetesarchitecture

Maschinell übersetzt. Original lesen

Eine Schemaänderung ist bei den meisten Deployments der riskanteste Teil. Neuer Code lässt sich in Sekunden zurückrollen. Eine Datenbank, die halb zwischen zwei Strukturen steckt, nicht. Bei ClickFortify, wo ich Lead Architect bin, laufen die Services auf Kubernetes, und ArgoCD deployt sie per GitOps. Vom Migrationsschritt wollte ich eine Eigenschaft: Wenn er fehlschlägt, läuft nichts Neues.

Das meine ich mit „sicher scheitern“ (fail closed). Eine fehlgeschlagene Migration soll das Release stoppen und nicht zulassen, dass neue Pods gegen ein Schema starten, das sie nicht erwarten.

Wo die Migration läuft

Die NestJS-Services von ClickFortify nutzen PostgreSQL über Prisma. Migrationen werden mit prisma migrate deploy angewendet. Der Befehl führt alle ausstehenden Migrationen der Reihe nach aus und ändert sonst nichts.

Die Frage war, wo er laufen soll. Läuft er beim Start im Anwendungscontainer, liefern sich alle Replikas ein Rennen um die Migration, und ein Fehler zeigt sich als Pods in einer Crash-Schleife. Läuft er von Hand auf einem Laptop, hängt er davon ab, dass jemand daran denkt. Keines von beiden scheitert sicher.

Stattdessen ist die Migration ein Kubernetes-Job, den ArgoCD als PreSync-Hook ausführt. ArgoCD führt PreSync-Hooks aus, bevor es die übrigen Manifeste anwendet. Schlägt der Hook-Job fehl, bricht die Synchronisierung ab, und das neue Deployment wird nie angewendet.

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

Dieses Manifest dient der Veranschaulichung und ist nicht aus einem echten kopiert.

Dasselbe Image wie das Release

Der Job läuft mit demselben Image wie das Release, das er absichert. Deployen heißt, ein neues Image-Tag in das Repository zu committen, das ArgoCD beobachtet, und dieses Tag wird sowohl auf den Migrations-Job als auch auf das api-Deployment angewendet. Die Migrationen, die laufen, sind also genau die, die mit dem Code ausgeliefert werden, der gleich live geht. Es gibt kein separates Migrations-Image, das aus dem Takt geraten kann.

Der Deploy-Zustand liegt in einem eigenen Repository, getrennt vom Anwendungscode. So muss ArgoCD nicht bei jedem Refresh ein großes Monorepo neu klonen, und was ArgoCD deployt, ist eine kleine, überprüfbare Historie aus Tags und Manifesten.

Keine blinden Wiederholungen

backoffLimit: 0 ist Absicht. Kubernetes würde einen fehlgeschlagenen Job bereitwillig wiederholen, aber eine Migration, die mittendrin fehlgeschlagen ist, hat das Schema womöglich halb angewendet hinterlassen. Sie ohne Prüfung erneut auszuführen, kann das verschlimmern. Ein Versuch, dann anhalten und einen Menschen die Logs lesen lassen.

activeDeadlineSeconds begrenzt, wie lange der Job hängen kann, zum Beispiel an einem Lock, bevor er als fehlgeschlagen gilt.

Logs, die erhalten bleiben

Die Löschrichtlinie des Hooks ist BeforeHookCreation. ArgoCD löscht den alten Job erst, wenn es den nächsten anlegen will. Bis dahin bleiben der fehlgeschlagene Job und seine Pod-Logs im Cluster, also genau dann, wenn man sie braucht. Eine Richtlinie, die den Hook bei einem Fehler löscht, würde die Beweise wegwerfen.

Ein gesunder Lauf ist in diesen Logs leicht zu erkennen: Prisma meldet die gefundenen Migrationen, dann entweder die Liste der angewendeten oder dass nichts ausstand, und der Job wird abgeschlossen.

Die Reihenfolge kann wichtig sein

Migrationen vor den neuen Pods auszuführen, ist der richtige Standard, und manchmal ist es zwingend. Nehmen wir ein Release, in dem der neue Code eine Spalte nicht mehr beschreibt und der Standardwert der Spalte für die neue Welt nun falsch ist. Die Migration, die die bestehenden Zeilen und den Standardwert korrigiert, muss vor dem Start des neuen Codes greifen, sonst erhalten neue Zeilen den veralteten Standardwert, und nichts korrigiert ihn.

Ein PreSync-Hook liefert diese Reihenfolge ohne Zusatzaufwand.

Was ich jemandem raten würde, der das aufsetzt

  1. Führen Sie Migrationen als Pre-Deploy-Hook aus, der das Release abbrechen kann, nicht beim Start der Anwendung.
  2. Bauen Sie den Migrations-Job aus demselben Image-Tag wie das Release, damit Code und Schema nicht auseinanderlaufen.
  3. Setzen Sie backoffLimit: 0 und bewahren Sie die Logs fehlgeschlagener Jobs auf; ein halb angewendetes Schema braucht einen Menschen, keine Wiederholung.
  4. Deployen Sie ausschließlich per Sync, denn ein Neustart überspringt die Hooks.