9. Deployment und Paketierung
Deployment und Paketierung
Abschnitt betitelt „Deployment und Paketierung“Im Vorjahr war Ausliefern ein git push. Dokploy hat gebaut und gestartet, und meistens hat es funktioniert. Das ist ein Deployment, aber kein Release-Prozess.
Der Unterschied zeigt sich an vier Fragen, die Sie ab jetzt jederzeit beantworten können müssen:
- Welche Version läuft gerade in Produktion?
- Aus welchem Commit stammt sie, und wer hat sie freigegeben?
- Ist genau dieses Artefakt vorher in der Testumgebung gelaufen?
- Wie lange brauche ich zurück auf die vorige Version?
Wer die vierte Frage nicht mit einer Zahl beantworten kann, hat keinen Rückweg, sondern eine Hoffnung. Am Ende dieser zwei Wochen sind alle vier beantwortet, und zwar aus dem System heraus und nicht aus dem Gedächtnis.
Warum eine Pipeline
Abschnitt betitelt „Warum eine Pipeline“„Es läuft bei mir” heißt: auf einem Rechner, mit einer bestimmten Node-Version, mit Paketen, die vor drei Wochen installiert wurden, mit einer .env, die niemand sonst hat. „Es ist reproduzierbar gebaut” heißt: auf einem leeren Läufer, aus dem Stand des Repositorys, mit festgelegten Versionen, jederzeit wiederholbar mit demselben Ergebnis.
Automatisiert gehört alles, was mechanisch ist: bauen, prüfen, paketieren, in die Testumgebung ausliefern. Manuell bleibt genau eine Sache, und zwar mit Absicht: die Freigabe für Produktion. Ein Mensch entscheidet, wann etwas zu den Benutzern geht.
Daraus folgt die wichtigste Regel dieses Kapitels: Die Pipeline ist der einzige Weg in die Produktion. Sobald daneben noch jemand von Hand deployen kann, gibt es zwei Wege, und der zweite ist der undokumentierte, der um 22 Uhr benutzt wird.
Die Pipeline in GitLab CI
Abschnitt betitelt „Die Pipeline in GitLab CI“Vier Begriffe genügen für den Anfang. Eine Stufe fasst Aufträge zusammen, die parallel laufen dürfen. Ein Auftrag ist ein Skript in einem Image. Ein Läufer ist die Maschine, die das ausführt. Artefakte sind Dateien, die ein Auftrag an den nächsten weitergibt.
stages: - check - build - deploy
variables: IMAGE: $CI_REGISTRY_IMAGE
default: image: node:22-alpine cache: key: files: - package-lock.json paths: - .npm/ before_script: - npm ci --cache .npm --prefer-offline --ignore-scripts
typecheck: stage: check script: - npx tsc --noEmit
tests: stage: check script: - npm run test -- --run
dependencies-check: stage: check script: - npm audit --omit=dev --audit-level=high - npx license-checker --production --failOn "AGPL-3.0"
secrets-scan: stage: check image: zricethezav/gitleaks:latest before_script: [] script: - gitleaks detect --source . --redact
image: stage: build image: docker:27 services: - docker:27-dind before_script: - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" script: - docker build --label org.opencontainers.image.revision=$CI_COMMIT_SHA --label org.opencontainers.image.version=${CI_COMMIT_TAG:-dev} -t $IMAGE:$CI_COMMIT_SHA . - docker push $IMAGE:$CI_COMMIT_SHA
deploy-test: stage: deploy environment: name: test url: https://test.projekt.example.at script: - ./deploy.sh test $CI_COMMIT_SHA rules: - if: $CI_COMMIT_BRANCH == "main"
deploy-production: stage: deploy environment: name: production url: https://projekt.example.at script: - ./deploy.sh production $CI_COMMIT_SHA - ./smoketest.sh https://projekt.example.at rules: - if: $CI_COMMIT_TAG when: manualDer letzte Auftrag ist der Kern: Er läuft nur auf einer Markierung, und er läuft nur, wenn ihn jemand startet. GitLab schreibt dabei mit, wer das war und welche Version an welche Umgebung ging.
Was blockiert und was nur meldet
Abschnitt betitelt „Was blockiert und was nur meldet“| Prüfung | Ergebnis bei Fund |
|---|---|
| Typprüfung, Tests | Abbruch |
| Geheimnis im Repository | Abbruch, immer |
| Schwachstelle hoch oder kritisch in Produktionsabhängigkeiten | Abbruch |
| Schwachstelle mittel oder niedrig | Meldung, Aufgabe anlegen |
| Formatierung und Linting | Abbruch, weil billig zu beheben |
| Veraltete Pakete | Meldung |
Diese Tabelle haben Sie in Kapitel 7 entworfen; hier wird sie durchgesetzt.
Laufzeit ist ein Qualitätsmerkmal
Abschnitt betitelt „Laufzeit ist ein Qualitätsmerkmal“Eine Pipeline, die zehn Minuten braucht, wird benutzt. Eine, die vierzig Minuten braucht, wird umgangen, und dann läuft die Prüfung nicht mehr. Halten Sie die Laufzeit klein: Prüfaufträge laufen parallel, Abhängigkeiten kommen aus dem Zwischenspeicher, langsame Tests laufen nächtlich statt bei jedem Commit.
Die Aufträge laufen auf einem Läufer. Für Schulprojekte gibt es zwei Wege: die gemeinsamen Läufer der GitLab-Instanz, sofern die Schule welche bereitstellt, oder ein eigener Läufer als Container auf Ihrem VPS. Der eigene ist schnell eingerichtet und hat zwei Haken, die Sie kennen müssen. Er verbraucht Speicher auf demselben Server, auf dem Ihre Anwendung läuft, also planen Sie ihn in die Bestandsaufnahme aus Kapitel 1 ein. Und er hat Zugriff auf alles, was Sie ihm an Variablen geben, weshalb Produktionsgeheimnisse an geschützte Zweige gebunden gehören.
Merge Requests und Vorschau
Abschnitt betitelt „Merge Requests und Vorschau“Ein Merge Request wird erst zusammengeführt, wenn die Pipeline grün ist und ein Review vorliegt. Das ist keine Bürokratie, sondern der Grund, warum der Hauptzweig jederzeit auslieferbar bleibt.
Wer mehr Speicher übrig hat, kann je Zweig eine Vorschauumgebung starten lassen, also einen eigenen Stapel unter einer Adresse wie mr-42.projekt.example.at, den GitLab beim Schließen des Merge Requests wieder abräumt. Der Nutzen ist groß, weil ein Review dann am laufenden Stand stattfindet statt am Quelltext. Der Preis ist Speicher und eine saubere Aufräumregel; ohne die liegen nach drei Wochen zwölf tote Stapel herum. Für ein Team mit knappem Server ist die gemeinsame Testumgebung der vernünftigere Weg.
Umgebungen
Abschnitt betitelt „Umgebungen“| Entwicklung | Test | Produktion | |
|---|---|---|---|
| Wo | eigener Rechner | Server, eigene Subdomain | Server, echte Domain |
| Daten | erzeugt | erzeugt | echt |
| Geheimnisse | eigene | eigene | eigene |
| Fremddienste | Sandkasten | Sandkasten | Produktivzugang |
| Auslieferung | keine | automatisch bei jedem Stand auf main | manuell freigegeben |
Gleiche Bauart, unterschiedliche Konfiguration: Es ist dasselbe Image, das in Test und Produktion läuft, und der Unterschied steckt ausschließlich in Umgebungsvariablen. Sobald Sie für Produktion neu bauen, testen Sie nicht mehr das, was Sie ausliefern.
Geheimnisse liegen in GitLab als geschützte und maskierte Variablen, sind je Umgebung verschieden, und die Produktionswerte sind an geschützte Zweige und Markierungen gebunden. Damit kann ein Auftrag aus einem beliebigen Zweig sie nicht auslesen.
Artefakte und Versionen
Abschnitt betitelt „Artefakte und Versionen“Einmal bauen, mehrfach ausliefern
Abschnitt betitelt „Einmal bauen, mehrfach ausliefern“Das Container-Image ist Ihr Artefakt. Es wird genau einmal gebaut, in die Registry geschoben und wandert dann unverändert durch die Umgebungen. Was Sie in Produktion starten, ist byteweise dasselbe, das in der Testumgebung gelaufen ist.
Daraus folgt das Verbot von latest. Eine Marke, die sich ändert, beantwortet die Frage „was läuft gerade” nicht. Verwenden Sie zwei unveränderliche Marken:
registry.example.at/werkstatt:sha-9f3c1ab immer, für jeden Standregistry.example.at/werkstatt:1.4.2 zusätzlich bei einem ReleaseSemantische Versionierung
Abschnitt betitelt „Semantische Versionierung“Die Version folgt dem bekannten Schema: die dritte Stelle für Korrekturen, die zweite für neue Funktionen, die erste für Bruchstellen. Dazu ein CHANGELOG.md, das für jede Version aufzählt, was dazugekommen, geändert und behoben wurde.
Damit die Kette vom laufenden Prozess zurück zum Quellcode geschlossen ist, wandern Version und Commit in das Image und in die Anwendung:
export async function GET() { return Response.json({ version: process.env.APP_VERSION ?? "dev", commit: process.env.APP_COMMIT ?? "unknown", builtAt: process.env.APP_BUILD_TIME ?? null, });}Dieser eine Endpunkt beantwortet im Störfall die erste aller Fragen, und im nächsten Kapitel steht dieselbe Version in jedem Protokolleintrag.
Migrationen im laufenden Betrieb
Abschnitt betitelt „Migrationen im laufenden Betrieb“Warum die Reihenfolge zählt
Abschnitt betitelt „Warum die Reihenfolge zählt“Beim Ausrollen gibt es einen Zeitraum, in dem alte und neue Anwendungsversion gleichzeitig auf derselben Datenbank arbeiten. Selbst beim einfachen Neustart sind das einige Sekunden, in denen noch Anfragen der alten Version laufen.
Daraus folgt die Regel: Die Migration läuft vor dem Ausrollen und muss mit der alten Version verträglich sein. Eine Migration, die eine Spalte umbenennt, bricht die alte Version sofort.
Erweitern, umstellen, aufräumen
Abschnitt betitelt „Erweitern, umstellen, aufräumen“-
Erweitern. Neue Spalte anlegen, nullable oder mit Standardwert. Beide Versionen laufen damit.
-
Doppelt schreiben. Die neue Anwendungsversion schreibt alt und neu, liest weiterhin alt. Bestandsdaten werden in Blöcken nachgefüllt.
-
Umstellen. Die nächste Version liest neu. Die alte Spalte bleibt unangetastet.
-
Aufräumen. Erst wenn keine Version mehr die alte Spalte braucht, wird sie entfernt. Das ist ein eigenes Release, oft Wochen später.
Der Preis sind mehrere Auslieferungen für eine Umbenennung. Der Ertrag ist, dass zu keinem Zeitpunkt etwas kaputt ist und jeder Schritt für sich zurückgenommen werden kann.
| Unbedenklich | Braucht das Verfahren oben |
|---|---|
| Spalte hinzufügen (nullable) | Spalte umbenennen |
| Tabelle hinzufügen | Spalte löschen |
| Index hinzufügen (nebenläufig) | Typ ändern |
| Wert zu einer Aufzählung hinzufügen | Nicht-Null-Bedingung nachrüsten |
Sperren und große Tabellen
Abschnitt betitelt „Sperren und große Tabellen“ALTER TABLE nimmt eine sehr weitreichende Sperre. Bei einer kleinen Tabelle merkt das niemand, bei einer großen steht die Anwendung. Zwei Vorkehrungen gehören in jede Migration in Produktion:
SET lock_timeout = '3s'; -- better to fail than to block everythingSET statement_timeout = '60s';CREATE INDEX CONCURRENTLY ... -- index without a write lockNachfüllen von Bestandsdaten geschieht in Blöcken mit Pausen, nicht als ein UPDATE über eine Million Zeilen.
Ausrollstrategien
Abschnitt betitelt „Ausrollstrategien“| Strategie | Wie | Auf einem VPS |
|---|---|---|
| Neustart | alt stoppen, neu starten | machbar, kurze Lücke |
| Rollierend | Instanzen nacheinander tauschen | ab zwei Instanzen |
| Blau-Grün | zweiter Stapel parallel, Proxy schaltet um | machbar, wenn der Speicher reicht |
| Kanarienvogel | kleiner Anteil des Verkehrs auf neu | unrealistisch, zu wenig Verkehr |
Für die meisten Schulprojekte ist Blau-Grün mit Traefik der beste erreichbare Weg: Der neue Stapel startet, der Health Check aus Kapitel 11 wird abgewartet, dann schaltet der Proxy um. Der alte Stapel bleibt eine Stunde stehen, weil das der schnellste Rückweg ist, den es gibt.
Wartungsfenster
Abschnitt betitelt „Wartungsfenster“Nicht jede Änderung geht ohne Unterbrechung. Eine Datenmigration, die eine große Tabelle umschreibt, ein Wechsel der Datenbankversion, ein Umzug auf einen neuen Server: Dafür ist ein angekündigtes Wartungsfenster der bessere Weg als der Versuch, es unterbrechungsfrei zu erzwingen.
Ein brauchbares Wartungsfenster hat vier Bestandteile: eine Ankündigung mit Zeitraum, eine Wartungsseite über den Reverse Proxy statt einer Fehlermeldung, ein Zeitlimit, nach dem abgebrochen und zurückgerollt wird, und eine Meldung danach. Legen Sie es in Randzeiten, prüfen Sie vorher in der Testumgebung, wie lange der Vorgang tatsächlich dauert, und rechnen Sie das Doppelte.
Funktionsschalter entkoppeln Auslieferung und Freischaltung: Code geht in Produktion, bleibt aber aus, bis jemand ihn einschaltet. Das nimmt großen Änderungen den Schrecken und hat einen Preis, den man kennen muss: Jeder Schalter verdoppelt die Zahl der möglichen Zustände, und Schalter, die niemand aufräumt, werden zur zweiten Codebasis. Notieren Sie zu jedem Schalter, wann er wieder verschwindet.
Freigabe, Installation, Rückweg
Abschnitt betitelt „Freigabe, Installation, Rückweg“Der Ablauf
Abschnitt betitelt „Der Ablauf“-
Kandidat steht. Ein Commit auf
main, Pipeline grün, seit mindestens einem Tag in der Testumgebung. -
Version markieren.
git tag v1.4.2,CHANGELOG.mdgepflegt. -
Freigabe. Eine Person startet den Produktionsauftrag. GitLab hält fest, wer und wann.
-
Migration, dann Ausrollen, in dieser Reihenfolge.
-
Rauchtest automatisch: Startseite, Anmeldung, ein Lesezugriff, ein Schreibzugriff,
/api/versiongegen die erwartete Version. -
Beobachten. Fehlerrate und Antwortzeit für 15 Minuten ansehen (Kapitel 10).
-
Freigabeprotokoll ergänzen: Version, Zeit, freigegeben von, Auffälligkeiten.
Rollback wird geübt
Abschnitt betitelt „Rollback wird geübt“./deploy.sh production sha-<previous> # start the previous image again./smoketest.sh https://projekt.example.atFühren Sie das einmal absichtlich durch, mit Uhr, und schreiben Sie die Zeit auf. Diese Zahl ist Ihre Wiederherstellungsdauer für den Fall „schlechte Version”, und sie gehört in die Betriebsdokumentation. Ein Rollback, das noch nie gelaufen ist, ist eine Vermutung über ein Skript.
Am Ende dieser zwei Wochen steht docs/betrieb.md in der Fassung, die auch beim Projektabschluss verlangt wird: Umgebungen, Adressen, Zugänge ohne Geheimnisse, Auslieferungsweg, Rollback-Weg, Migrationsverfahren, Freigabeprotokoll.
Häufige Stolperfallen
Abschnitt betitelt „Häufige Stolperfallen“- Je Umgebung neu bauen. Dann testen Sie nie das, was Sie ausliefern.
latestals Marke. Niemand weiß hinterher, was läuft.- Zweiter Weg in die Produktion, von Hand, undokumentiert.
- Migration nach dem Ausrollen oder Migration, die die alte Version bricht.
- Zerstörende Migration im selben Release wie eine Funktionsänderung, und damit kein gefahrloser Rückweg.
ALTER TABLEohnelock_timeoutauf einer großen Tabelle.- Geheimnisse in der Pipeline-Konfiguration im Klartext statt in geschützten Variablen.
- Pipeline mit 40 Minuten Laufzeit, die deshalb umgangen wird.
- Kein Rauchtest nach dem Ausrollen. Der Ausfall wird von Benutzern gemeldet.
- Rollback nie ausprobiert.
- Funktionsschalter nie aufgeräumt.
- Keine Verbindung zwischen laufender Version und Commit.
Lernergebnisse
Abschnitt betitelt „Lernergebnisse“- Erklären: den Unterschied zwischen einem Deployment und einem Release-Prozess erklären.
- Anwenden: eine Pipeline aufsetzen, die baut, prüft, paketiert und ausliefert.
- Beurteilen: festlegen, welche Prüfungen die Auslieferung blockieren und welche nur melden.
- Anwenden: getrennte Umgebungen mit eigener Konfiguration und geschützten Geheimnissen betreiben.
- Anwenden: ein unveränderliches, versioniertes Artefakt erzeugen und durch alle Umgebungen führen.
- Erklären: von einer laufenden Instanz auf Version und Commit zurückschließen.
- Analysieren: Migrationen so schneiden, dass alte und neue Version gleichzeitig laufen können.
- Anwenden: eine Migration mit Sperrschutz und blockweisem Nachfüllen schreiben.
- Beurteilen: eine Ausrollstrategie für die eigene Umgebung begründet wählen.
- Anwenden: ein Release freigeben, prüfen und im Bedarfsfall zurücknehmen, mit gemessener Dauer.
Arbeit am Jahresprojekt
Abschnitt betitelt „Arbeit am Jahresprojekt“- Pipeline in Betrieb mit Prüf-, Bau- und Auslieferungsstufe, Produktionsauftrag manuell und an eine Markierung gebunden.
- Ein Artefakt je Stand, unveränderlich markiert, in einer Registry, ohne
latest. - Zwei Umgebungen mit getrennten Geheimnissen, dasselbe Image in beiden.
- Versionsendpunkt in der Anwendung, der Version und Commit ausgibt.
- Eine Migration nach dem Vier-Schritte-Verfahren, dokumentiert.
- Ein Release mit Markierung, Änderungsprotokoll, Freigabeprotokoll und automatischem Rauchtest.
- Ein vorgeführtes Rollback mit gemessener Dauer.
docs/betrieb.mdvollständig.
Passende Übungen
Abschnitt betitelt „Passende Übungen“- Aufgabe - Pipeline mit Prüfstufen aufsetzen
- Aufgabe - Ein Artefakt durch drei Umgebungen
- Aufgabe - Zwei-Phasen-Migration ohne Ausfall
- Aufgabe - Release und Rollback vorführen