Bash · Release-Skripte · Dry-Run · Deployment · DevOps
Release-Skripte mit Checkpoints
und Dry-Run-Modus

Ein Release-Skript ohne Dry-Run-Modus ist ein Blindflug in die Produktion. Mit Checkpoint-Flags, --dry-run, Rollback-Logik und einem vollständigen Schritt-für-Schritt-Protokoll entstehen Release-Skripte, die man vorher testen, bei Fehlern abbrechen und nach Unterbrechungen fortsetzen kann.

17 Min. Lesezeit --dry-run · Checkpoint-Flags · Rollback · Protokoll · getopts Bash 4.x · 5.x · CI/CD · Deployment

1. Warum Release-Skripte Dry-Run und Checkpoints brauchen

Release-Skripte sind unter den Shell-Skripten diejenigen mit dem höchsten Schadenspotenzial: Sie modifizieren Datenbanken, überschreiben Dateien, starten und stoppen Dienste und leeren Caches — in einer Produktionsumgebung, die echten Traffic verarbeitet. Ein Release-Skript, das bei einem Fehler in Schritt 7 von 12 abbricht und keinen definierten Zustand hinterlässt, schafft Mehrarbeit und Ausfallzeiten, die durch eine bessere Struktur vermeidbar wären.

Der Dry-Run-Modus ist die erste Schutzmaßnahme: Er führt alle Prüfungen, Validierungen und Protokollierungen durch, überspringt aber alle destruktiven Operationen. So kann ein Release-Skript vor dem echten Einsatz vollständig gegen die Zielumgebung simuliert werden, ohne Daten zu verändern. Checkpoints sind die zweite Schutzmaßnahme: Nach jedem erfolgreich abgeschlossenen Schritt wird ein Status-Flag gesetzt, sodass bei einem Fehler und erneutem Start nur die nicht abgeschlossenen Schritte wiederholt werden — nicht das gesamte Release von vorne.

Rollback-Logik ist die dritte Ebene: Wenn ein Schritt fehlschlägt, stellt das Release-Skript den Zustand vor dem Schritt automatisch wieder her. Das setzt voraus, dass vor jedem destruktiven Schritt ein Snapshot oder Backup erstellt wird. Diese drei Mechanismen — Dry-Run, Checkpoints und Rollback — zusammen machen Release-Skripte zu deterministischen, sicheren Werkzeugen statt zu nervösen Einmaloperationen.

2. Grundarchitektur eines Release-Skripts

Ein produktionstaugliches Release-Skript folgt einer festen Struktur: Header mit Abhängigkeiten und Konfiguration, Flag-Parsing, Precondition-Checks, sequenzielle Schrittausführung mit Checkpoint-Tracking, Cleanup und abschließendem Deployment-Report. Die Schritte werden als Funktionen implementiert, die genau drei Dinge tun: den Vorher-Zustand sichern, die Änderung durchführen und bei Fehler die gesicherte Rollback-Funktion aufrufen. Diese Struktur macht das Release-Skript lesbar, testbar und erweiterbar ohne globale Seiteneffekte.

Die zentrale Variable für den Dry-Run-Modus ist DRY_RUN. Alle Funktionen, die Änderungen vornehmen, rufen ihre Befehle über eine Wrapper-Funktion run_cmd auf, die im Dry-Run-Modus den Befehl nur ausgibt, ohne ihn auszuführen. Das ist eleganter als das Verstreuen von [[ $DRY_RUN -eq 1 ]] && echo-Prüfungen über das ganze Release-Skript. Die Wrapper-Funktion protokolliert außerdem jeden Befehl mit Zeitstempel in das Audit-Log — sowohl im Dry-Run als auch im echten Lauf.


#!/usr/bin/env bash
# release.sh — Structured release script with dry-run and checkpoints
set -euo pipefail
IFS=$'\n\t'

readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly RELEASE_ID="$(date +%Y%m%d-%H%M%S)-$$"
readonly CHECKPOINT_DIR="/var/run/release-checkpoints"
readonly LOG_FILE="/var/log/releases/${RELEASE_ID}.log"
readonly ROLLBACK_STACK_FILE="${CHECKPOINT_DIR}/${RELEASE_ID}.rollback"

# ---- Flags (defaults) ----
DRY_RUN=0
START_FROM=""
SKIP_STEPS=()
FORCE=0

# ---- Core: run_cmd wraps all destructive operations ----
run_cmd() {
  local description="$1"; shift
  if [[ $DRY_RUN -eq 1 ]]; then
    printf '[DRY-RUN] %s\n  $ %s\n' "$description" "$*" >&2
    return 0
  fi
  printf '[EXEC]    %s\n  $ %s\n' "$description" "$*" >&2
  "$@"
}

# ---- Rollback stack: push commands to run on failure ----
push_rollback() {
  printf '%s\n' "$*" >> "$ROLLBACK_STACK_FILE"
}

execute_rollback() {
  if [[ ! -f "$ROLLBACK_STACK_FILE" ]]; then
    return 0
  fi
  echo "[ROLLBACK] Führe Rollback-Schritte aus..." >&2
  # Execute in reverse order (tac = reverse cat)
  while IFS= read -r cmd; do
    eval "$cmd" || echo "[ROLLBACK WARN] Schritt fehlgeschlagen: $cmd" >&2
  done < <(tac "$ROLLBACK_STACK_FILE")
  rm -f "$ROLLBACK_STACK_FILE"
}

3. Dry-Run-Modus: Befehle ankündigen statt ausführen

Die Implementierung eines robusten Dry-Run-Modus in Release-Skripten geht über das einfache Überspringen von Befehlen hinaus. Ein vollständiger Dry-Run führt alle Precondition-Checks durch — Umgebungsvariablen vorhanden? Abhängige Dienste erreichbar? Berechtigungen korrekt? — und gibt für jeden Schritt aus, welcher Befehl ausgeführt würde, mit welchen Argumenten und welche Seiteneffekte zu erwarten sind. Der Operator sieht nach einem Dry-Run genau, was das Release-Skript tun wird, ohne dass etwas geändert wurde.

Wichtig für den Dry-Run-Modus in Release-Skripten: Nicht alle Operationen können vollständig simuliert werden. Datenbankmigrationen, die auf dem aktuellen Datenbankzustand basieren, können im Dry-Run nur ihren SQL ausgeben, nicht ihre tatsächlichen Auswirkungen. Das Release-Skript muss in diesen Fällen explizit dokumentieren, dass der Dry-Run für diesen Schritt unvollständig ist. Eine gute Praxis: Jede Schritt-Funktion hat eine separate describe_step()-Funktion, die im Dry-Run aufgerufen wird und den geplanten Schritt menschenlesbar beschreibt.

4. Checkpoint-Flags: Abgebrochene Releases fortsetzen

Checkpoints in Release-Skripten lösen das Problem des unvollständig ausgeführten Releases. Wenn ein Release-Prozess in Schritt 7 von 12 fehlschlägt und nach Behebung des Fehlers neu gestartet wird, sollen die Schritte 1–6 nicht erneut ausgeführt werden. Das Checkpoint-System speichert nach jedem erfolgreich abgeschlossenen Schritt eine Markierungsdatei in einem dedizierten Verzeichnis. Beim Start prüft das Release-Skript, welche Checkpoints bereits gesetzt sind, und überspringt die entsprechenden Schritte.

Das Checkpoint-Verzeichnis muss pro Release-Lauf eindeutig sein — mit der Release-ID als Teil des Pfads. So können parallele Releases auf verschiedenen Servern nicht gegenseitig ihre Checkpoints überschreiben. Die --resume-Option des Release-Skripts gibt die Release-ID an, deren Checkpoints weiterverwendet werden sollen. Mit --from STEP_NAME kann das Release-Skript auch ohne gespeicherte Checkpoints ab einem bestimmten Schritt gestartet werden — nützlich für Notfall-Deployments, die nur einen Teilbereich aktualisieren müssen.


#!/usr/bin/env bash
set -euo pipefail

# ---- Checkpoint management ----
readonly CHECKPOINT_DIR="${CHECKPOINT_DIR:-/var/run/release-checkpoints}"
mkdir -p "$CHECKPOINT_DIR"

checkpoint_set() {
  local step="$1"
  touch "${CHECKPOINT_DIR}/${RELEASE_ID}.${step}"
  printf '[CHECKPOINT] %s abgeschlossen\n' "$step" >&2
}

checkpoint_done() {
  local step="$1"
  [[ -f "${CHECKPOINT_DIR}/${RELEASE_ID}.${step}" ]]
}

checkpoint_clear() {
  rm -f "${CHECKPOINT_DIR}/${RELEASE_ID}."*
  printf '[CHECKPOINT] Alle Checkpoints für %s gelöscht\n' "$RELEASE_ID" >&2
}

# ---- Step execution with checkpoint logic ----
run_step() {
  local step_name="$1"
  local step_fn="$2"

  # Skip if checkpoint already set (resume mode)
  if checkpoint_done "$step_name"; then
    printf '[SKIP] Schritt %s bereits abgeschlossen (Checkpoint)\n' "$step_name" >&2
    return 0
  fi

  # Skip if step is in SKIP_STEPS array
  local skip
  for skip in "${SKIP_STEPS[@]:-}"; do
    if [[ "$skip" == "$step_name" ]]; then
      printf '[SKIP] Schritt %s übersprungen (--skip Flag)\n' "$step_name" >&2
      return 0
    fi
  done

  printf '\n[STEP] === %s ===\n' "$step_name" >&2
  local start_ts
  start_ts=$(date +%s)

  "$step_fn"  # Call the step function

  local duration=$(( $(date +%s) - start_ts ))
  printf '[STEP] %s abgeschlossen in %ds\n' "$step_name" "$duration" >&2

  [[ $DRY_RUN -eq 0 ]] && checkpoint_set "$step_name"
}

# ---- Example step implementation ----
step_composer_install() {
  push_rollback "echo 'Composer rollback: restore vendor from backup'"
  run_cmd "Composer-Abhängigkeiten installieren" \
    composer install --no-dev --optimize-autoloader --no-interaction
}

step_db_migrate() {
  push_rollback "bin/magento setup:rollback --db-rollback-file=${DB_BACKUP_FILE:-}"
  run_cmd "Datenbankmigrationen ausführen" \
    bin/magento setup:upgrade --keep-generated
}

# ---- Main release sequence ----
main() {
  run_step "composer_install"  step_composer_install
  run_step "db_migrate"        step_db_migrate
  # Add further steps as needed
  [[ $DRY_RUN -eq 0 ]] && checkpoint_clear
  printf '\n[RELEASE] Abgeschlossen: %s\n' "$RELEASE_ID" >&2
}

5. Rollback-Logik: Automatisch zurückrollen

Rollback in Release-Skripten ist kein optionales Feature, sondern eine Grundvoraussetzung für Produktionssicherheit. Die Implementierung folgt dem Stack-Prinzip: Vor jedem destruktiven Schritt wird ein Rollback-Kommando auf einen Stack geschrieben. Bei einem Fehler werden die Rollback-Kommandos in umgekehrter Reihenfolge ausgeführt — das zuletzt eingetragene zuerst. Das stellt sicher, dass Abhängigkeiten zwischen Schritten korrekt berücksichtigt werden: Eine Datenbankänderung, die auf einer Dateiänderung aufbaut, muss zuerst rückgängig gemacht werden, bevor die Datei wiederhergestellt wird.

Die Rollback-Logik in Release-Skripten muss selbst fehlertolerant sein. Wenn ein Rollback-Schritt fehlschlägt, darf der nächste Rollback-Schritt trotzdem ausgeführt werden. Jeder Fehler im Rollback wird geloggt, aber nicht als fataler Fehler behandelt. Am Ende des Rollbacks gibt das Release-Skript einen vollständigen Bericht aus: welche Rollback-Schritte erfolgreich waren und welche manuelles Eingreifen erfordern. Dieser Bericht ist die Grundlage für den Post-Mortem-Prozess.

6. Schritt-für-Schritt-Protokoll und Audit-Trail

Ein vollständiges Audit-Log ist für Release-Skripte in regulierten Umgebungen Pflicht und in allen anderen Umgebungen wertvolle Debugging-Hilfe. Das Log enthält für jeden Schritt: Zeitstempel des Starts und Endes, ausgeführter Befehl mit allen Argumenten, Exit-Code, stdout und stderr. Im Dry-Run-Modus enthält das Log, welche Befehle ausgeführt worden wären. So ist das Audit-Log eines Dry-Runs und des echten Releases strukturell identisch — der Unterschied liegt nur in einem Flag-Wert.

Für Release-Skripte in Multi-Server-Umgebungen ist ein zentralisiertes Log-Format wichtig. JSON-formatierte Log-Zeilen ({ "timestamp": "...", "release_id": "...", "step": "...", "status": "..." }) lassen sich mit jq filtern und in Monitoring-Systeme wie Elasticsearch oder Datadog importieren. Die Release-ID in jeder Log-Zeile ermöglicht die Korrelation aller Logs eines Releases über alle beteiligten Server hinweg — unabdingbar für Incident-Analysen nach einem fehlgeschlagenen Deploy.


#!/usr/bin/env bash
set -euo pipefail

readonly LOG_FILE="${LOG_DIR:-/var/log/releases}/${RELEASE_ID:-test}.log"
mkdir -p "$(dirname "$LOG_FILE")"

# ---- Structured JSON audit log ----
audit_log() {
  local level="$1" step="$2" message="$3"
  local ts
  ts="$(date --iso-8601=seconds 2>/dev/null || date -u '+%Y-%m-%dT%H:%M:%SZ')"
  printf '{"timestamp":"%s","release_id":"%s","level":"%s","step":"%s","message":"%s","dry_run":%s}\n' \
    "$ts" "${RELEASE_ID:-unknown}" "$level" "$step" \
    "${message//\"/\\\"}" "$([[ ${DRY_RUN:-0} -eq 1 ]] && echo true || echo false)" \
    >> "$LOG_FILE"
}

# ---- Step report summary ----
declare -A STEP_STATUS=()
declare -A STEP_DURATION=()

record_step_result() {
  local step="$1" status="$2" duration="$3"
  STEP_STATUS["$step"]="$status"
  STEP_DURATION["$step"]="$duration"
  audit_log "$status" "$step" "Step completed in ${duration}s"
}

print_release_report() {
  local sep
  sep="$(printf '%0.s─' {1..60})"
  printf '\n%s\n' "$sep" >&2
  printf '%-30s %10s %10s\n' "RELEASE REPORT: ${RELEASE_ID:-unknown}" "STATUS" "DAUER" >&2
  printf '%s\n' "$sep" >&2
  for step in "${!STEP_STATUS[@]}"; do
    local st="${STEP_STATUS[$step]}"
    local dur="${STEP_DURATION[$step]:-?}s"
    printf '%-30s %10s %10s\n' "$step" "$st" "$dur" >&2
  done
  printf '%s\n' "$sep" >&2
  printf 'Vollständiges Log: %s\n' "$LOG_FILE" >&2
}

# Ensure report is printed on exit
trap print_release_report EXIT

7. Kommandozeilen-Flags mit getopts und manuellem Parsing

Release-Skripte benötigen eine vollständige Kommandozeilen-Schnittstelle. Die Bash-internen Möglichkeiten: getopts für einzelne Buchstaben-Flags (-d, -f, -v) und manuelles Parsing für lange Flags (--dry-run, --from, --skip, --resume). getopts ist portabel und korrekt, aber nur für kurze Optionen. Für Release-Skripte mit vielen Optionen empfiehlt sich manuelles Parsing in einer while/case-Schleife, die $1 verbraucht und mit shift voranschreitet.

Das Flag-Parsing-Modul eines Release-Skripts sollte bei unbekannten Flags eine verständliche Fehlermeldung und einen Usage-Text ausgeben. Mit einer usage()-Funktion, die alle Flags mit Beschreibung und Standardwerten auflistet, wird das Release-Skript selbstdokumentierend. --help gibt usage() aus und beendet das Skript mit Exit-Code 0. --version gibt die Versionsnummer aus. Diese Konventionen machen Release-Skripte für neue Teammitglieder ohne Dokumentation benutzbar.

8. Stage-basierte Ausführung mit Abhängigkeiten

Komplexe Release-Skripte lassen sich durch Stage-Abhängigkeiten strukturieren. Statt einer einfachen sequenziellen Schritt-Liste definiert man Stages mit optionalen Abhängigkeiten: Stage B kann erst ausgeführt werden, wenn Stage A erfolgreich war. Das erlaubt parallele Ausführung unabhängiger Stages und erzwingt die korrekte Reihenfolge für abhängige Stages. In Bash ist eine einfache Abhängigkeitsprüfung über die Checkpoint-Flags realisierbar: Vor einer Stage wird geprüft, ob alle Dependency-Checkpoints gesetzt sind.

Ein praxisrelevantes Muster für Release-Skripte: Verschiedene Environments (dev, staging, prod) mit unterschiedlichen Stage-Konfigurationen. Eine Konfigurations-Map ordnet jedem Environment zu, welche Stages ausgeführt werden und welche übersprungen werden. In dev wird die Datenbankmigrationsphase in einem Dry-Run-ähnlichen Modus ausgeführt, der nur den SQL ausgibt. In staging läuft alles vollständig. In prod wird vor der Datenbankmigrationsphase automatisch ein Backup erstellt und ein Checkpoint mit dem Backup-Pfad gesetzt, sodass bei Rollback das korrekte Backup verwendet wird.

9. Release-Skript-Muster im Vergleich

Verschiedene Implementierungsansätze für Release-Skripte haben unterschiedliche Stärken. Die Wahl hängt von der Komplexität des Releases, der Teamgröße und den Anforderungen an Rollback und Auditierbarkeit ab.

Muster Dry-Run Checkpoints Rollback Einsatz
Sequenziell Einfach Nachrüstbar Manuell Einfache Deployments
Mit Checkpoints Vollständig Automatisch Stack-basiert Produktions-Releases
Stage-basiert Pro Stage Pro Stage Pro Stage Komplexe Multi-Service-Releases
Ohne Struktur Kein Kein Kein Nur für Dev-Umgebungen

Für die meisten Produktionsumgebungen ist das Checkpoint-Muster der beste Einstieg. Es ist überschaubar komplex, lässt sich schrittweise in bestehende Release-Skripte einbauen und bringt sofort den größten Nutzen: Abgebrochene Releases können sicher fortgesetzt werden, ohne die bereits abgeschlossenen Schritte zu wiederholen. Das Stage-basierte Muster empfiehlt sich erst, wenn mehrere unabhängige Systeme in einem Release koordiniert werden müssen — zum Beispiel bei Microservice-Deployments oder Multi-Region-Rollouts.

Mironsoft

Shell-Automatisierung, DevOps-Tooling und Deployment-Infrastruktur

Release-Skripte, die man vorher testen kann?

Wir entwickeln Release-Skripte mit vollständigem Dry-Run-Modus, Checkpoint-System und automatischer Rollback-Logik — für sichere, reproduzierbare Deployments in eurer Produktionsumgebung.

Release-Automation

Vollständige Release-Skripte mit Dry-Run, Checkpoints und Rollback für eure Deployment-Pipeline

Audit-Trail

JSON-strukturierte Deployment-Logs mit Release-ID-Korrelation für Monitoring und Post-Mortems

Rollback-Logik

Automatische Rollback-Strategie für Datenbank, Dateisystem und Service-Konfigurationen

10. Zusammenfassung

Produktionstaugliche Release-Skripte benötigen drei zentrale Sicherheitsmechanismen: Dry-Run-Modus für gefahrlose Vorab-Tests, Checkpoint-Flags für das sichere Fortsetzen nach Unterbrechungen und Rollback-Logik für die automatische Wiederherstellung bei Fehlern. Diese Mechanismen zusammen machen Release-Skripte von nervösen Einmaloperationen zu deterministischen, testbaren Werkzeugen. Der strukturelle Aufwand — die run_cmd-Wrapper-Funktion, das Checkpoint-Verzeichnis und der Rollback-Stack — amortisiert sich beim ersten abgebrochenen Release, das ohne Datenverlust fortgesetzt werden kann.

Das vollständige Audit-Log mit JSON-Struktur und Release-ID ist der weitere wichtige Bestandteil. Es ermöglicht die Nachverfolgung jedes Deployments — wer hat wann was deployed, welche Schritte wurden ausgeführt, wo wurde abgebrochen und wie wurde der Rollback durchgeführt. Für Teams, die mehrere Release-Skripte für verschiedene Komponenten betreiben, empfiehlt sich eine gemeinsame Bibliothek (lib/release.sh) mit den Checkpoint-, Rollback- und Logging-Funktionen. Das stellt sicher, dass alle Release-Skripte dieselben Sicherheitsmechanismen verwenden.

Release-Skripte mit Checkpoints und Dry-Run — Das Wichtigste auf einen Blick

Dry-Run-Modus

run_cmd-Wrapper für alle destruktiven Operationen. Im Dry-Run: Befehl ausgeben, nicht ausführen. Precondition-Checks laufen trotzdem vollständig.

Checkpoints

Nach jedem Schritt Markierungsdatei setzen. Beim Start abgeschlossene Schritte überspringen. --resume für Fortsetzung nach Abbruch.

Rollback-Stack

Vor jedem destruktiven Schritt Rollback-Kommando auf Stack schreiben. Bei Fehler: Kommandos in umgekehrter Reihenfolge ausführen.

Audit-Log

JSON-strukturierte Logs mit Release-ID, Schritt, Status und Dauer. Im Dry-Run und echten Lauf identisches Format — nur dry_run-Flag unterscheidet sich.

11. FAQ: Release-Skripte mit Checkpoints und Dry-Run

1Was ist ein Dry-Run-Modus?
Alle Validierungen laufen, destruktive Befehle werden ausgegeben aber nicht ausgeführt. Precondition-Checks sind vollständig aktiv.
2Checkpoints in Bash implementieren?
touch /var/run/checkpoints/${RELEASE_ID}.${STEP} nach jedem Schritt. Beim Start prüfen ob Datei existiert und Schritt überspringen.
3Rollback-Stack in Bash?
Rollback-Kommandos in Datei schreiben, bei Fehler mit tac in umgekehrter Reihenfolge ausführen. Fehler protokollieren, weiter machen.
4Lange Flags --dry-run parsen?
while/case-Schleife mit shift. getopts unterstützt nur kurze Flags (-d, -f). Für --dry-run, --from, --skip immer manuelles Parsing.
5Rollback-Schritt schlägt fehl?
Fehler loggen, nächsten Rollback-Schritt trotzdem ausführen. Am Ende Zusammenfassung mit manuell zu erledigenden Schritten ausgeben.
6Multi-Umgebungs-Konfiguration?
source config/${DEPLOY_ENV}.sh — separate Konfigurationsdateien pro Umgebung. Das Release-Skript selbst bleibt umgebungsunabhängig.
7Parallelen Release-Lauf verhindern?
flock: exec 9>/var/lock/release.lock; flock -n 9 || exit 1. Lock wird automatisch beim Prozessende freigegeben.
8Team nach Release benachrichtigen?
Im trap EXIT: curl an Slack/Teams Webhook. Im DRY_RUN überspringen oder als Test senden. Automatisch bei jedem Fehler.
9Checkpoints nach erfolgreichem Release löschen?
Ja. Nach vollständigem Erfolg alle Checkpoint-Dateien für die Release-ID löschen, um Konflikte bei zukünftigen Releases zu vermeiden.
10Release-Skripte ohne Produktion testen?
--dry-run gegen Staging. BATS mit Stubs für externe Befehle. Docker-Compose-Umgebung für Integration-Tests.