22 Einführung in GitLab CI/CD

Continuous Integration und Continuous Delivery (CI/CD) transformieren Softwareentwicklung von manuellem, fehleranfälligem Prozess zu automatisierter, reproduzierbarer Pipeline. GitLab CI/CD ist nicht nur ein Feature von GitLab – es ist eine vollständige DevOps-Plattform, die direkt in Git integriert ist.

Dieses Kapitel erklärt die fundamentalen Konzepte von CI/CD, wie GitLab CI/CD technisch funktioniert, und führt praktisch in .gitlab-ci.yml, Pipelines, und Runner ein. Am Ende verstehst du, warum CI/CD essentiell ist und wie du deine erste Pipeline erstellst.

22.1 Was ist CI/CD? Das Problem, das es löst

22.1.1 Die Welt vor CI/CD: Integration Hell

Stell dir vor: Zehn Entwickler arbeiten parallel an Features für zwei Wochen. Jeder entwickelt auf eigenem Branch. Am “Integration Day” mergen alle ihre Branches in main.

Was passiert:

Developer 1: git merge feature-a → 50 Konflikte
Developer 2: git merge feature-b → Kompilierungsfehler
Developer 3: git merge feature-c → Tests schlagen fehl
Developer 4: git merge feature-d → Feature-a und Feature-d sind inkompatibel
...

Ergebnis: Tagelange Integration, Debugging von Konflikten, frustrierte Teams. Dies ist Integration Hell.

22.1.2 Continuous Integration: Das Gegenmittel

Continuous Integration (CI): Entwickler integrieren Code kontinuierlich (mehrmals täglich) in gemeinsamen Branch.

Wie CI hilft:

Developer 1: git push → Pipeline läuft → Tests OK → Merged
Developer 2: git push → Pipeline läuft → Tests OK → Merged
Developer 3: git push → Pipeline läuft → Tests FAIL → Fix, dann Merged

Vorteile: - Konflikte sind klein: Stündliche Integration statt wöchentlich - Probleme werden sofort gefunden: Tests laufen bei jedem Push - main ist immer stabil: Nur Code, der Tests besteht, wird integriert

22.1.3 Continuous Delivery/Deployment: Vom Build zu Production

Continuous Delivery (CD): Code ist immer bereit, deployed zu werden. Deployment ist automatisiert, aber manuell getriggert.

Continuous Deployment: Code wird automatisch in Production deployed, wenn Tests passen.

CI: Automatisierte Tests, Builds, Quality Checks Continuous Delivery: Code kann deployed werden (button-click away) Continuous Deployment: Code wird deployed (fully automated)

22.2 GitLab CI/CD: Architektur und Integration

22.2.1 GitLab CI/CD ist nativ integriert

Im Gegensatz zu externen CI/CD-Tools (Jenkins, CircleCI) ist GitLab CI/CD built-in:

Vorteile: - Single Platform: Git + CI/CD + Issue Tracking + Registry in einem - Tight Integration: Push-Events triggern Pipelines automatisch - Permissions: GitLab-Permissions gelten für CI/CD (keine separate User-DB) - Unified UI: Pipeline-Status direkt in Merge Requests, Commits

Architektur-Rückblick:

Flow: 1. Developer pusht Code 2. Rails empfängt Push-Event 3. Rails parst .gitlab-ci.yml, erstellt Pipeline in DB 4. Sidekiq queued Jobs 5. Runner pollt für verfügbare Jobs 6. Runner führt Job aus (Build, Test, Deploy) 7. Runner reportet Result zurück 8. Rails updated Pipeline-Status

22.3 .gitlab-ci.yml: Configuration as Code

Die .gitlab-ci.yml-Datei im Repository-Root definiert die gesamte Pipeline.

22.3.1 Minimales Beispiel

# .gitlab-ci.yml
test:
  script:
    - echo "Hello, GitLab CI/CD!"

Was passiert: 1. Datei wird committed und gepusht 2. GitLab detected .gitlab-ci.yml 3. Pipeline mit einem Job “test” wird erstellt 4. Runner führt echo "Hello, GitLab CI/CD!" aus 5. Job-Status wird angezeigt (passed)

Im GitLab UI:

Pipeline #123 (main) ✓ passed
└─ test ✓ passed (5s)

22.3.2 Stages und Jobs

Stages organisieren Jobs in Phasen. Jobs innerhalb einer Stage laufen parallel, Stages laufen sequenziell.

stages:
  - build
  - test
  - deploy

build_job:
  stage: build
  script:
    - echo "Building application..."
    - npm install
    - npm run build

test_unit:
  stage: test
  script:
    - echo "Running unit tests..."
    - npm test

test_integration:
  stage: test
  script:
    - echo "Running integration tests..."
    - npm run test:integration

deploy_staging:
  stage: deploy
  script:
    - echo "Deploying to staging..."
    - ./deploy.sh staging

Execution-Flow:

Stage: build
└─ build_job ✓ (30s)

Stage: test (runs after build)
├─ test_unit ✓ (10s)      ← parallel
└─ test_integration ✓ (20s) ← parallel

Stage: deploy (runs after test)
└─ deploy_staging ✓ (15s)

Visualisierung:

22.3.3 Job-Eigenschaften

job_name:
  stage: test
  image: node:18-alpine      # Docker Image für Job
  before_script:             # Läuft vor script
    - npm install
  script:                    # Hauptkommandos
    - npm test
  after_script:              # Läuft nach script (auch bei Failure)
    - echo "Job finished"
  artifacts:                 # Files zum Download
    paths:
      - coverage/
    expire_in: 1 week
  cache:                     # Beschleunigt Builds
    paths:
      - node_modules/
  only:                      # Wann Job läuft
    - main
    - merge_requests
  variables:                 # Job-spezifische Variablen
    NODE_ENV: test

22.3.4 Realistische Pipeline: Node.js Application

# .gitlab-ci.yml
stages:
  - build
  - test
  - deploy

variables:
  NODE_VERSION: "18"

build:
  stage: build
  image: node:${NODE_VERSION}-alpine
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 day
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/

test:unit:
  stage: test
  image: node:${NODE_VERSION}-alpine
  script:
    - npm ci
    - npm test
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/
  coverage: '/Lines\s*:\s*(\d+\.\d+)%/'

test:lint:
  stage: test
  image: node:${NODE_VERSION}-alpine
  script:
    - npm ci
    - npm run lint
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/

deploy:staging:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk add --no-cache openssh-client
  script:
    - ssh user@staging.example.com "cd /app && ./deploy.sh"
  only:
    - main
  environment:
    name: staging
    url: https://staging.example.com

Pipeline-Flow:

1. build ✓ (45s)
   → Creates dist/ artifacts

2. test:unit ✓ (15s) ║ test:lint ✓ (10s)
   ↓ (parallel)      ║

3. deploy:staging ✓ (20s)
   → Only if on main branch

22.4 Runner: Die Execution-Engine

22.4.1 Was sind Runner?

Runner sind separate Prozesse/Server, die CI/CD-Jobs ausführen. Sie sind nicht Teil des GitLab-Servers.

Typen:

1. Shared Runners: - Verfügbar für alle Projekte - Auf GitLab.com: Von GitLab managed - Self-Managed: Von Admin konfiguriert

2. Specific Runners: - Registriert für spezifisches Projekt/Gruppe - Typisch: Eigene Hardware für spezielle Jobs (GPU, Large Memory)

3. Group Runners: - Verfügbar für alle Projekte in einer Gruppe

22.4.2 Runner-Executors

Executor definiert, wie Jobs ausgeführt werden:

1. Docker Executor (Empfohlen):

# Job läuft in Docker-Container
job:
  image: node:18-alpine
  script:
    - npm test

Vorteile: - Isoliert (jeder Job in fresh Container) - Reproduzierbar (gleiche Environment jedes Mal) - Kein Cleanup nötig (Container wird destroyed)

2. Shell Executor: - Job läuft direkt auf Runner-Maschine - Schneller (kein Container-Overhead) - Weniger isoliert (Job kann Runner-System modifizieren)

3. Kubernetes Executor: - Job läuft als Kubernetes-Pod - Hochskalierbar (dynamic Pod-Creation) - Cloud-Native

4. VirtualBox/Parallels: - Job läuft in VM - Maximale Isolation - Langsamer

22.4.3 Runner-Installation (Self-Managed)

# Ubuntu/Debian
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash
sudo apt-get install gitlab-runner

# Registration
sudo gitlab-runner register

# Prompted for:
# - GitLab URL: https://gitlab.com/
# - Registration Token: (from Project Settings → CI/CD → Runners)
# - Description: my-runner
# - Tags: docker, linux
# - Executor: docker
# - Default Docker Image: alpine:latest

Config (/etc/gitlab-runner/config.toml):

[[runners]]
  name = "my-runner"
  url = "https://gitlab.com/"
  token = "abc123"
  executor = "docker"
  [runners.docker]
    image = "alpine:latest"
    privileged = false
    volumes = ["/cache"]
  [runners.cache]
    [runners.cache.s3]
      ServerAddress = "s3.amazonaws.com"
      BucketName = "gitlab-runner-cache"

22.4.4 Runner-Tags

Tags matchen Jobs zu Runnern:

# Job mit Tag "docker"
job:
  tags:
    - docker
  script:
    - echo "Runs on docker runner"

# Job mit Tag "gpu"
ml_training:
  tags:
    - gpu
  script:
    - python train_model.py

Runner mit Tag “docker” führt erste Job aus, Runner mit Tag “gpu” führt zweite Job aus.

22.5 Pipeline-Trigger: Wann läuft eine Pipeline?

22.5.1 1. Push Events (Default)

git commit -m "Add feature"
git push origin feature-branch

→ Pipeline wird automatisch getriggert.

22.5.2 2. Merge Request Pipelines

job:
  script:
    - echo "Runs on MR"
  only:
    - merge_requests

Pipeline läuft bei MR-Creation und jedem Push zum MR-Branch.

22.5.3 3. Scheduled Pipelines

UI: CI/CD → Schedules → New Schedule - Cron-Expression: 0 2 * * * (täglich um 2 Uhr) - Target Branch: main

job:
  script:
    - echo "Nightly build"
  only:
    - schedules

22.5.4 4. Manual Pipelines

UI: CI/CD → Pipelines → Run Pipeline - Branch auswählen - Variablen setzen (optional)

deploy_production:
  stage: deploy
  script:
    - ./deploy.sh production
  when: manual  # Requires manual trigger in UI

22.5.5 5. API-Triggered Pipelines

curl --request POST \
  --form "token=$CI_TRIGGER_TOKEN" \
  --form ref=main \
  "https://gitlab.com/api/v4/projects/:id/trigger/pipeline"

Nützlich für externe Systeme (Webhooks von Dritt-Services).

22.5.6 6. Pipeline-Trigger von anderen Projekten

# Project A: Trigger Pipeline in Project B
trigger_downstream:
  trigger:
    project: user/project-b
    branch: main

Multi-Project-Pipelines für Microservices.

22.6 CI/CD-Variablen: Configuration und Secrets

22.6.1 Predefined Variables

GitLab stellt hunderte Variablen automatisch bereit:

job:
  script:
    - echo "Project: $CI_PROJECT_NAME"
    - echo "Branch: $CI_COMMIT_REF_NAME"
    - echo "Commit: $CI_COMMIT_SHA"
    - echo "Runner: $CI_RUNNER_DESCRIPTION"
    - echo "Job: $CI_JOB_NAME"

Wichtige Variablen: - CI_PROJECT_DIR: Working Directory - CI_COMMIT_SHA: Commit Hash - CI_COMMIT_REF_NAME: Branch/Tag Name - CI_COMMIT_TAG: Tag (wenn getriggert von Tag) - CI_MERGE_REQUEST_IID: MR ID - CI_PIPELINE_ID: Pipeline ID - CI_JOB_ID: Job ID

Vollständige Liste

22.6.2 Custom Variables

Projekt-Level (Settings → CI/CD → Variables):

Key: DATABASE_URL
Value: postgresql://localhost/db
Protected: ☑ (nur auf protected branches)
Masked: ☑ (nicht in logs sichtbar)

In .gitlab-ci.yml:

variables:
  DEPLOY_ENV: staging
  NODE_VERSION: "18"

job:
  variables:
    CUSTOM_VAR: value
  script:
    - echo $DEPLOY_ENV
    - echo $NODE_VERSION
    - echo $CUSTOM_VAR

22.6.3 Sensitive Data (Secrets)

NIEMALS Secrets in .gitlab-ci.yml committen:

# ✗ FALSCH
job:
  script:
    - deploy --api-key sk_live_abc123  # SECRET EXPOSED!

✓ RICHTIG: Via GitLab UI:

Settings → CI/CD → Variables
Key: API_KEY
Value: sk_live_abc123
Protected: ☑
Masked: ☑
# ✓ RICHTIG
job:
  script:
    - deploy --api-key $API_KEY

22.6.4 File Variables

Für Multi-Line-Secrets (SSL-Certs, SSH-Keys):

Settings → CI/CD → Variables
Key: SSH_PRIVATE_KEY
Value: [paste entire key]
Type: File
job:
  script:
    - cat $SSH_PRIVATE_KEY  # Variable ist jetzt Pfad zu temp-file
    - ssh -i $SSH_PRIVATE_KEY user@host

22.7 Artifacts und Caching

22.7.1 Artifacts: Job-Outputs

Artifacts sind Files, die von Jobs generiert und zwischen Stages weitergegeben werden:

build:
  script:
    - npm run build
  artifacts:
    paths:
      - dist/          # Build-Output
    expire_in: 1 day   # Auto-delete nach 1 Tag

deploy:
  script:
    - cp dist/* /var/www/  # dist/ verfügbar von build-Job
  dependencies:
    - build                # Explizit: Nutze Artifacts von build

Download: Im GitLab UI → Pipeline → Job → Browse/Download Artifacts

22.7.2 Cache: Dependency-Beschleunigung

Cache beschleunigt Builds durch Speichern von Dependencies:

# Cache node_modules zwischen Jobs
variables:
  CACHE_KEY: ${CI_COMMIT_REF_SLUG}

job:
  cache:
    key: $CACHE_KEY
    paths:
      - node_modules/
  script:
    - npm ci    # Wenn Cache hit, sehr schnell
    - npm test

Artifacts vs. Cache:

Artifacts Cache
Purpose Job-Output (builds, reports) Beschleunigung (dependencies)
Reliability Garantiert verfügbar Best-Effort (kann fehlen)
Sharing Zwischen Jobs in Pipeline Zwischen Pipelines
Storage GitLab Server Runner-lokal oder S3

22.8 Pipeline-Visualisierung

GitLab visualisiert Pipelines im UI:

Pipeline Graph:

┌─────────┐
│  build  │
└────┬────┘
     │
     ├──────┬──────┐
     ▼      ▼      ▼
  ┌────┐ ┌────┐ ┌────┐
  │test│ │lint│ │scan│
  └──┬─┘ └──┬─┘ └──┬─┘
     └──────┴──────┘
          │
          ▼
     ┌────────┐
     │ deploy │
     └────────┘

Job-Logs: Live-Streaming während Job läuft, persistent nach Completion.

Pipeline Status in MRs:

Merge Request #123
Pipeline #456: ✓ passed (2m 34s)
├─ build ✓ 45s
├─ test ✓ 30s
└─ deploy ✓ 20s

[Merge] button enabled

22.9 Erstes Projekt: Praktischer Einstieg

22.9.1 1. Repository mit .gitlab-ci.yml erstellen

mkdir my-first-pipeline
cd my-first-pipeline
git init

# Create simple script
echo 'console.log("Hello, CI/CD!");' > app.js

# Create .gitlab-ci.yml
cat > .gitlab-ci.yml << 'EOF'
stages:
  - test
  - deploy

test_job:
  stage: test
  image: node:18-alpine
  script:
    - node app.js
    - echo "Tests passed!"

deploy_job:
  stage: deploy
  script:
    - echo "Deploying application..."
    - echo "Deployment complete!"
  only:
    - main
EOF

git add .
git commit -m "Initial commit with CI/CD"

22.9.2 2. Push zu GitLab

# Create project on GitLab.com
# Then:
git remote add origin git@gitlab.com:username/my-first-pipeline.git
git push -u origin main

22.9.3 3. Pipeline beobachten

GitLab UI: 1. Navigate to project 2. CI/CD → Pipelines 3. Click on Pipeline #1 4. Watch jobs execute in real-time

Ergebnis:

Pipeline #1 (main) ✓ passed (15s)
├─ test_job ✓ passed (8s)
│  Output: Hello, CI/CD!
│          Tests passed!
└─ deploy_job ✓ passed (5s)
   Output: Deploying application...
           Deployment complete!

22.9.4 4. MR mit Pipeline

git checkout -b feature-update
echo 'console.log("Updated!");' > app.js
git add app.js
git commit -m "Update message"
git push origin feature-update

Create Merge Request in GitLab UI → Pipeline läuft automatisch auf feature-update.

22.10 GitLab CI/CD vs. Alternativen

22.10.1 Jenkins

Jenkins: - Self-Hosted only - Plugin-basiert (tausende Plugins) - Groovy-basierte Pipelines oder UI-Konfiguration - Sehr flexibel, aber komplex

GitLab CI/CD: - Native Integration in GitLab - YAML-basierte Konfiguration - Einfacher für Standard-Workflows - Keine separate Installation nötig

22.10.2 GitHub Actions

GitHub Actions: - Native in GitHub - YAML-basierte Workflows - Marketplace für Actions (wiederverwendbare Components) - Enge GitHub-Integration

GitLab CI/CD: - Mehr DevOps-Features (Registry, Security Scanning, etc.) - Unified Platform (nicht nur Git + CI) - Self-Managed-Option

22.10.3 CircleCI, TravisCI

Externe SaaS-Tools: - Weniger Integration mit Git-Hosting - Separate Accounts, Permissions - Oft pro-Minute-Billing

GitLab CI/CD: - Single Platform, single Login - GitLab-Permissions gelten - Self-Managed = unbegrenzte CI/CD

22.11 Zusammenfassung: CI/CD als DevOps-Fundament

GitLab CI/CD transformiert Code-Commits zu deployed Applikationen – automatisiert, reproduzierbar, schnell.

Core Concepts: - .gitlab-ci.yml: Configuration as Code - Stages & Jobs: Organisierte Execution - Runner: Execution-Engine (Docker, Shell, K8s) - Artifacts & Cache: Data-Sharing und Performance - Variables: Configuration und Secrets - Pipeline Triggers: Push, MR, Schedule, Manual, API

Workflow:

Developer → git push → GitLab → Pipeline Created → Runner → Job Execution → Results

Vorteile: - Frühe Feedback: Tests laufen sofort bei Push - Konsistenz: Gleiche Build-Environment jedes Mal - Schnelligkeit: Parallele Jobs, Caching - Qualität: Automatisierte Tests, Linting, Security Scans - Confidence: Nur getesteter Code in Production

Die folgenden Kapitel vertiefen diese Konzepte: YAML-Syntax, Runner-Konfiguration, Advanced Patterns, Security-Integration, Deployment-Strategien.

GitLab CI/CD ist nicht nur Automation – es ist die Infrastruktur, die moderne Software-Delivery ermöglicht. Master es, und du transformierst Entwicklung von manuell zu automatisiert, von langsam zu schnell, von fragil zu robust.