07 — Pipeline in der Praxis
Teil C — Wie man ausliefert. Zurück zur Übersicht.
Lernziel
Nach diesem Kapitel weißt du, wie Runner arbeiten, wie Artefakte und Registries zusammenspielen und wie Geheimnisse sicher in eine Pipeline gelangen. Du kannst Pipelines wiederverwendbar aufbauen und kennst die Angriffsfläche, die eine Pipeline selbst darstellt.
7.1 Wer führt eigentlich aus?
Eine Pipeline ist eine Beschreibung. Ausgeführt wird sie von einem Runner (GitLab), Agent (Jenkins) oder Runner (GitHub Actions) — einem Prozess, der Aufträge abholt und abarbeitet.
| Betriebsart | Vorteil | Nachteil |
|---|---|---|
| Gehostet beim Anbieter | kein Betriebsaufwand, immer aktuell | Code verlässt das Haus, Laufzeitkontingente |
| Selbst betrieben | volle Kontrolle, Zugriff auf interne Netze | Pflege, Härtung, Skalierung liegen bei dir |
In regulierten Umgebungen ist die Frage schnell beantwortet: Selbst betriebene Runner im eigenen Netz, weil sie sonst weder an interne Registries noch an Testdatenbanken herankommen — und weil Quellcode das Haus nicht verlässt.
Ephemere Runner sind der Standard. Für jeden Auftrag entsteht ein frischer Container, der danach verworfen wird.
Ein dauerhaft laufender Runner, der Aufträge nacheinander im selben Arbeitsverzeichnis abarbeitet, ist eine Schneeflocke mit Rechten. Bleibt aus einem Auftrag etwas zurück — eine Datei, eine Umgebungsvariable, ein installiertes Paket — beeinflusst das den nächsten. Das Ergebnis sind Pipelines, die „meistens„ durchlaufen, und Fehlersuchen, die niemand mag.
7.2 Artefakte und Registries
Ein Artefakt ist das Ergebnis des Bauvorgangs: ein Container-Image, ein Python-Rad, ein JAR, ein RPM. Es wird einmal erzeugt und in einer Registry abgelegt.
| Art | Beispiele |
|---|---|
| Container-Registry | Quay, Harbor, GitLab Container Registry, ghcr.io |
| Paket-Registry | Nexus, Artifactory, GitLab Package Registry |
| Zwischenartefakte der Pipeline | Testberichte, Abdeckungsberichte, SBOM |
Warum eine interne Registry auch ohne eigene Images sinnvoll ist: Sie spiegelt externe Images und macht dich unabhängig von deren Verfügbarkeit, Rate Limits und Löschungen. Wer schon einmal erlebt hat, dass ein Basis-Image aus dem Netz verschwand und damit sämtliche Builds standen, richtet die Spiegelung beim nächsten Mal vorher ein.
Beschriftung von Artefakten
# Nachvollziehbar: Commit, Version, Zeitpunkt podman build \ --label "org.opencontainers.image.revision=$CI_COMMIT_SHA" \ --label "org.opencontainers.image.version=$CI_COMMIT_TAG" \ --label "org.opencontainers.image.created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ --label "org.opencontainers.image.source=$CI_PROJECT_URL" \ -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
Diese Beschriftungen sind kein Selbstzweck. Sie beantworten im Störungsfall in einem einzigen Befehl die Frage, welcher Commit gerade produktiv läuft:
podman inspect kfz-rechner --format '{{index .Labels "org.opencontainers.image.revision"}}'
Der Tag latest hat in einer Pipeline nichts zu suchen. Er ist ein beweglicher Zeiger — was gestern darunter lag, kann heute etwas anderes sein. Damit ist weder reproduzierbar, was getestet wurde, noch nachweisbar, was läuft.
In der Pipeline wird mit dem Commit-Kürzel getaggt, in der Produktion mit dem Digest referenziert.
7.3 Geheimnisse in der Pipeline
Die Pipeline braucht Zugangsdaten: für die Registry, für Zielsysteme, für Signaturschlüssel. Sie dürfen niemals im Repository stehen.
| Mechanismus | Einsatz |
|---|---|
| CI-Variablen (maskiert, geschützt) | einfache Fälle, kleine Teams |
| Externer Tresor (HashiCorp Vault, OpenBao) | Unternehmensumgebungen, rotierende Geheimnisse |
| OIDC-Verbund ohne dauerhafte Zugangsdaten | Cloud-Anbieter, heutiger Stand der Technik |
| Signaturschlüssel in einem HSM | Codesignatur, hohe Schutzanforderung |
# GitLab: Variable als "masked" und "protected" anlegen # masked -> wird in Logs durch [MASKED] ersetzt # protected -> nur auf geschützten Zweigen verfügbar deploy: script: - echo "$DEPLOY_KEY" > /tmp/key && chmod 600 /tmp/key - ssh -i /tmp/key conops@server './deploy.sh' after_script: - shred -u /tmp/key
Maskierung ist keine Verschlüsselung. Sie ersetzt bekannte Zeichenketten in der Ausgabe — und versagt zuverlässig, sobald der Wert umkodiert wird. Ein echo „$GEHEIM“ | base64 erscheint unmaskiert im Protokoll, ebenso ein Wert, der in einer Fehlermeldung als JSON auftaucht.
Deshalb gilt: Niemals set -x in Skripten mit Geheimnissen, und niemals Umgebungsvariablen zur Fehlersuche vollständig ausgeben. Wer im Zweifel ist, ob ein Geheimnis im Protokoll gelandet ist, wechselt es. Das ist billiger als die Gewissheit.
7.4 Pipelines wiederverwenden
Bei zwanzig Projekten will niemand zwanzigmal dieselben achtzig Zeilen pflegen.
- ci-vorlagen/python.yml
# Zentrale Vorlage, eigenes Repository .python-tests: image: python:3.12-slim before_script: - pip install --quiet -r requirements.txt script: - ruff check src/ tests/ - pytest --cov=src cache: key: files: [requirements.txt] paths: [.cache/pip]
- .gitlab-ci.yml
include: - project: 'betrieb/ci-vorlagen' ref: v2.1.0 # feste Version, nicht "main" file: '/python.yml' unit-tests: extends: .python-tests stage: testen
Bei GitHub Actions leisten Reusable Workflows und Composite Actions dasselbe.
Wichtig ist die feste Versionsangabe. Eine eingebundene Vorlage ist fremder Code, der in deiner Pipeline mit deinen Zugangsdaten läuft. Wer ref: main schreibt, übernimmt jede Änderung ungeprüft — auch die, die jemand anders versehentlich hineingeschrieben hat.
7.5 Die Pipeline als Angriffsfläche
Ein Aspekt, der in Schulungen fast immer fehlt: Die Pipeline hat Zugriff auf Quellcode, Registry und Produktivsysteme. Wer sie kontrolliert, kontrolliert die Auslieferung.
| Risiko | Gegenmaßnahme |
|---|---|
| Fremde Actions und Vorlagen | auf Commit-Hash festnageln, nicht auf bewegliche Tags |
| Pipeline-Definition änderbar durch jeden | Änderungen an CI-Dateien gesondert prüfen lassen |
| Geheimnisse auf Feature-Branches | „protected“-Variablen nur auf geschützten Zweigen |
| Fremde Pull Requests mit Zugriff | für externe Beiträge keine Geheimnisse bereitstellen |
| Runner mit Zugriff auf alles | Netzsegmentierung, minimale Rechte je Auftrag |
| Manipuliertes Basis-Image | eigene Spiegel-Registry, Signaturprüfung |
# Schlecht: beweglicher Tag - uses: actions/checkout@v4 # Besser: auf den Commit festgenagelt - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
Der Angriff auf die Lieferkette ist kein theoretisches Szenario mehr — mehrere große Vorfälle der letzten Jahre liefen genau über kompromittierte Build-Prozesse. Mehr dazu in Kapitel 14.
7.6 Laufzeit im Griff behalten
# Parallelisieren, was unabhängig ist stages: [pruefen, testen] lint: stage: pruefen unit-tests: stage: testen integration-tests: stage: testen # läuft parallel zu unit-tests # Matrix: mehrere Varianten gleichzeitig test: parallel: matrix: - PYTHON: ["3.11", "3.12", "3.13"] image: python:$PYTHON # Nur bauen, was sich geändert hat container-bauen: rules: - changes: - src/**/* - Containerfile
Was am meisten bringt, in dieser Reihenfolge: Abhängigkeiten zwischenspeichern, unabhängige Schritte parallelisieren, Container-Schichten so anordnen, dass der Cache greift (Kapitel 10), langsame E2E-Tests nachgelagert ausführen.
7.7 Lab: Die vollständige Pipeline
- .gitlab-ci.yml
stages: - pruefen - testen - bauen - scannen - ausrollen variables: IMAGE: "$CI_REGISTRY_IMAGE" TAG: "$CI_COMMIT_SHORT_SHA" default: interruptible: true # alter Lauf wird bei neuem Push abgebrochen # --------------------------------------------------------------- lint: stage: pruefen image: python:3.12-slim script: - pip install --quiet ruff - ruff check src/ tests/ - ruff format --check src/ tests/ geheimnisse-suchen: stage: pruefen image: zricethezav/gitleaks:latest script: - gitleaks detect --source . --verbose --no-git # --------------------------------------------------------------- unit-tests: stage: testen image: python:3.12-slim script: - pip install --quiet -r requirements.txt - pytest --junitxml=report.xml --cov=src --cov-report=xml artifacts: when: always reports: junit: report.xml integration-tests: stage: testen image: python:3.12-slim services: - name: postgres:16 alias: db variables: POSTGRES_PASSWORD: test DB_HOST: db script: - pip install --quiet -r requirements.txt - pytest tests/integration/ -v # --------------------------------------------------------------- container-bauen: stage: bauen image: quay.io/podman/stable script: - podman login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" - | podman build \ --label "org.opencontainers.image.revision=$CI_COMMIT_SHA" \ --label "org.opencontainers.image.source=$CI_PROJECT_URL" \ -t "$IMAGE:$TAG" . - podman push "$IMAGE:$TAG" - podman inspect "$IMAGE:$TAG" --format '{{.Digest}}' > digest.txt artifacts: paths: [digest.txt] rules: - if: $CI_COMMIT_BRANCH == "main" # --------------------------------------------------------------- schwachstellen: stage: scannen image: aquasec/trivy:latest script: - trivy image --exit-code 0 --severity LOW,MEDIUM "$IMAGE:$TAG" - trivy image --exit-code 1 --severity HIGH,CRITICAL "$IMAGE:$TAG" rules: - if: $CI_COMMIT_BRANCH == "main" # --------------------------------------------------------------- ausrollen-test: stage: ausrollen environment: name: test script: - ./scripts/deploy.sh test "$IMAGE@$(cat digest.txt)" - ./scripts/smoketest.sh https://kfz-test.intern rules: - if: $CI_COMMIT_BRANCH == "main"
Aufgaben:
- Bring die Pipeline vollständig zum Laufen und miss die Gesamtdauer.
- Verschiebe
geheimnisse-suchentestweise in die letzte Stufe. Was ändert sich an der Rückmeldezeit? - Lass
trivyeine kritische Schwachstelle finden — etwa mit einem absichtlich veralteten Basis-Image. - Baue die Vorlage aus 7.4 und lagere die Python-Stufen dorthin aus.
- Prüfe im Protokoll, ob irgendwo ein Geheimnis unmaskiert auftaucht.
7.8 Anti-Pattern
| Anti-Pattern | Folge | Abhilfe |
|---|---|---|
| Pipeline zusammengeklickt | nicht versioniert, nicht reproduzierbar | Pipeline as Code |
latest als Tag | unklar, was läuft | Commit-Kürzel und Digest |
| Geheimnisse im Protokoll | Kompromittierung | Maskierung, kein set -x |
| Fremde Actions auf beweglichen Tags | Lieferkettenrisiko | auf Commit-Hash festnageln |
| Kein Zwischenspeicher | jede Pipeline lädt alles neu | Cache je Abhängigkeitsdatei |
| Dauerhafte Runner mit Rückständen | „läuft mal, läuft mal nicht„ | ephemere Runner |
| Copy-Paste über zwanzig Projekte | Änderungen erreichen nie alle | zentrale Vorlagen mit Version |
| Scan ohne Konsequenz | Bericht wird erzeugt und ignoriert | Schwellwert setzt die Pipeline rot |
7.9 Reflexionsfrage
Wer könnte in deiner Umgebung eine Änderung an der Pipeline-Definition vornehmen, die beim nächsten Lauf mit Produktionsrechten ausgeführt wird — und würde das jemandem auffallen?
Weiter mit Kapitel 08 — Deployment-Strategien
