| FOTO | AUTO | EDV | AUDIO |

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:

  1. Bring die Pipeline vollständig zum Laufen und miss die Gesamtdauer.
  2. Verschiebe geheimnisse-suchen testweise in die letzte Stufe. Was ändert sich an der Rückmeldezeit?
  3. Lass trivy eine kritische Schwachstelle finden — etwa mit einem absichtlich veralteten Basis-Image.
  4. Baue die Vorlage aus 7.4 und lagere die Python-Stufen dorthin aus.
  5. 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