post

AWS Deployment Pipelines sind Architektur

Deployment Pipelines sind Architektur, weil sie bestimmen, wie häufig, sicher und reversibel ein System verändert wird.

≈ 8 Min. Lesezeit

Wenn ich in einer Schulung frage, wo die Architektur eines Systems liegt, zeigen die meisten auf das Fachmodell, auf die Service-Grenzen, vielleicht auf ein Datenbankschema. Auf die Pipeline zeigt fast niemand. Dabei entscheidet gerade sie über die Eigenschaften, die einem Team im Alltag wirklich wehtun oder wirklich helfen: wie oft eine Änderung überhaupt live gehen kann, wie sicher das passiert und wie schnell sie sich zurücknehmen lässt. Eine Pipeline ist keine Fleißarbeit hinter der eigentlichen Entwicklung. Sie ist eine Architekturentscheidung, die man genauso begründen muss wie die Wahl einer Datenbank.

Der konkrete Anlass für diesen Text ist eine kleine GitHub Action, die nichts weiter tut, als ein AWS-CDK-Projekt zu deployen. Ein paar Zeilen YAML. Und trotzdem legt genau dieser Baustein fest, welcher Fehler früh auffällt, welcher erst in der Produktion – und wer eine Änderung an der Infrastruktur überhaupt auslösen darf.

Die Pipeline legt die Änderbarkeit fest

Architektur beantwortet die Frage, was sich unabhängig voneinander ändern lässt. Genau das ist auch die Kernfrage einer Pipeline. Ein System, das nur im Quartals-Release deploybar ist, hat eine andere Architektur als eines, das dieselbe Codebasis zwanzigmal am Tag ausrollt – selbst wenn beide identischen Anwendungscode enthalten. Die Änderbarkeit steckt nicht allein im Modul-Schnitt, sondern im Weg von einem Commit bis in die Laufzeitumgebung.

Auf AWS wird dieser Weg besonders sichtbar, seit Infrastruktur selbst Code ist. Mit dem CDK beschreibe ich Lambda-Funktionen, API-Gateways, Tabellen und Rechte in TypeScript. cdk synth übersetzt das in ein CloudFormation-Template, cdk deploy bringt es in ein Konto. Damit verschiebt sich etwas Grundlegendes: Die Frage „wie sieht meine Infrastruktur aus?" und die Frage „wie kommt eine Änderung dorthin?" fallen zusammen. Beide leben im selben Repository, durchlaufen denselben Review und dieselbe Pipeline. Das habe ich in Infrastruktur als Programmiersprache ausführlicher aufgeschrieben; hier interessiert mich der Schritt danach – was passiert, wenn dieser Code automatisch ausgerollt wird.

Ein minimaler Stack sieht so aus:

import * as cdk from "@aws-cdk/core";
import * as lambda from "@aws-cdk/aws-lambda";
import * as apigw from "@aws-cdk/aws-apigateway";

export class ApiStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const handler = new lambda.Function(this, "ApiHandler", {
      runtime: lambda.Runtime.NODEJS_12_X,
      handler: "index.handler",
      code: lambda.Code.fromAsset("dist/api"),
    });

    new apigw.LambdaRestApi(this, "Api", { handler });
  }
}

Das Beispiel ist bewusst klein. Es soll keine vollständige Anwendung zeigen, sondern die Grenze markieren: Alles, was hier steht, ist ab jetzt Teil des deploybaren Artefakts. Wenn ich die Runtime ändere, ein Recht erweitere oder das API umbaue, geht das denselben Weg wie eine Codeänderung. Die spannende Entscheidung ist nicht, wie viele Constructs ich schreibe, sondern welche Kontrolle zwischen diesem Code und der Produktion steht.

Die „CDK Action" als kleinster ehrlicher Baustein

Der einfachste Weg, dieses Deployment zu automatisieren, ist eine GitHub Action, die auf jeden Push nach main reagiert:

name: deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v1
        with:
          node-version: 12
      - run: npm ci
      - run: npm test
      - run: npx cdk deploy --require-approval never
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: eu-central-1

Das sieht harmlos aus, trifft aber lauter Architekturentscheidungen. on: push heißt: Jeder Merge nach main ist ein Deployment – der Trunk ist die Auslieferung, nicht bloß ein Zwischenstand. Der Schritt npm test vor cdk deploy zieht eine Qualitätsgrenze: Was rot ist, geht nicht raus. --require-approval never verzichtet auf die interaktive Rückfrage des CDK bei sicherheitsrelevanten Änderungen – bequem, aber es verschiebt die Verantwortung für IAM-Änderungen vollständig in den Review davor. Und die zwei Secrets legen fest, mit welchen Rechten diese Pipeline im Konto agiert; in der Praxis ist das der Punkt, an dem die meiste unausgesprochene Macht steckt.

Genau deshalb finde ich diesen kleinen Baustein lehrreicher als jede große Referenzpipeline. Man kann in fünf Minuten darüber diskutieren, und jede Zeile steht für eine bewusste oder unbewusste Entscheidung über Häufigkeit, Sicherheit und Zuständigkeit.

Der Review-Punkt liegt vor dem Deploy, nicht danach

Bei Anwendungscode reviewen wir den Diff. Bei Infrastruktur tun das viele nicht – und wundern sich, dass ein Merge nebenbei eine Security Group öffnet oder eine Tabelle ersetzt. Das CDK hat dafür ein gutes Werkzeug, das in einer Pipeline zum eigentlichen Review-Artefakt wird:

# what will actually change in the account
cdk diff ApiStack

cdk diff zeigt nicht meinen TypeScript-Diff, sondern den Unterschied zwischen dem, was im Konto steht, und dem, was ich deployen will – inklusive der IAM-Statements, die CloudFormation daraus ableitet. In einer sauberen Pipeline läuft dieser Schritt automatisch und hängt das Ergebnis an den Pull Request. Damit entscheidet ein Mensch über die Wirkung, nicht über den Quelltext. Das ist ein methodischer Unterschied mit direkter Betriebswirkung: Ein Reviewer, der sieht „diese Änderung fügt iam:PassRole hinzu", stoppt sie, bevor sie im Konto liegt. Ein Reviewer, der nur stack.ts liest, übersieht sie oft.

So sieht der Weg als Ganzes aus:

flowchart LR
  Commit --> Build["Build & Test"]
  Build --> Synth["cdk synth<br/>CloudFormation-Template"]
  Synth --> Diff["cdk diff<br/>Review-Artefakt"]
  Diff --> Approve{"Freigabe?"}
  Approve -->|ja| Deploy["cdk deploy"]
  Approve -->|nein| Stop["Halt"]
  Deploy --> Observe["Metriken &<br/>Alarme"]
  Observe -->|Fehler| Rollback["Rollback auf<br/>vorherige Revision"]

Jeder Knoten in dieser Skizze ist eine Stellschraube. Wo die Freigabe sitzt, entscheidet über Tempo gegen Kontrolle. Ob nach Deploy überhaupt beobachtet wird, entscheidet, ob ein schlechtes Release auffällt, bevor der Kunde es meldet. Und ob es einen echten Rollback-Pfad gibt, entscheidet darüber, ob eine Änderung reversibel ist – oder nur hoffentlich richtig.

Dasselbe Artefakt durch mehrere Umgebungen

Sobald mehr als eine Umgebung im Spiel ist – Staging und Produktion, oft in getrennten AWS-Konten – wird die Pipeline zur eigentlichen Definition dessen, was „promoten" heißt. Der Anspruch, den ich hier hochhalte: Es ist dasselbe geprüfte Artefakt, das durch die Stufen wandert, nicht ein zweiter Build mit leicht anderen Zutaten. Beim CDK bedeutet das, dieselbe App mit unterschiedlichem Kontext zu synthetisieren.

const app = new cdk.App();

new ApiStack(app, "ApiStack-staging", {
  env: { account: "111111111111", region: "eu-central-1" },
});

new ApiStack(app, "ApiStack-prod", {
  env: { account: "222222222222", region: "eu-central-1" },
});

Damit steht die Umgebungs-Topologie im selben Repository wie der Code und durchläuft denselben Review. Der methodische Gewinn ist Vertrauen: Wenn Staging und Produktion aus einer Quelle synthetisiert werden, testet Staging tatsächlich das, was später in Produktion läuft – und nicht eine plausible Kopie, die an drei Stellen abweicht. Genau diese Abweichungen sind es, die sonst erst im teuersten Konto auffallen. Die Trennung in eigene Konten ist dabei kein Betriebsdetail, sondern eine Architekturgrenze: Ein Deploy nach Staging kann Produktion nicht versehentlich berühren, weil ihm schlicht die Rechte im anderen Konto fehlen.

Trade-offs, die man benennen muss

Die interessante Arbeit an einer Pipeline besteht nicht darin, sie zum Laufen zu bringen, sondern darin, ihre Kompromisse offenzulegen. Ein paar, die in fast jedem Projekt auftauchen:

  • Auto-Deploy auf main gegen manuelles Gate. Der automatische Weg verkürzt die Durchlaufzeit drastisch, verlangt aber ein belastbares Testnetz und gute Alarme. Das Gate gibt ein Sicherheitsgefühl, sammelt in der Praxis aber Änderungen zu großen, riskanten Batches an – der häufigste Grund für seltene, angstbesetzte Releases.
  • Ein Stack gegen viele Stacks. Ein großer Stack ist einfach zu deployen, aber jede Änderung berührt alles. Getrennte Stacks für API, Daten und statisches Frontend erlauben es, das Risiko klein zu halten und Teile unabhängig auszurollen – auf Kosten von mehr Koordination an den Schnittstellen.
  • Rollback per Redeploy gegen Vorwärts-Fix. CloudFormation kann bei einem fehlgeschlagenen Deploy automatisch zurückrollen. Das trägt aber nur, solange die Änderung nicht bereits Daten migriert oder ein Schema erweitert hat. Ab da ist „einfach zurück" eine Illusion, und man braucht additive, rückwärtskompatible Schritte statt eines Undo-Knopfs.

Keine dieser Entscheidungen ist universell richtig. Aber sie gehören in ein Architektur-Gespräch und in ein kurzes ADR, nicht in eine YAML-Datei, die jemand vor einem Jahr committet hat und die seither niemand mehr hinterfragt.

Warum das ein CTO interessieren sollte

Für die technische Leitung übersetzt sich das alles in Zahlen, die seit „Accelerate" ein etabliertes Vokabular haben. Vier Kennzahlen beschreiben die Leistungsfähigkeit einer Auslieferung ziemlich vollständig, und alle vier werden von der Pipeline bestimmt, nicht vom Anwendungscode:

  • Deployment-Häufigkeit – wie oft geht überhaupt etwas live. Ein Auto-Deploy auf main hebt sie um Größenordnungen gegenüber einem manuellen Quartals-Gate.
  • Lead Time – wie lange von „Code fertig" bis „in Produktion". cdk diff im PR plus automatischer Deploy drückt das von Tagen auf Minuten.
  • Change Failure Rate – welcher Anteil der Releases erzeugt einen Fehler. Der Test-Schritt und das Diff-Review vor dem Deploy senken sie, ohne die Häufigkeit zu opfern.
  • Time to Restore – wie schnell ist der Normalzustand wieder da. Ein echter, geübter Rollback-Pfad ist hier der Unterschied zwischen Minuten und einem verlorenen Nachmittag.

Das Wichtige an diesen vier: Sie ziehen nicht gegeneinander. Teams, die häufig deployen, sind meistens auch die mit der niedrigeren Fehlerrate – weil kleine, häufige Änderungen leichter zu prüfen und zu reparieren sind als große, seltene. Eine gut geschnittene Pipeline ist deshalb kein Luxus für die Betriebsseite, sondern das, was kürzere Zyklen, weniger Reibung und höhere Qualität gleichzeitig möglich macht. Das ist ein Architektur-Effekt, und man kann ihn messen.

Woran ich eine Pipeline prüfe

Eine Idee ist für mich erst belastbar, wenn auch der Fehlerpfad beschrieben ist. Bei einer Pipeline stelle ich deshalb immer dieselben unbequemen Fragen. Was passiert, wenn cdk deploy mitten im Umbau abbricht – bleibt ein halber Zustand zurück? Ist der Deploy idempotent genug, dass ein zweiter Lauf ihn heilt? Wo liegt die Grenze, ab der ein Rollback nicht mehr genügt, weil bereits Daten migriert wurden? Und ganz banal: Wer darf diese Pipeline auslösen, und ist das nachvollziehbar protokolliert?

Der zweite Fehler, den ich oft sehe, ist die Überbewertung des glücklichen Pfades. Im Demo-Konto läuft alles sauber. Interessant wird es bei Teilfehlern, Rechten, die knapp nicht ausreichen, Ressourcen, die CloudFormation ersetzen statt ändern will, und bei dem einen Deploy, der um 17 Uhr am Freitag hängenbleibt. Genau dort zeigt sich, ob das Pipeline-Modell trägt – und ob „reversibel" ein echtes Versprechen ist oder nur ein Wort in einer Präsentation.

Fazit

Die Verlockung ist, eine Pipeline als Werkzeugfrage zu behandeln: welche Action, welcher Runner, welcher Provider. Der eigentliche Gehalt liegt woanders. Eine Pipeline kodiert, wie oft ein Team seine Meinung ändern darf, wie sicher es das tut und wie leicht es einen Fehler zurücknimmt. Das sind Architektureigenschaften, und sie entstehen nicht im Toolnamen, sondern in den paar bewussten Entscheidungen dahinter – Auto-Deploy oder Gate, ein Stack oder viele, Undo oder Vorwärts-Fix. Wer diese Entscheidungen benennt und misst, bekommt kürzere Zyklen und weniger Reibung geschenkt. Wer sie einer alten YAML-Datei überlässt, bekommt sie trotzdem – nur unbewusst und meist zum ungünstigsten Zeitpunkt.

Weiterführende Quellen

Kommentare