⚙️ Modul 5: CI/CD Pipelines
Lernziele: Sie verstehen CI/CD-Konzepte vollständig, kennen die GitLab CI/CD Architektur im Detail, können komplexe Pipelines für Netzwerk-Automatisierung erstellen und beherrschen Best Practices für sichere, wartbare Pipelines.
⏱️ Geschätzte Dauer: 180 Minuten | 📊 Schwierigkeit: Mittel bis Fortgeschritten
📋 Inhaltsverzeichnis
- 1. CI/CD Konzepte verstehen
- 2. GitLab CI/CD Architektur
- 3. .gitlab-ci.yml Syntax im Detail
- 4. Pipeline-Variablen & Secrets
- 5. Environments & Deployments
- 6. Manual Approvals & Gates
- 7. Netzwerk-Pipeline komplett
- 8. Praxis-Übungen (10 Übungen)
- 9. Troubleshooting
- 10. Best Practices
1. CI/CD Konzepte verstehen
Bevor wir in die Technik eintauchen, müssen wir verstehen, warum CI/CD so revolutionär für IT-Operations ist. Stellen Sie sich vor, Sie arbeiten in einer Autofabrik...
Die Fließband-Analogie
In einer modernen Autofabrik läuft kein einziges Fahrzeug vom Band, ohne mehrereQualitätsprüfungen durchlaufen zu haben:
Genau so funktioniert CI/CD: Ihr Code ist das Produkt, die Pipeline ist das Fließband, und die Tests sind die Qualitätskontrolleure.
Die drei Säulen: CI, CD und CD
Ja, es gibt tatsächlich zwei CDs – und das verwirrt viele Anfänger. Lassen Sie uns das klären:
Continuous Integration
Definition: Entwickler integrieren ihren Code mehrmals täglich in einen gemeinsamen Branch. Jede Integration wird automatisch gebaut und getestet.
Analogie: Stellen Sie sich ein Team vor, das ein Puzzle baut. Statt dass jeder stundenlang an seinem Teil arbeitet und am Ende hofft, dass alles zusammenpasst, prüft jeder sofort, ob sein neues Teil zum Gesamtbild passt.
Ziel: Fehler früh erkennen, "Integration Hell" vermeiden
Continuous Delivery
Definition: Code wird automatisch durch alle Test-Stages geschickt und ist jederzeit bereit für Production – aber ein Mensch drückt den finalen Deploy-Button.
Analogie: Das Auto ist fertig gebaut, geprüft, gewaschen und steht im Auslieferungsbereich. Aber der Versandleiter muss noch "Freigabe" erteilen, bevor der LKW losfährt.
Ziel: Jederzeit release-fähig sein, aber mit menschlicher Kontrolle
Continuous Deployment
Definition: Jede Änderung, die alle Tests besteht, wirdautomatisch in Production deployed. Kein manueller Eingriff nötig.
Analogie: Das Fließband läuft komplett automatisch – vom Rohstoff bis zur Auslieferung an den Kunden. Menschen überwachen nur noch, greifen aber nicht ein.
Ziel: Maximale Geschwindigkeit, minimale menschliche Fehler
Der traditionelle Weg vs. CI/CD
❌ Traditionell (Wasserfall)
- 1. Entwickler arbeiten wochenlang isoliert
- 2. Großer "Big Bang" Merge am Ende
- 3. Manuelles Testen über Tage/Wochen
- 4. Änderungsantrag, CAB-Meeting, Approval
- 5. Manuelles Deployment am Wochenende
- 6. Hoffen, dass alles klappt...
⏱️ Release-Zyklus: Wochen bis Monate
✅ CI/CD (Agile)
- 1. Kleine Änderungen, häufige Commits
- 2. Automatischer Build bei jedem Push
- 3. Automatische Tests in Sekunden
- 4. Automatische Dokumentation & Reports
- 5. One-Click Deployment (oder automatisch)
- 6. Automatisches Rollback bei Problemen
⏱️ Release-Zyklus: Stunden bis Tage
Warum CI/CD für Netzwerke?
Netzwerk-Engineers fragen oft: "Warum brauchen wir das? Wir sind doch keine Software-Entwickler!" – Die Antwort:
Frühe Fehler
Syntax-Fehler werden beim Commit erkannt, nicht erst beim Deployment
Audit Trail
Jede Änderung ist dokumentiert: Wer, wann, was, warum
Reproduzierbar
Gleicher Code = gleiches Ergebnis, jedes Mal
Easy Rollback
Bei Problemen: Ein Klick zurück zur letzten Version
2. GitLab CI/CD Architektur
Um GitLab CI/CD effektiv zu nutzen, müssen Sie verstehen, wie die einzelnen Komponenten zusammenspielen. Es ist wie ein Orchester – jedes Instrument hat seine Rolle.
Die Kernkomponenten
.gitlab-ci.yml
Die Partitur – definiert was gespielt wird
GitLab Server
Der Dirigent – koordiniert alles
Runners
Die Musiker – führen die Arbeit aus
GitLab Runners verstehen
Ein Runner ist der Arbeiter, der Ihre Jobs tatsächlich ausführt. GitLab selbst koordiniert nur – die Arbeit passiert auf den Runners.
🌍 Shared Runners
- • Von GitLab bereitgestellt (gitlab.com)
- • Für alle Projekte verfügbar
- • Begrenzte Minuten pro Monat (Free Tier)
- • Keine spezielle Konfiguration nötig
Use Case: Kleine Projekte, Standard-Builds
🏠 Specific Runners
- • Selbst gehostet (On-Premise)
- • Nur für bestimmte Projekte
- • Volle Kontrolle über Ressourcen
- • Zugriff auf interne Netze möglich
Use Case: Enterprise, Netzwerk-Labs, Security
Executors – Wie Jobs laufen
Der Executor bestimmt, wie ein Runner die Jobs ausführt:
| Executor | Beschreibung | Use Case |
|---|---|---|
| docker | Jeder Job läuft in einem frischen Docker-Container | ⭐ Standard, isoliert, reproduzierbar |
| shell | Jobs laufen direkt auf der Runner-Maschine | Zugriff auf lokale Tools/Hardware |
| kubernetes | Jobs laufen als Pods in K8s | Skalierung, Cloud-Native |
| ssh | Verbindet sich per SSH zu einem Server | Legacy-Systeme, spezielle Hardware |
Artifacts – Dateien zwischen Jobs teilen
Artifacts sind Dateien, die ein Job produziert und die an andere Jobs weitergegeben oder heruntergeladen werden können. Denken Sie an sie als "Arbeitsergebnisse":
# Job 1: Erstellt einen Report
generate-report:
stage: test
script:
- ansible-playbook check-network.yml > network-report.txt
- echo "Prüfung am $(date)" >> network-report.txt
artifacts:
paths:
- network-report.txt # Diese Datei wird gespeichert
expire_in: 1 week # Nach einer Woche löschen
when: always # Auch bei Fehler speichern
# Job 2: Nutzt den Report aus Job 1
analyze-report:
stage: deploy
script:
- cat network-report.txt # Datei ist verfügbar!
- grep -c "OK" network-report.txtArtifact-Typen
📄 paths
Beliebige Dateien und Ordner. Wird als ZIP herunterladbar.
📊 reports
Strukturierte Reports (JUnit, Coverage) die GitLab visualisiert.
🔗 dependencies
Steuert, welche Artifacts ein Job herunterladen darf.
Cache – Build-Zeiten optimieren
Cache ist wie der Browser-Cache: Dateien werden zwischen Pipeline-Runs wiederverwendet, um Zeit zu sparen. Anders als Artifacts:
📦 Artifacts
- • Zwischen Jobs einer Pipeline
- • Werden heruntergeladen und gespeichert
- • Für Ergebnisse (Reports, Binaries)
- • Zuverlässig, garantiert vorhanden
💾 Cache
- • Zwischen Pipeline-Runs
- • Bleibt auf dem Runner (lokal)
- • Für Dependencies (pip, npm)
- • Best-effort, kann fehlen
# Cache für Python Dependencies
default:
cache:
key: python-deps # Eindeutiger Schlüssel
paths:
- .cache/pip # pip Cache-Ordner
- venv/ # Virtual Environment
policy: pull-push # Lesen und schreiben
install-deps:
stage: prepare
script:
- python -m venv venv
- source venv/bin/activate
- pip install --cache-dir=.cache/pip -r requirements.txt
# Beim nächsten Run sind die Packages schon da!3. .gitlab-ci.yml Syntax im Detail
Die .gitlab-ci.yml ist das Herzstück Ihrer CI/CD Pipeline. Lassen Sie uns jeden wichtigen Bestandteil durchgehen.
Grundstruktur
# 1. STAGES - Die Phasen Ihrer Pipeline
stages:
- validate # Erste Phase
- test # Zweite Phase
- deploy # Dritte Phase
# 2. GLOBAL DEFAULTS - Gilt für alle Jobs
default:
image: python:3.11
tags:
- docker
before_script:
- pip install -q ansible
# 3. GLOBAL VARIABLES - Überall verfügbar
variables:
ANSIBLE_HOST_KEY_CHECKING: "false"
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
# 4. JOBS - Die eigentliche Arbeit
my-first-job:
stage: validate
script:
- echo "Hello World!"
- python --versionStages – Die Reihenfolge
Stages definieren die Reihenfolge, in der Jobs ausgeführt werden:
stages:
- prepare # 1. Vorbereitung (Dependencies)
- validate # 2. Validierung (Syntax, Lint)
- test # 3. Tests (Unit, Integration)
- build # 4. Build (Docker Image, etc.)
- deploy # 5. Deployment
- verify # 6. Post-Deployment Tests
- cleanup # 7. Aufräumen- Jobs der gleichen Stage laufen parallel
- Die nächste Stage startet erst, wenn die vorherige erfolgreich war
- Ein fehlgeschlagener Job stoppt die Pipeline (außer bei
allow_failure: true)
Jobs – Die Arbeitspakete
Jeder Job ist ein eigenständiges Arbeitspaket. Hier sind alle wichtigen Attribute:
mein-job:
# Welche Stage?
stage: test
# Welches Docker-Image?
image: ansible/ansible:latest
# Auf welchen Runnern? (Tags müssen matchen)
tags:
- docker
- linux
# VOR dem eigentlichen Script
before_script:
- pip install yamllint
- echo "Vorbereitung fertig"
# DAS EIGENTLICHE SCRIPT (Pflichtfeld!)
script:
- yamllint config.yaml
- ansible-lint playbook.yml
# NACH dem Script (auch bei Fehler)
after_script:
- echo "Aufräumen..."
- rm -rf tmp/
# Variablen nur für diesen Job
variables:
DEBUG: "true"
# Dateien speichern
artifacts:
paths:
- results/
expire_in: 1 day
# Wann soll der Job laufen?
only:
- main
- develop
# Wann NICHT?
except:
- tags
# Manueller Trigger?
when: manual
# Darf fehlschlagen?
allow_failure: false
# Timeout
timeout: 30 minutes
# Wiederholungen bei Fehler
retry:
max: 2
when:
- runner_system_failure
- stuck_or_timeout_failureRules – Moderne Bedingungen
rules ist der moderne Ersatz für only/except und bietet viel mehr Flexibilität:
deploy-production:
stage: deploy
script:
- ansible-playbook deploy.yml
rules:
# Regel 1: Auf main-Branch → Manual
- if: $CI_COMMIT_BRANCH == "main"
when: manual
allow_failure: false
# Regel 2: Bei Tags die mit "v" starten → Automatisch
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+/
when: on_success
# Regel 3: Merge Requests → Nur wenn Änderungen in bestimmten Dateien
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
- playbooks/**/*
- inventory/**/*
when: on_success
# Regel 4: Alles andere → Nicht ausführen
- when: neverWichtige CI/CD Variablen für Rules
| Variable | Beispiel | Beschreibung |
|---|---|---|
| $CI_COMMIT_BRANCH | main, develop | Aktueller Branch |
| $CI_COMMIT_TAG | v1.2.3 | Git Tag (falls vorhanden) |
| $CI_PIPELINE_SOURCE | push, merge_request_event, web | Was hat die Pipeline getriggert? |
| $CI_MERGE_REQUEST_TARGET_BRANCH_NAME | main | Ziel-Branch des MRs |
needs – Parallele Abhängigkeiten
Mit needs können Sie Jobs in verschiedenen Stages parallel laufen lassen, wenn sie voneinander abhängen:
stages:
- build
- test
- deploy
# Diese beiden Jobs laufen parallel
build-frontend:
stage: build
script: npm run build
artifacts:
paths: [dist/]
build-backend:
stage: build
script: go build -o app
# Dieser Job WARTET auf beide
integration-test:
stage: test
needs:
- build-frontend
- build-backend
script: ./run-tests.sh
# Dieser Job braucht NUR das Backend
deploy-api:
stage: deploy
needs:
- build-backend # Startet, sobald build-backend fertig ist!
script: ./deploy-api.shdeploy-api startet sofort nach build-backend, ohne auf build-frontend zu warten!
extends – Templates wiederverwenden
Mit extends können Sie wiederkehrende Konfigurationen in Templates auslagern (DRY-Prinzip):
# Template (beginnt mit "." → wird nicht ausgeführt)
.ansible-template:
image: ansible/ansible:latest
before_script:
- pip install -q jmespath netaddr
- ansible --version
variables:
ANSIBLE_FORCE_COLOR: "true"
tags:
- docker
# Jobs erben vom Template
lint-playbooks:
extends: .ansible-template
stage: validate
script:
- ansible-lint playbooks/
syntax-check:
extends: .ansible-template
stage: validate
script:
- ansible-playbook --syntax-check playbooks/*.yml
dry-run:
extends: .ansible-template
stage: test
script:
- ansible-playbook --check playbooks/deploy.yml.template). Diese werden von GitLab als "versteckt" behandelt und nicht als eigenständige Jobs ausgeführt.4. Pipeline-Variablen & Secrets Management
Variablen machen Ihre Pipelines flexibel und sicher. Sie können Werte zentral definieren, sensible Daten schützen und Pipelines dynamisch steuern.
Variablen-Hierarchie
GitLab CI/CD Variablen können an verschiedenen Stellen definiert werden. Spätere Definitionen überschreiben frühere:
Variablen in .gitlab-ci.yml
# Globale Variablen (für alle Jobs)
variables:
NDFC_HOST: "10.1.1.100"
ANSIBLE_HOST_KEY_CHECKING: "false"
ENVIRONMENT: "staging"
stages:
- test
- deploy
# Job mit eigenen Variablen (überschreibt globale)
test-staging:
stage: test
variables:
ENVIRONMENT: "staging" # Überschreibt global
DEBUG: "true" # Nur für diesen Job
script:
- echo "Environment: $ENVIRONMENT"
- echo "Debug: $DEBUG"
- echo "Host: $NDFC_HOST"
deploy-production:
stage: deploy
variables:
ENVIRONMENT: "production"
script:
- echo "Deploying to $ENVIRONMENT on $NDFC_HOST"Secrets sicher speichern
Schritt für Schritt: Secrets einrichten
- 1. In GitLab: Settings → CI/CD → Variables → "Add Variable"
- 2. Variable konfigurieren:
- • Key: NDFC_PASSWORD (Großbuchstaben, Unterstriche)
- • Value: Ihr geheimes Passwort
- • ☑️ Mask variable: Wird in Logs als [MASKED] angezeigt
- • ☑️ Protect variable: Nur auf protected Branches verfügbar
- 3. In Pipeline nutzen: Als
$NDFC_PASSWORD
deploy-to-ndfc:
stage: deploy
variables:
# Öffentliche Variablen OK
NDFC_HOST: "10.1.1.100"
NDFC_USER: "admin"
# NDFC_PASSWORD kommt aus GitLab CI/CD Variables!
script:
# Die Variable ist automatisch verfügbar
- echo "Connecting to $NDFC_HOST as $NDFC_USER"
- ansible-playbook -e "ndfc_password=$NDFC_PASSWORD" deploy.yml
rules:
- if: $CI_COMMIT_BRANCH == "main"Variable-Typen in GitLab
🔤 Variable (Standard)
Wird als Umgebungsvariable exportiert:
$MY_VAR📄 File
Inhalt wird in temporäre Datei geschrieben:
cat $MY_FILE# Beispiel: SSH Key als File-Variable
# In GitLab: Type = "File", Key = "SSH_PRIVATE_KEY"
deploy-via-ssh:
stage: deploy
script:
# $SSH_PRIVATE_KEY ist der PFAD zur temporären Datei!
- chmod 600 "$SSH_PRIVATE_KEY"
- ssh -i "$SSH_PRIVATE_KEY" admin@switch.local "show run"Dynamische Variablen
Sie können Variablen auch dynamisch während der Pipeline erstellen:
# Variablen an nachfolgende Jobs weitergeben
generate-version:
stage: prepare
script:
- VERSION="1.0.$(date +%Y%m%d)"
- echo "VERSION=$VERSION" >> build.env
artifacts:
reports:
dotenv: build.env # Macht Variablen verfügbar
use-version:
stage: build
needs:
- generate-version
script:
- echo "Building version $VERSION" # $VERSION ist verfügbar!5. Environments & Deployments
Environments in GitLab repräsentieren Ihre Zielumgebungen (Staging, Production) und ermöglichen Tracking, History und Rollbacks.
Environments definieren
stages:
- deploy
deploy-staging:
stage: deploy
script:
- ansible-playbook -i inventory/staging deploy.yml
environment:
name: staging
url: https://staging.network.local # Optional: Link in GitLab UI
deploy-production:
stage: deploy
script:
- ansible-playbook -i inventory/production deploy.yml
environment:
name: production
url: https://network.local
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manualWas bringt das?
Dashboard
Überblick aller Environments in Operations → Environments
History
Wer hat wann was deployed?
Rollback
Ein-Klick Rollback zu vorherigen Versionen
Protection
Nur bestimmte User dürfen deployen
Dynamische Environments
Für Feature Branches können Sie dynamische Environments erstellen, die automatisch wieder gelöscht werden:
# Review App pro Branch
deploy-review:
stage: deploy
script:
- echo "Deploying $CI_COMMIT_REF_SLUG"
- ./deploy-review.sh "$CI_COMMIT_REF_SLUG"
environment:
name: review/$CI_COMMIT_REF_SLUG # z.B. review/feature-xyz
url: https://$CI_COMMIT_REF_SLUG.review.local
on_stop: stop-review # Cleanup-Job
rules:
- if: $CI_COMMIT_BRANCH != "main" && $CI_COMMIT_BRANCH != "develop"
stop-review:
stage: deploy
script:
- ./cleanup-review.sh "$CI_COMMIT_REF_SLUG"
environment:
name: review/$CI_COMMIT_REF_SLUG
action: stop
rules:
- if: $CI_COMMIT_BRANCH != "main" && $CI_COMMIT_BRANCH != "develop"
when: manual6. Manual Approvals & Gates
Für kritische Infrastruktur wollen Sie nicht, dass jede Änderung automatisch deployed wird. Manual Approvals schaffen die nötige Kontrolle.
Einfacher manueller Trigger
deploy-production:
stage: deploy
script:
- ansible-playbook deploy.yml
environment:
name: production
when: manual # Erfordert Klick in GitLab UI
rules:
- if: $CI_COMMIT_BRANCH == "main"Approval mit Blockierung
Standardmäßig läuft die Pipeline weiter, auch wenn ein manueller Job noch nicht gestartet wurde. Mit allow_failure: false wird die Pipeline blockiert:
stages:
- validate
- approve
- deploy
lint-config:
stage: validate
script:
- yamllint config.yaml
# GATE: Blockiert bis jemand approved
approval-gate:
stage: approve
script:
- echo "Deployment approved by $GITLAB_USER_LOGIN at $(date)"
when: manual
allow_failure: false # ⚠️ Pipeline wartet hier!
deploy-network:
stage: deploy
script:
- ansible-playbook deploy.yml
needs:
- approval-gate # Startet erst nach Approval!Mehrere Approver für kritische Changes
Für besonders kritische Deployments können Sie mehrere Approvals erfordern:
stages:
- test
- approve
- deploy
# Zwei unabhängige Approvals erforderlich
approval-team-lead:
stage: approve
script:
- echo "Approved by Team Lead: $GITLAB_USER_LOGIN"
when: manual
allow_failure: false
approval-network-admin:
stage: approve
script:
- echo "Approved by Network Admin: $GITLAB_USER_LOGIN"
when: manual
allow_failure: false
deploy-core:
stage: deploy
script:
- ansible-playbook deploy-core-switches.yml
needs:
- approval-team-lead
- approval-network-admin # Beide müssen approved haben!Protected Environments
🛡️ Environment Protection einrichten
- 1. Settings → CI/CD → Protected Environments
- 2. "Protect an environment" → "production"
- 3. "Allowed to deploy" → Nur Senior Engineers
- 4. Optional: "Required approvals" → 2
7. Vollständige Netzwerk-Pipeline
Jetzt setzen wir alles zusammen! Diese Pipeline ist ein komplettes Beispiel für Netzwerk-Automatisierung mit VXLAN EVPN Fabric Deployment.
Pipeline-Architektur
- • yamllint
- • ansible-lint
- • syntax-check
- • dry-run
- • pre-checks
- • diff-report
- • ⚠️ manual gate
- • deploy fabric
- • deploy networks
- • ping-tests
- • bgp-checks
- • compliance
Die komplette .gitlab-ci.yml
# ============================================
# VXLAN EVPN Fabric Deployment Pipeline
# ============================================
stages:
- validate
- test
- approve
- deploy
- verify
# ============================================
# GLOBAL CONFIGURATION
# ============================================
variables:
# Ansible Settings
ANSIBLE_FORCE_COLOR: "true"
ANSIBLE_HOST_KEY_CHECKING: "false"
ANSIBLE_STDOUT_CALLBACK: "yaml"
# NDFC Connection (Secrets aus GitLab CI/CD Variables!)
NDFC_HOST: "10.1.1.100"
NDFC_USER: "admin"
# NDFC_PASSWORD → kommt aus CI/CD Variables
# Pipeline Settings
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
default:
image: python:3.11-slim
tags:
- docker
before_script:
- pip install --quiet ansible-core yamllint ansible-lint jmespath
cache:
key: pip-cache
paths:
- .cache/pip
# ============================================
# TEMPLATES
# ============================================
.ansible-base:
before_script:
- pip install --quiet ansible-core yamllint ansible-lint jmespath netaddr
- ansible-galaxy collection install cisco.dcnm --force
- ansible --version
# ============================================
# STAGE 1: VALIDATE
# ============================================
yaml-lint:
stage: validate
script:
- echo "🔍 Prüfe YAML-Syntax..."
- yamllint -c .yamllint.yml .
rules:
- changes:
- "**/*.yml"
- "**/*.yaml"
ansible-lint:
stage: validate
extends: .ansible-base
script:
- echo "🔍 Prüfe Ansible Best Practices..."
- ansible-lint playbooks/ --exclude .cache
rules:
- changes:
- playbooks/**/*
- roles/**/*
allow_failure: true # Warnungen blockieren nicht
syntax-check:
stage: validate
extends: .ansible-base
script:
- echo "🔍 Prüfe Playbook-Syntax..."
- |
for playbook in playbooks/*.yml; do
echo "Checking $playbook..."
ansible-playbook --syntax-check "$playbook"
done
artifacts:
when: on_failure
paths:
- ansible.log
expire_in: 1 day
# ============================================
# STAGE 2: TEST
# ============================================
dry-run-fabric:
stage: test
extends: .ansible-base
script:
- echo "🧪 Dry-Run Fabric Deployment..."
- ansible-playbook playbooks/deploy-fabric.yml --check --diff
environment:
name: staging
artifacts:
paths:
- dry-run-report.txt
expire_in: 1 week
rules:
- if: $CI_COMMIT_BRANCH =~ /^(main|develop)$/
pre-deployment-checks:
stage: test
extends: .ansible-base
script:
- echo "🔍 Pre-Deployment Checks..."
- ansible-playbook playbooks/pre-checks.yml
- echo "✅ Alle Switches erreichbar"
- echo "✅ NDFC API verfügbar"
- echo "✅ Keine Konflikte mit bestehendem Fabric"
artifacts:
reports:
junit: pre-check-results.xml
when: always
generate-diff-report:
stage: test
extends: .ansible-base
script:
- echo "📊 Generiere Änderungsbericht..."
- ansible-playbook playbooks/generate-diff.yml
- cat fabric-changes.md
artifacts:
paths:
- fabric-changes.md
expose_as: "Fabric Changes Report"
expire_in: 30 days
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
# ============================================
# STAGE 3: APPROVE (Manual Gate)
# ============================================
approval-gate:
stage: approve
script:
- echo "✅ Deployment freigegeben von: $GITLAB_USER_LOGIN"
- echo "📅 Zeitpunkt: $(date -Iseconds)"
- echo "🔖 Commit: $CI_COMMIT_SHA"
when: manual
allow_failure: false # Pipeline wartet!
rules:
- if: $CI_COMMIT_BRANCH == "main"
environment:
name: production
action: prepare
# ============================================
# STAGE 4: DEPLOY
# ============================================
deploy-fabric:
stage: deploy
extends: .ansible-base
script:
- echo "🚀 Deploying Fabric to NDFC..."
- |
ansible-playbook playbooks/deploy-fabric.yml -e "ndfc_host=$NDFC_HOST" -e "ndfc_user=$NDFC_USER" -e "ndfc_password=$NDFC_PASSWORD"
- echo "✅ Fabric Deployment abgeschlossen"
needs:
- approval-gate
environment:
name: production
url: https://$NDFC_HOST
artifacts:
paths:
- deployment-log.txt
when: always
expire_in: 90 days
rules:
- if: $CI_COMMIT_BRANCH == "main"
deploy-networks:
stage: deploy
extends: .ansible-base
script:
- echo "🌐 Deploying Networks & VRFs..."
- ansible-playbook playbooks/deploy-networks.yml
needs:
- deploy-fabric
environment:
name: production
rules:
- if: $CI_COMMIT_BRANCH == "main"
# ============================================
# STAGE 5: VERIFY (Post-Deployment)
# ============================================
verify-connectivity:
stage: verify
extends: .ansible-base
script:
- echo "🔍 Verifiziere Konnektivität..."
- ansible-playbook playbooks/verify-connectivity.yml
- echo "✅ Alle Ping-Tests erfolgreich"
needs:
- deploy-networks
artifacts:
reports:
junit: connectivity-report.xml
rules:
- if: $CI_COMMIT_BRANCH == "main"
verify-bgp:
stage: verify
extends: .ansible-base
script:
- echo "🔍 Verifiziere BGP EVPN Status..."
- ansible-playbook playbooks/verify-bgp.yml
- cat bgp-status.txt
needs:
- deploy-fabric
artifacts:
paths:
- bgp-status.txt
rules:
- if: $CI_COMMIT_BRANCH == "main"
compliance-check:
stage: verify
extends: .ansible-base
script:
- echo "📋 Compliance Check..."
- ansible-playbook playbooks/compliance-check.yml
allow_failure: true
rules:
- if: $CI_COMMIT_BRANCH == "main"
# ============================================
# ROLLBACK (Manuell bei Bedarf)
# ============================================
rollback:
stage: deploy
extends: .ansible-base
script:
- echo "⏪ Rolling back to previous state..."
- ansible-playbook playbooks/rollback.yml
- echo "✅ Rollback abgeschlossen"
when: manual
allow_failure: true
environment:
name: production
action: stop
rules:
- if: $CI_COMMIT_BRANCH == "main"Unterstützende Dateien
# YAMLlint Konfiguration
extends: default
rules:
line-length:
max: 120
level: warning
truthy:
allowed-values: ['true', 'false', 'yes', 'no']
comments:
min-spaces-from-content: 1
indentation:
spaces: 2
indent-sequences: true# Ansible-lint Konfiguration
skip_list:
- yaml[line-length]
- name[missing]
warn_list:
- experimental
exclude_paths:
- .cache/
- .git/8. Praxis-Übungen
Jetzt sind Sie dran! Die folgenden 10 Übungen führen Sie von einer einfachen "Hello World" Pipeline bis zu einer produktionsreifen Netzwerk-Automatisierung.
Erstellen Sie Ihre erste CI/CD Pipeline in GitLab.
Aufgabe:
- Erstellen Sie ein neues Projekt in GitLab (oder nutzen Sie ein bestehendes)
- Erstellen Sie die Datei
.gitlab-ci.ymlim Root des Repositories - Committen und pushen Sie
- Beobachten Sie die Pipeline in GitLab (CI/CD → Pipelines)
# Ihre erste Pipeline!
stages:
- test
hello-world:
stage: test
image: alpine:latest
script:
- echo "🎉 Hello World from CI/CD!"
- echo "Repository: $CI_PROJECT_NAME"
- echo "Branch: $CI_COMMIT_BRANCH"
- echo "Commit: $CI_COMMIT_SHORT_SHA"
- echo "Triggered by: $GITLAB_USER_NAME"Erwartetes Ergebnis:
Ein grüner Pipeline-Status und die Echo-Ausgaben im Job-Log.
Erweitern Sie die Pipeline um automatische YAML-Validierung.
Aufgabe:
- Erstellen Sie eine YAML-Testdatei
network-config.yaml - Fügen Sie einen yamllint Job zur Pipeline hinzu
- Bauen Sie absichtlich einen Fehler ein und beobachten Sie
- Korrigieren Sie den Fehler
# Beispiel Netzwerk-Konfiguration
fabric:
name: DC-Fabric
bgp_asn: 65001
spine_switches:
- name: spine-01
mgmt_ip: 10.1.1.1
- name: spine-02
mgmt_ip: 10.1.1.2
leaf_switches:
- name: leaf-01
mgmt_ip: 10.1.2.1
role: borderstages:
- validate
- test
yaml-lint:
stage: validate
image: python:3.11-slim
script:
- pip install yamllint
- echo "Checking all YAML files..."
- yamllint . -d relaxed
allow_failure: false
hello-world:
stage: test
image: alpine:latest
script:
- echo "YAML validation passed! ✅"
needs:
- yaml-lintExperiment:
Fügen Sie eine Zeile mit falschem Indent ein (3 Spaces statt 2) und beobachten Sie den Fehler.
Bauen Sie eine Pipeline mit mehreren Stages und Abhängigkeiten.
stages:
- prepare
- validate
- test
- report
# Stage 1: Vorbereitung
install-tools:
stage: prepare
image: python:3.11
script:
- pip install yamllint ansible-core
- echo "Tools installiert"
- pip freeze > requirements-frozen.txt
artifacts:
paths:
- requirements-frozen.txt
expire_in: 1 hour
# Stage 2: Validierung (2 Jobs parallel!)
validate-yaml:
stage: validate
image: python:3.11
script:
- pip install yamllint
- yamllint network-config.yaml
validate-schema:
stage: validate
image: python:3.11
script:
- echo "Schema validation would go here"
- echo "Both jobs run in parallel!"
# Stage 3: Tests
run-tests:
stage: test
image: python:3.11
needs:
- validate-yaml # Nur von diesem abhängig
script:
- echo "Running tests..."
- echo "Using frozen requirements from prepare stage"
- cat requirements-frozen.txt
# Stage 4: Report
generate-report:
stage: report
image: alpine:latest
script:
- echo "# Pipeline Report" > report.md
- echo "Date: $(date)" >> report.md
- echo "Status: SUCCESS" >> report.md
- cat report.md
artifacts:
paths:
- report.md
expire_in: 30 daysBeobachten Sie:
- Wie die Stages nacheinander laufen
- Wie validate-yaml und validate-schema parallel laufen
- Wie Artifacts weitergegeben werden
Lernen Sie den sicheren Umgang mit Variablen und Secrets.
Schritt 1: Geheime Variable anlegen
- GitLab → Settings → CI/CD → Variables
- Add Variable:
SECRET_API_KEY=super-secret-123 - ☑️ Mask variable aktivieren
Schritt 2: Pipeline mit Variablen
variables:
# Öffentliche Variablen (OK im Code)
API_URL: "https://api.example.com"
LOG_LEVEL: "INFO"
stages:
- test
show-variables:
stage: test
image: alpine:latest
variables:
JOB_SPECIFIC: "nur für diesen Job"
script:
# Öffentliche Variablen
- echo "API URL: $API_URL"
- echo "Log Level: $LOG_LEVEL"
- echo "Job Variable: $JOB_SPECIFIC"
# CI/CD vordefinierte Variablen
- echo "Pipeline ID: $CI_PIPELINE_ID"
- echo "Commit Author: $CI_COMMIT_AUTHOR"
# Geheime Variable (wird als [MASKED] angezeigt!)
- echo "Secret: $SECRET_API_KEY"
# NIE das tun (Secret würde geleakt):
# - cat file.txt | grep $SECRET_API_KEY # ❌
test-with-secret:
stage: test
image: python:3.11
script:
- |
python3 << 'EOF'
import os
secret = os.environ.get('SECRET_API_KEY', 'not set')
print(f"Secret length: {len(secret)} chars")
# In echtem Code: API-Call mit dem Secret
EOFWas passiert:
Das Secret wird im Log als [MASKED] angezeigt – die Maskierung funktioniert!
Nutzen Sie ein benutzerdefiniertes Docker-Image mit vorinstallierten Tools.
Option A: Öffentliches Image nutzen
stages:
- test
ansible-test:
stage: test
# Fertiges Ansible-Image von Docker Hub
image: cytopia/ansible:latest
script:
- ansible --version
- ansible-galaxy collection list
- echo "Ready for Network Automation!"Option B: Eigenes Image bauen (advanced)
FROM python:3.11-slim
# Netzwerk-Tools installieren
RUN pip install --no-cache-dir \
ansible-core \
netmiko \
napalm \
paramiko \
jmespath \
yamllint \
ansible-lint
# Ansible Collections installieren
RUN ansible-galaxy collection install \
cisco.dcnm \
cisco.nxos \
ansible.netcommon
WORKDIR /workspacestages:
- build
- test
build-image:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build -t $CI_REGISTRY_IMAGE:latest .
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker push $CI_REGISTRY_IMAGE:latest
only:
- main
use-custom-image:
stage: test
image: $CI_REGISTRY_IMAGE:latest
script:
- ansible --version
- python -c "import netmiko; print('Netmiko ready!')"Testen Sie Ansible Playbooks ohne tatsächliche Änderungen.
Schritt 1: Beispiel-Playbook erstellen
---
- name: Hello World Playbook
hosts: localhost
gather_facts: true
tasks:
- name: Show greeting
ansible.builtin.debug:
msg: "Hello from {{ ansible_hostname }}!"
- name: Create a test file (will be checked, not created)
ansible.builtin.file:
path: /tmp/test-file.txt
state: touchSchritt 2: Pipeline mit Dry-Run
stages:
- validate
- test
ansible-syntax:
stage: validate
image: cytopia/ansible:latest
script:
- ansible-playbook --syntax-check playbooks/hello.yml
ansible-dry-run:
stage: test
image: cytopia/ansible:latest
script:
- echo "🧪 Running Ansible in Check Mode (Dry-Run)..."
- ansible-playbook playbooks/hello.yml --check --diff
- echo "✅ Dry-Run completed - no changes made!"
artifacts:
paths:
- ansible.log
when: alwaysWas passiert:
--syntax-checkprüft nur die Syntax, führt nichts aus--checksimuliert die Ausführung ("was würde passieren?")--diffzeigt die Unterschiede an, die gemacht würden
Implementieren Sie einen manuellen Freigabe-Schritt.
stages:
- test
- approve
- deploy
automated-tests:
stage: test
image: alpine:latest
script:
- echo "Running automated tests..."
- sleep 2
- echo "✅ All tests passed!"
# DER GATE - Pipeline wartet hier!
manual-approval:
stage: approve
script:
- echo "======================================"
- echo "✅ DEPLOYMENT APPROVED"
- echo "======================================"
- echo "Approved by: $GITLAB_USER_LOGIN"
- echo "Approved at: $(date)"
- echo "Commit: $CI_COMMIT_SHORT_SHA"
when: manual
allow_failure: false # ⚠️ WICHTIG: Pipeline blockiert!
deploy-to-prod:
stage: deploy
image: alpine:latest
needs:
- manual-approval
script:
- echo "🚀 Deploying to production..."
- sleep 2
- echo "✅ Deployment successful!"
environment:
name: productionSo testen Sie:
- Pushen Sie die Änderungen
- Beobachten Sie, wie die Pipeline nach "test" anhält
- Der "approve" Job zeigt einen "Play" Button ▶️
- Klicken Sie auf den Button → Deploy startet
Generieren Sie Reports und machen Sie sie downloadbar.
stages:
- generate
- analyze
- report
generate-data:
stage: generate
image: python:3.11
script:
- mkdir -p output
- |
python3 << 'EOF'
import json
from datetime import datetime
data = {
"timestamp": datetime.now().isoformat(),
"pipeline_id": "$CI_PIPELINE_ID",
"devices_checked": 5,
"status": "healthy",
"findings": []
}
with open("output/health-check.json", "w") as f:
json.dump(data, f, indent=2)
print("Generated health-check.json")
EOF
- cat output/health-check.json
artifacts:
paths:
- output/
expire_in: 1 week
analyze-data:
stage: analyze
image: python:3.11
needs:
- generate-data
script:
- ls -la output/
- cat output/health-check.json
- |
python3 << 'EOF'
import json
with open("output/health-check.json") as f:
data = json.load(f)
print(f"Status: {data['status']}")
print(f"Devices checked: {data['devices_checked']}")
# Markdown Report erstellen
with open("output/report.md", "w") as f:
f.write("# Network Health Report\n\n")
f.write(f"**Status:** {data['status']}\n\n")
f.write(f"**Devices:** {data['devices_checked']}\n")
EOF
artifacts:
paths:
- output/
expose_as: "Network Reports" # Zeigt Download-Button in MR!
expire_in: 30 days
final-summary:
stage: report
image: alpine:latest
needs:
- analyze-data
script:
- echo "=== Final Report ==="
- cat output/report.mdSo laden Sie Artifacts herunter:
- In der Pipeline → Job → rechts "Browse" oder "Download"
- Bei Merge Requests: "Network Reports" Link direkt im MR
- API:
GET /projects/:id/jobs/:job_id/artifacts
Lernen Sie, wie man Pipeline-Fehler findet und behebt.
Häufige Fehler und Lösungen:
stages:
- debug
# Fehler 1: Exit Code nicht 0
# Lösung: || true oder set +e
handle-errors:
stage: debug
image: alpine:latest
script:
# ❌ Bricht ab wenn grep nichts findet:
# - grep "pattern" file.txt
# ✅ Ignoriert Fehler:
- grep "pattern" file.txt || true
# ✅ Oder: Exit-Status speichern
- |
if grep -q "pattern" file.txt; then
echo "Pattern gefunden"
else
echo "Pattern nicht gefunden (OK)"
fi
# Fehler 2: Fehlende Dependencies
# Lösung: before_script nutzen
check-dependencies:
stage: debug
image: python:3.11
before_script:
- pip install --quiet pyyaml
script:
- python -c "import yaml; print('YAML OK')"
# Fehler 3: Debug-Ausgaben hinzufügen
verbose-debug:
stage: debug
image: alpine:latest
script:
- set -x # Zeigt jeden Befehl an
- echo "Debug Mode aktiv"
- ls -la
- pwd
- env | sort # Alle Umgebungsvariablen
# Fehler 4: Interaktive Debug-Session (advanced)
# Nur für selbst-gehostete Runner!
# interactive-debug:
# stage: debug
# script:
# - sleep 3600 # 1 Stunde warten für SSH/Debug
# when: manualDebug-Checkliste:
- Job-Log lesen: Scroll zu "Running with gitlab-runner"
- Exit-Code prüfen: Welcher Befehl hatte Error?
- Variablen prüfen:
env | sorteinbauen - Artifacts prüfen: Logs als Artifact speichern
- Lokal testen: Gleichen Befehl in Docker ausführen
Das große Finale! Bauen Sie eine produktionsreife Pipeline.
Dateistruktur:
my-network-project/
├── .gitlab-ci.yml
├── .yamllint.yml
├── inventory/
│ ├── staging.yml
│ └── production.yml
├── playbooks/
│ ├── deploy.yml
│ ├── verify.yml
│ └── rollback.yml
├── group_vars/
│ └── all.yml
└── README.mdInventory (Beispiel):
all:
children:
switches:
hosts:
switch-01:
ansible_host: 10.1.1.1
switch-02:
ansible_host: 10.1.1.2
vars:
ansible_network_os: cisco.nxos.nxos
ansible_connection: network_cliPlaybooks:
---
- name: Deploy Network Configuration
hosts: switches
gather_facts: false
tasks:
- name: Show target device
ansible.builtin.debug:
msg: "Configuring {{ inventory_hostname }}"
# In Produktion: Echte Netzwerk-Module nutzen
- name: Configure VLAN (Example)
ansible.builtin.debug:
msg: "Would configure VLANs here"Die komplette Pipeline:
# ============================================
# PRODUCTION-READY NETWORK PIPELINE
# ============================================
stages:
- validate
- test
- approve
- deploy
- verify
variables:
ANSIBLE_FORCE_COLOR: "true"
ANSIBLE_HOST_KEY_CHECKING: "false"
default:
image: cytopia/ansible:latest
tags:
- docker
before_script:
- ansible --version
# --- VALIDATE ---
lint:
stage: validate
script:
- pip install yamllint ansible-lint
- yamllint .
- ansible-lint playbooks/
syntax:
stage: validate
script:
- ansible-playbook --syntax-check playbooks/*.yml
# --- TEST ---
dry-run-staging:
stage: test
script:
- ansible-playbook -i inventory/staging.yml playbooks/deploy.yml --check
environment:
name: staging
# --- APPROVE ---
approval:
stage: approve
script:
- echo "Approved by $GITLAB_USER_LOGIN at $(date)"
when: manual
allow_failure: false
rules:
- if: $CI_COMMIT_BRANCH == "main"
# --- DEPLOY ---
deploy-production:
stage: deploy
script:
- ansible-playbook -i inventory/production.yml playbooks/deploy.yml
needs:
- approval
environment:
name: production
rules:
- if: $CI_COMMIT_BRANCH == "main"
# --- VERIFY ---
post-deploy-check:
stage: verify
script:
- ansible-playbook -i inventory/production.yml playbooks/verify.yml
needs:
- deploy-production
artifacts:
paths:
- verification-report.txt
rules:
- if: $CI_COMMIT_BRANCH == "main"
# --- ROLLBACK (Manual) ---
rollback:
stage: deploy
script:
- ansible-playbook -i inventory/production.yml playbooks/rollback.yml
when: manual
allow_failure: true
environment:
name: production
action: stop✅ Checkliste für produktionsreif:
- ☐ Alle YAML-Dateien werden gelintet
- ☐ Syntax-Check für alle Playbooks
- ☐ Dry-Run gegen Staging-Umgebung
- ☐ Manual Approval Gate
- ☐ Deployment nur auf main-Branch
- ☐ Post-Deployment Verification
- ☐ Rollback-Option vorhanden
- ☐ Secrets in CI/CD Variables
9. Troubleshooting
Pipelines können aus vielen Gründen fehlschlagen. Hier sind die häufigsten Probleme und ihre Lösungen.
Häufige Pipeline-Fehler
❌ "This job is stuck"
Ursache: Kein passender Runner verfügbar (Tags stimmen nicht überein)
Lösung: Prüfen Sie Settings → CI/CD → Runners. Entfernen Sietags: oder nutzen Sie Tags, die Ihre Runner haben.
❌ "Job failed: exit code 1"
Ursache: Ein Befehl im Script hat einen Fehler zurückgegeben
Lösung: Job-Log lesen. Suchen Sie nach dem ersten roten Eintrag. Oft: Fehlende Dependencies, falsche Pfade, Typos.
❌ "yaml invalid"
Ursache: Syntax-Fehler in .gitlab-ci.yml
Lösung: Nutzen Sie den CI/CD Lint (CI/CD → Editor → "Validate"). Achten Sie auf Einrückung (2 Spaces, keine Tabs)!
❌ "Variable is empty"
Ursache: Protected Variable auf non-protected Branch
Lösung: Entweder Variable "unprotected" machen, oder auf protected Branch testen (main).
❌ "Artifacts too large"
Ursache: Artifact-Größe überschreitet Limit (100 MB default)
Lösung: Nur relevante Dateien als Artifact speichern. Nutzen Sie exclude: in paths.
Debug-Strategien
debug-job:
stage: test
script:
# 1. Alle Umgebungsvariablen anzeigen
- env | sort
# 2. Verzeichnisstruktur zeigen
- pwd
- ls -la
- find . -type f -name "*.yml"
# 3. Befehle mit Verbose-Modus
- set -x # Zeigt jeden Befehl vor Ausführung
- pip install ansible
- set +x
# 4. Fehler abfangen und trotzdem weitermachen
- ./might-fail.sh || echo "Fehler ignoriert"
# 5. Exit-Code speichern
- |
./important-script.sh
EXIT_CODE=$?
echo "Exit Code war: $EXIT_CODE"
if [ $EXIT_CODE -ne 0 ]; then
echo "Fehler aufgetreten, aber wir machen weiter"
fi
artifacts:
when: always # Auch bei Fehler Artifacts speichern
paths:
- "*.log"Pipeline lokal testen
# gitlab-runner lokal installieren (macOS)
brew install gitlab-runner
# Oder mit Docker (alle Plattformen)
docker pull gitlab/gitlab-runner:latest
# Pipeline lokal ausführen
gitlab-runner exec docker my-job-name
# Mit Variablen
gitlab-runner exec docker my-job-name \
--env "MY_VAR=value" \
--env "SECRET=test-secret"10. Best Practices
Diese Best Practices haben sich in Enterprise-Umgebungen bewährt und helfen, wartbare, sichere und effiziente Pipelines zu bauen.
Pipeline-Design
✅ Do
- • Schnelle Jobs zuerst (Fail Fast)
- • Kurze, fokussierte Jobs
- • Templates für Wiederverwendung
- • Aussagekräftige Job-Namen
- • Artifacts für wichtige Outputs
- • Cache für Dependencies
❌ Don't
- • Secrets im Code speichern
- • Große Monolith-Jobs
- •
allow_failure: trueüberall - • Unbegrenzte Artifact-Retention
- • Hardcoded Pfade/IPs
- • Fehlende Timeouts
Security Best Practices
# ✅ GUTE Security-Praktiken
variables:
# Nie Secrets hier!
PUBLIC_CONFIG: "ok"
deploy-secure:
stage: deploy
script:
# Secrets kommen aus CI/CD Variables
- echo "$DB_PASSWORD" | some-tool --password-stdin
# Niemals so:
# - some-tool --password="$DB_PASSWORD" # ❌ Sichtbar in Process List!
# Secrets in Dateien schreiben (temporär)
- echo "$SSH_KEY" > /tmp/key && chmod 600 /tmp/key
- ssh -i /tmp/key server
- rm /tmp/key
# Nur auf protected Branches deployen
rules:
- if: $CI_COMMIT_BRANCH == "main"
# Nur bestimmte Runner nutzen (für sensible Jobs)
tags:
- secure-runner
# Job-Token einschränken
id_tokens:
OIDC_TOKEN:
aud: https://vault.company.comPerformance-Tipps
| Tipp | Ersparnis | Wie |
|---|---|---|
| Cache nutzen | ~50% Build-Zeit | cache: paths: [.cache/] |
| Kleine Images | ~30% Pull-Zeit | python:3.11-slim statt python:3.11 |
| needs: statt Stages | Parallelität | needs: [job1] statt auf ganze Stage warten |
| Interruptible Jobs | Runner-Zeit | interruptible: true (bei neuem Push abbrechen) |
Checkliste für neue Pipelines
📋 Pipeline Review Checklist
Struktur:
- ☐ Stages sind sinnvoll benannt
- ☐ Jobs haben beschreibende Namen
- ☐ Templates werden verwendet
- ☐ Timeouts sind gesetzt
Security:
- ☐ Keine Secrets im Code
- ☐ Protected Variables korrekt
- ☐ Deploy nur auf main
- ☐ Manual Gates vorhanden
Reliability:
- ☐ Artifacts werden gespeichert
- ☐ Fehler werden abgefangen
- ☐ Rollback ist möglich
- ☐ Post-Deploy Verification
Performance:
- ☐ Cache ist konfiguriert
- ☐ Kleine Docker Images
- ☐ Jobs laufen parallel wo möglich
- ☐ Keine unnötigen Stages
Zusammenfassung
🎓 Das haben Sie in diesem Modul gelernt
Konzepte:
- ✅ CI/CD Unterschied verstanden
- ✅ GitLab Architektur (Runner, Executor)
- ✅ Stages, Jobs, Pipelines
- ✅ Artifacts vs. Cache
Praktische Skills:
- ✅ .gitlab-ci.yml schreiben
- ✅ Variablen und Secrets nutzen
- ✅ Manual Approvals einrichten
- ✅ Netzwerk-Pipeline bauen
🔑 Key Takeaways
- 1. CI/CD ist ein Fließband für Code – automatische Qualitätsprüfung bei jedem Commit
- 2.
.gitlab-ci.ymlist die "Partitur" Ihrer Pipeline - 3. Secrets gehören in CI/CD Variables, NIEMALS in den Code
- 4. Für Netzwerke: Continuous Delivery mit Manual Gates ist der Sweet Spot
- 5. Fail Fast: Schnelle Checks zuerst, teure Operationen später