23 .gitlab-ci.yml: Die Pipeline-Konfiguration meistern

Die .gitlab-ci.yml-Datei ist die Definition deiner CI/CD-Pipeline als Code. Sie liegt im Repository-Root und beschreibt vollständig, was bei jedem Push, Merge Request oder Schedule passieren soll. Diese Datei ist mächtig – sie kann simple “run tests” Pipelines oder komplexe Multi-Stage-Deployments mit Dutzenden parallelen Jobs beschreiben.

Dieses Kapitel erklärt systematisch die YAML-Syntax, baut von einfachen zu komplexen Konfigurationen auf, zeigt Best Practices, und erklärt fortgeschrittene Features wie extends, include, rules, und needs.

23.1 YAML-Grundlagen: Die Sprache verstehen

23.1.1 Was ist YAML?

YAML (YAML Ain’t Markup Language) ist eine menschenlesbare Daten-Serialisierungssprache. Im Gegensatz zu JSON oder XML ist YAML auf Lesbarkeit optimiert.

Grundprinzipien: - Indentation definiert Struktur (wie Python) - Spaces, keine Tabs (2 oder 4 Spaces konsistent) - Key-Value-Pairs: key: value - Listen: - item - Dictionaries: Verschachtelte Key-Value-Pairs

23.1.2 YAML-Syntax-Essentials

Scalars (einfache Werte):

string: "Hello"
number: 42
boolean: true
null_value: null

Listen:

# Style 1: Block
fruits:
  - apple
  - banana
  - orange

# Style 2: Flow
fruits: [apple, banana, orange]

Dictionaries (Maps):

# Style 1: Block
person:
  name: John
  age: 30
  city: Berlin

# Style 2: Flow
person: {name: John, age: 30, city: Berlin}

Multi-Line-Strings:

# Literal Block (behält Zeilenumbrüche)
script: |
  echo "Line 1"
  echo "Line 2"
  echo "Line 3"

# Folded Block (joined zu einer Zeile)
description: >
  This is a very long
  description that spans
  multiple lines.

Kommentare:

# Dies ist ein Kommentar
job: # Inline-Kommentar
  script:
    - echo "Hello"  # Noch ein Kommentar

23.1.3 YAML-Fehler vermeiden

Häufiger Fehler: Tabs statt Spaces

# ✗ FALSCH (Tabs)
job:
→script:
→→- echo "Hello"

# ✓ RICHTIG (Spaces)
job:
  script:
    - echo "Hello"

Häufiger Fehler: Inkonsistente Indentation

# ✗ FALSCH
job:
  script:
    - echo "Line 1"
     - echo "Line 2"  # 5 Spaces statt 4

# ✓ RICHTIG
job:
  script:
    - echo "Line 1"
    - echo "Line 2"

YAML-Validation: Online-Tools oder yamllint:

yamllint .gitlab-ci.yml

23.2 Die .gitlab-ci.yml-Struktur

23.2.1 Top-Level-Keywords

# Global Config
default:              # Defaults für alle Jobs
  image: alpine:latest
  before_script: []

workflow:             # Pipeline-Level Rules
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

variables:            # Global Variables
  DEPLOY_ENV: staging

stages:               # Pipeline Stages (Order)
  - build
  - test
  - deploy

include:              # Externe Configs
  - local: 'templates/.gitlab-ci-template.yml'

# Jobs
job_name:
  stage: build
  script:
    - echo "Building"

Diese Top-Level-Keywords organisieren die Pipeline. Jobs sind die eigentlichen Arbeitseinheiten.

23.3 Jobs: Die Bausteine der Pipeline

23.3.1 Minimaler Job

hello_world:
  script:
    - echo "Hello, GitLab CI!"

Das passiert: 1. Runner nimmt Job 2. Führt echo "Hello, GitLab CI!" aus 3. Reportet Success

Stage: Default ist .pre (läuft vor allen definierten Stages)

23.3.2 Job mit Stage

stages:
  - build
  - test

build_app:
  stage: build
  script:
    - echo "Building application"

test_app:
  stage: test
  script:
    - echo "Testing application"

Execution-Order: build_app → dann test_app (sequenziell, weil verschiedene Stages)

23.3.3 Script: Single vs. Multi-Line

Single Command:

job:
  script: echo "One-liner"

Multiple Commands:

job:
  script:
    - echo "Command 1"
    - echo "Command 2"
    - npm install
    - npm test

Multi-Line Command (mit |):

job:
  script:
    - |
      if [ "$CI_COMMIT_BRANCH" == "main" ]; then
        echo "On main branch"
      else
        echo "On feature branch"
      fi

23.3.4 before_script und after_script

job:
  before_script:
    - echo "Setup: Installing dependencies"
    - npm install
  script:
    - echo "Main: Running tests"
    - npm test
  after_script:
    - echo "Cleanup: Removing temp files"
    - rm -rf /tmp/*

Execution-Order: 1. before_script 2. script 3. after_script (läuft immer, auch bei Failure)

Use Cases: - before_script: Dependencies installieren, Environment setup - script: Eigentliche Arbeit (Build, Test, Deploy) - after_script: Cleanup, Logs hochladen

Wichtig: after_script kann Job-Status nicht beeinflussen. Wenn after_script fehlschlägt, bleibt Job-Status unverändert.

23.4 Image: Docker-Container für Jobs

23.4.1 Globales Image

default:
  image: node:18-alpine

build:
  script:
    - npm install  # Läuft in node:18-alpine

test:
  script:
    - npm test  # Auch in node:18-alpine

23.4.2 Job-spezifisches Image

default:
  image: node:18-alpine

build:
  script:
    - npm install

test_node:
  script:
    - npm test  # node:18-alpine (default)

test_python:
  image: python:3.11-slim
  script:
    - pytest  # python:3.11-slim (überschreibt default)

23.4.3 Image von Private Registry

job:
  image: registry.gitlab.com/myproject/custom-image:latest
  script:
    - ./run-tests.sh

GitLab authentifiziert automatisch mit $CI_JOB_TOKEN gegen GitLab Container Registry.

Für externe Registries:

# Settings → CI/CD → Variables
# DOCKER_AUTH_CONFIG = {"auths":{"registry.example.com":{"auth":"base64(user:pass)"}}}

job:
  image: registry.example.com/image:tag
  script:
    - echo "Authenticated via DOCKER_AUTH_CONFIG"

23.4.4 Services: Sidecar-Container

Use Case: Job braucht Database, Redis, etc.

test:
  image: node:18
  services:
    - postgres:14
    - redis:7-alpine
  variables:
    POSTGRES_DB: testdb
    POSTGRES_USER: user
    POSTGRES_PASSWORD: pass
  script:
    - npm test  # Tests können postgres und redis nutzen

Verfügbarkeit: - PostgreSQL: postgres:5432 (Hostname = Service-Name) - Redis: redis:6379

Aliase für Custom Hostnames:

services:
  - name: postgres:14
    alias: database
  - name: redis:7
    alias: cache

script:
  - psql -h database -U user testdb
  - redis-cli -h cache PING

23.5 Variables: Konfiguration und Secrets

23.5.1 Lokale Variablen

variables:
  DATABASE_URL: postgresql://localhost/db
  NODE_ENV: test

job:
  script:
    - echo $DATABASE_URL
    - echo $NODE_ENV

23.5.2 Job-Variablen

variables:
  GLOBAL_VAR: global

job1:
  variables:
    JOB_VAR: job-specific
  script:
    - echo $GLOBAL_VAR  # Verfügbar
    - echo $JOB_VAR     # Verfügbar

job2:
  script:
    - echo $GLOBAL_VAR  # Verfügbar
    - echo $JOB_VAR     # NICHT verfügbar

23.5.3 GitLab-Predefined Variables

job:
  script:
    - echo "Project: $CI_PROJECT_NAME"
    - echo "Branch: $CI_COMMIT_REF_NAME"
    - echo "Commit: $CI_COMMIT_SHA"
    - echo "Pipeline ID: $CI_PIPELINE_ID"
    - echo "Job ID: $CI_JOB_ID"
    - echo "Runner: $CI_RUNNER_DESCRIPTION"

Wichtige Predefined Variables:

Variable Beschreibung Beispiel
CI_PROJECT_DIR Working Directory /builds/user/project
CI_COMMIT_SHA Full Commit Hash 7e8f9a0b...
CI_COMMIT_SHORT_SHA Short Commit Hash 7e8f9a0
CI_COMMIT_REF_NAME Branch/Tag Name main oder v1.0.0
CI_COMMIT_TAG Tag Name (wenn Tag) v1.0.0
CI_COMMIT_BRANCH Branch Name (wenn Branch) main
CI_MERGE_REQUEST_IID MR ID 123
CI_PIPELINE_SOURCE Trigger-Source push, merge_request_event, schedule
CI_JOB_TOKEN Job-spezifischer Token Für API/Registry-Auth
CI_REGISTRY Registry URL registry.gitlab.com
CI_REGISTRY_IMAGE Image Path registry.gitlab.com/user/project

23.5.4 Protected und Masked Variables

Im GitLab UI: Settings → CI/CD → Variables

Key: API_KEY
Value: sk_live_abc123
Type: Variable
Protected: ☑  # Nur auf protected branches
Masked: ☑     # Nicht in logs sichtbar
deploy:
  script:
    - curl -H "Authorization: Bearer $API_KEY" api.example.com
  only:
    refs:
      - main  # Protected branch → API_KEY verfügbar

Masked: Variable wird in Logs durch [masked] ersetzt.

23.5.5 File Variables

UI-Setup:

Key: SSH_PRIVATE_KEY
Value: [paste entire SSH key]
Type: File
deploy:
  before_script:
    - eval "$(ssh-agent -s)"
    - ssh-add "$SSH_PRIVATE_KEY"  # Variable ist Filepath
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
  script:
    - scp -r dist/ user@server:/var/www/

23.6 Artifacts: Job-Outputs weitergeben

23.6.1 Einfache Artifacts

build:
  script:
    - npm run build
  artifacts:
    paths:
      - dist/

Effekt: dist/-Verzeichnis wird gespeichert und in nachfolgenden Jobs verfügbar.

23.6.2 Artifacts mit Expiration

build:
  script:
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 week  # Auto-delete nach 1 Woche

Expiration-Syntax: 30 mins, 2 hrs, 3 days, 4 weeks, 5 months, never

23.6.3 Dependencies: Explizite Artifact-Nutzung

build:
  stage: build
  script:
    - npm run build
  artifacts:
    paths:
      - dist/

test:
  stage: test
  script:
    - npm test  # dist/ automatisch verfügbar

deploy:
  stage: deploy
  dependencies:
    - build  # Nur Artifacts von build, nicht von test
  script:
    - scp -r dist/ server:/var/www/

Ohne dependencies: Job erhält Artifacts von allen vorherigen Jobs. Mit dependencies: []: Job erhält keine Artifacts.

23.6.4 Reports: Spezielle Artifacts

test:
  script:
    - npm test
  artifacts:
    reports:
      junit: junit.xml            # JUnit Test-Report
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura.xml

GitLab parst diese Reports und zeigt sie im UI: - Test-Failures direkt in MR - Coverage-Percentage als Badge - Trend-Grafiken über Zeit

23.7 Cache: Performance-Optimierung

23.7.1 Grundlegendes Caching

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

Wie Cache funktioniert: 1. Vor Job: Runner prüft, ob Cache existiert 2. Wenn ja: Extracted in Working Directory 3. Nach Job: Cache-Paths werden archiviert und hochgeladen

23.7.2 Cache-Keys

Problem: Verschiedene Branches sollten verschiedene Caches haben.

job:
  cache:
    key: $CI_COMMIT_REF_SLUG  # Branch-spezifisch
    paths:
      - node_modules/
  script:
    - npm ci

main-Branch: Cache-Key = main feature-x-Branch: Cache-Key = feature-x

23.7.3 Cache-Policy

build:
  cache:
    key: $CI_COMMIT_REF_SLUG
    paths:
      - node_modules/
    policy: pull-push  # Default: Download + Upload
  script:
    - npm ci
    - npm run build

test:
  cache:
    key: $CI_COMMIT_REF_SLUG
    paths:
      - node_modules/
    policy: pull  # Nur Download, kein Upload
  script:
    - npm test

Policies: - pull-push: Download before, Upload after (Default) - pull: Nur Download (für Jobs, die Cache nicht ändern) - push: Nur Upload (für Jobs, die Cache initial befüllen)

23.7.4 Cache vs. Artifacts: Wann was?

Cache Artifacts
Purpose Beschleunigung (Dependencies) Job-Output (Build-Results)
Reliability Best-Effort (kann fehlen) Garantiert verfügbar
Sharing Zwischen Pipelines (über Zeit) Innerhalb Pipeline (zwischen Jobs)
Storage Runner-lokal oder S3 GitLab Server
Cleanup Automatisch bei Disk-Full Nach Expiration
Use Case node_modules/, .m2/, pip cache dist/, build/, Test-Reports

Faustregel: Dependencies → Cache, Build-Outputs → Artifacts

23.8 Rules: Moderne Job-Kontrolle

rules ersetzt only/except (veraltet) für komplexe Bedingungen.

23.8.1 Einfache Rules

deploy:
  script:
    - ./deploy.sh
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

Effekt: Job läuft nur auf main-Branch.

23.8.2 Multiple Rules

deploy:
  script:
    - ./deploy.sh
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: always
    - if: $CI_COMMIT_BRANCH =~ /^release-/
      when: manual
    - when: never  # Default: läuft nicht

Logik: - Auf main: Job läuft automatisch - Auf release-*: Job ist manual - Sonst: Job läuft nicht

23.8.3 Rules mit Changes

test_frontend:
  script:
    - npm test
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      changes:
        - "frontend/**/*"

Effekt: Job läuft nur bei MRs, die frontend/-Files ändern.

23.8.4 Rules mit Exists

deploy_docker:
  script:
    - docker build -t myapp .
  rules:
    - exists:
        - Dockerfile

Effekt: Job läuft nur, wenn Dockerfile existiert.

23.9 Stages: Pipeline-Organisation

23.9.1 Default Stages

Wenn keine stages definiert:

# Implizit:
stages:
  - .pre
  - build
  - test
  - deploy
  - .post

.pre und .post sind spezielle Stages (laufen vor/nach allen anderen).

23.9.2 Custom Stages

stages:
  - prepare
  - compile
  - verify
  - package
  - publish
  - cleanup

23.9.3 Needs: DAG-Pipelines

Problem: Stages sind strikt sequenziell. Aber manchmal können Jobs früher starten.

stages:
  - build
  - test
  - deploy

build_app:
  stage: build
  script:
    - npm run build
  artifacts:
    paths:
      - dist/

test_unit:
  stage: test
  needs: [build_app]  # Braucht nur build_app
  script:
    - npm test

test_integration:
  stage: test
  needs: [build_app]
  script:
    - npm run test:integration

deploy:
  stage: deploy
  needs: [test_unit, test_integration]
  script:
    - ./deploy.sh

DAG-Visualisierung:

23.10 Extends: DRY-Prinzip

Problem: Viele Jobs haben ähnliche Configuration.

.test_template:
  image: node:18
  before_script:
    - npm ci

test_unit:
  extends: .test_template
  script:
    - npm test

test_integration:
  extends: .test_template
  script:
    - npm run test:integration

Konvention: Templates beginnen mit . (werden nicht als Jobs ausgeführt).

23.11 Include: Externe Konfigurationen

23.11.1 Local Include

include:
  - local: '.gitlab/ci/build.yml'
  - local: '.gitlab/ci/test.yml'

23.11.2 Project Include

include:
  - project: 'my-group/ci-templates'
    ref: main
    file: '/templates/nodejs.yml'

23.11.3 Template Include

include:
  - template: Security/SAST.gitlab-ci.yml

23.12 Zusammenfassung

Die .gitlab-ci.yml ist mächtig. Systematisch aufgebaut:

Basics: Jobs mit script, organized in stages Configuration: image, variables, before_script, after_script Data-Sharing: artifacts (inter-job), cache (inter-pipeline) Control-Flow: rules (wann läuft Job), needs (DAG), workflow (pipeline-level) Reusability: extends (DRY), include (modularität), default (globals)

Master .gitlab-ci.yml, und CI/CD wird von kryptisch zu klar, von limitiert zu unbegrenzt mächtig.