.gitlab-ci.yml: Die
Pipeline-Konfiguration meisternDie .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.
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
Scalars (einfache Werte):
string: "Hello"
number: 42
boolean: true
null_value: nullListen:
# 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 KommentarHä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.gitlab-ci.yml-Struktur# 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.
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)
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)
Single Command:
job:
script: echo "One-liner"Multiple Commands:
job:
script:
- echo "Command 1"
- echo "Command 2"
- npm install
- npm testMulti-Line Command (mit |):
job:
script:
- |
if [ "$CI_COMMIT_BRANCH" == "main" ]; then
echo "On main branch"
else
echo "On feature branch"
fijob:
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.
default:
image: node:18-alpine
build:
script:
- npm install # Läuft in node:18-alpine
test:
script:
- npm test # Auch in node:18-alpinedefault:
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)job:
image: registry.gitlab.com/myproject/custom-image:latest
script:
- ./run-tests.shGitLab 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"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 nutzenVerfü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 PINGvariables:
DATABASE_URL: postgresql://localhost/db
NODE_ENV: test
job:
script:
- echo $DATABASE_URL
- echo $NODE_ENVvariables:
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ügbarjob:
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 |
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ügbarMasked: Variable wird in Logs durch
[masked] ersetzt.
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/build:
script:
- npm run build
artifacts:
paths:
- dist/Effekt: dist/-Verzeichnis wird
gespeichert und in nachfolgenden Jobs verfügbar.
build:
script:
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week # Auto-delete nach 1 WocheExpiration-Syntax: 30 mins,
2 hrs, 3 days, 4 weeks,
5 months, never
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.
test:
script:
- npm test
artifacts:
reports:
junit: junit.xml # JUnit Test-Report
coverage_report:
coverage_format: cobertura
path: coverage/cobertura.xmlGitLab parst diese Reports und zeigt sie im UI: - Test-Failures direkt in MR - Coverage-Percentage als Badge - Trend-Grafiken über Zeit
job:
cache:
paths:
- node_modules/
script:
- npm ci # Wenn cache hit, sehr schnell
- npm testWie 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
Problem: Verschiedene Branches sollten verschiedene Caches haben.
job:
cache:
key: $CI_COMMIT_REF_SLUG # Branch-spezifisch
paths:
- node_modules/
script:
- npm cimain-Branch: Cache-Key =
main feature-x-Branch:
Cache-Key = feature-x
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 testPolicies: - 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)
| 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
rules ersetzt only/except
(veraltet) für komplexe Bedingungen.
deploy:
script:
- ./deploy.sh
rules:
- if: $CI_COMMIT_BRANCH == "main"Effekt: Job läuft nur auf
main-Branch.
deploy:
script:
- ./deploy.sh
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: always
- if: $CI_COMMIT_BRANCH =~ /^release-/
when: manual
- when: never # Default: läuft nichtLogik: - Auf main: Job läuft
automatisch - Auf release-*: Job ist manual - Sonst: Job
läuft nicht
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.
deploy_docker:
script:
- docker build -t myapp .
rules:
- exists:
- DockerfileEffekt: Job läuft nur, wenn Dockerfile
existiert.
Wenn keine stages definiert:
# Implizit:
stages:
- .pre
- build
- test
- deploy
- .post.pre und .post sind spezielle Stages
(laufen vor/nach allen anderen).
stages:
- prepare
- compile
- verify
- package
- publish
- cleanupProblem: 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.shDAG-Visualisierung:
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:integrationKonvention: Templates beginnen mit .
(werden nicht als Jobs ausgeführt).
include:
- local: '.gitlab/ci/build.yml'
- local: '.gitlab/ci/test.yml'include:
- project: 'my-group/ci-templates'
ref: main
file: '/templates/nodejs.yml'include:
- template: Security/SAST.gitlab-ci.ymlDie .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.