IT-Admin.tech

Robuste Bash-Skripte: Fehlerhandling, set-Flags und idempotente Patterns

Ablaufdiagramm für Fehlerhandling, Locking und Cleanup in Bash-Skripten vor unscharfem Terminal
Ablaufdiagramm: Traps, Locking, atomische Writes und Docker ENTRYPOINT als visuelle Grundlage für betriebssichere Bash-Jobs.

Robuste Bash-Skripte sind ein Betriebsmerkmal: In Cron, systemd-Timern oder Docker-Entrypoints müssen Skripte Fehler klar behandeln, sauber aufräumen und wiederholt ausführbar sein, ohne Seiteneffekte zu erzeugen. Dieser Beitrag liefert ein kompaktes, praxisnahes Musterwerk aus set-Flags (Optionen, die das Verhalten der Shell ändern), Traps (Funktionen, die bei Signalen oder Fehlern ausgeführt werden) und idempotenten Patterns (Wiederholbarkeit ohne verändernde Nebeneffekte).

Warum einfache Skripte in Produktion versagen

Fehler treten selten durch komplexe Logik auf – meist sind es falsche Annahmen über Umgebung, Rückgabewerte oder Parallelität. Häufige Ursachen:

  • Fehler werden nicht sichtbar oder maskiert (Pipelines, grep ohne Treffer).
  • Unerwartete Umgebung in Cron/Containers (minimaler PATH, IFS, fehlende Tools).
  • Parallel laufende Jobs schreiben in dieselben Ressourcen.
  • Unvollständiges Cleanup nach Abbruch (Tempfiles, Mounts, Locks).

Das Ziel ist praktikable Robustheit: eindeutige Exit-Codes, nachvollziehbare Logs, idempotente Schritte und kontrollierte Nebenläufigkeit.

Robustes Grundgerüst für Bash-Skripte

Ein konsistentes Gerüst reduziert Fallen. Passen Sie es an Ihre Anforderungen an, aber behalten Sie die zentralen Bausteine: set-Flags, definierte Umgebung, Logging, Traps und Cleanup.

Shell
#!/usr/bin/env bash

set -Eeuo pipefail
IFS=$'nt'
PATH="/usr/sbin:/usr/bin:/sbin:/bin"
export LC_ALL=C

log() { printf '%s [%s] pid=%s %sn' "$(date -Is)" "$1" "$$" "$2" >&2; }

die() { log "ERROR" "$2"; exit "$1"; }

on_error() { local rc=$?; log "ERROR" "Fehler rc=${rc} in Zeile ${1:-?}: ${2:-?}"; exit "$rc"; }
cleanup() { :; }

trap 'on_error "$LINENO" "$BASH_COMMAND"' ERR
trap cleanup EXIT

Erklärung: set -e beendet bei nicht abgefangenen Fehlern, -u warnt bei unset-Variablen, pipefail sorgt dafür, dass Pipelines Fehler melden, und -E erlaubt ERR-Traps in Funktionen. IFS reduziert ungewollte Worttrennung, ein definierter PATH vermeidet unterschiedliche Tool-Versionen in Cron/Container.

set-Flags: Nutzen, Risiken, Praxis

set -e: sinnvoll mit expliziten Ausnahmen

set -e ist nützlich, wenn ein Fehler das Weitermachen gefährlich macht. Probleme entstehen, wenn typische Tools Exit-Code-1 für „kein Ergebnis“ verwenden (z. B. grep). Lösen Sie das durch explizite Prüfungen.

Shell
# Optionales Entfernen, Fehler toleriert
rm -f -- "/var/tmp/maybe-there" || true

# Grep ohne Treffer bewusst behandeln
if grep -q "pattern" file; then
  echo "gefunden"
else
  echo "nicht gefunden"
fi

set -u: schützt vor Tippfehlern

set -u verhindert stille Fehler bei leerem Variablengebrauch, verlangt aber Defaults oder klare Fehlermeldungen.

Shell
: "${BACKUP_DIR:?BACKUP_DIR ist nicht gesetzt}"
RETENTION_DAYS="${RETENTION_DAYS:-14}"

pipefail und -E: Diagnosen verbessern

pipefail macht Pipelines verlässlicher; -E und eine ERR-Trap geben Zeile und Kommando aus, was Cron-Logs wesentlich aussagekräftiger macht.

Fehlerhandling: Exit Codes, Traps und Rückfallpfade

Exit Codes standardisieren

Exit-Codes sind die einfachste Schnittstelle zu Monitoring und Orchestrierung. Legen Sie Teamregeln fest (z. B. 2 = Usage/Parameter, 10+ = externe Abhängigkeiten) und dokumentieren Sie sie im Runbook.

Shell
usage() { cat <<'EOF'
Usage: job.sh --source DIR --target DIR
EOF
}

# Beispiel: Parameterprüfung
[[ -n "$SOURCE" ]] || die 2 "--source fehlt"

Cleanup mit Zustandsmarken

EXIT-Trap läuft bei jedem Exit. Für komplexe Ressourcen verwenden Sie einfache Zustandsvariablen, damit Cleanup idempotent ist.

Shell
TMPDIR=""
MOUNTED=0
cleanup() {
  local rc=$?
  if [[ "$MOUNTED" -eq 1 ]]; then
    umount "/mnt/work" || log "WARN" "Unmount fehlgeschlagen"
  fi
  [[ -n "${TMPDIR}" && -d "${TMPDIR}" ]] && rm -rf -- "${TMPDIR}" || true
  log "INFO" "Beende mit rc=${rc}"
}

trap cleanup EXIT
TMPDIR="$(mktemp -d)"
mount /dev/sdb1 /mnt/work && MOUNTED=1

Idempotente Patterns: wiederholbar und sicher

Idempotenz heißt: Mehrfaches Ausführen ergibt denselben Zielzustand. Das ist zentral bei Deployments, Migrationen oder init-Skripten in Containern.

Check-then-Do mit belastbaren Prüfungen

Shell
ensure_dir() {
  local dir="$1" mode="$2"
  [[ -d "$dir" ]] || mkdir -p -- "$dir"
  chmod "$mode" -- "$dir"
}
ensure_dir "/var/lib/myjob" "0750"

Wichtig ist, was Sie prüfen: Existenz alleine reicht selten; prüfen Sie Inhalt, Rechte oder Dienstantworten, wenn nötig.

Marker-Files und atomare Writes

Marker-Files sind praktisch, aber nur mit atomarem Schreiben (tmp + mv) zuverlässig.

Shell
mark_done() {
  local marker="$1" tmp="${marker}.tmp.$$"
  printf '%sn' "$(date -Is)" > "$tmp"
  mv -f -- "$tmp" "$marker"
}

if [[ ! -f "/var/lib/myjob/.init_done" ]]; then
  # ...Initialisierung...
  mark_done "/var/lib/myjob/.init_done"
fi

Marker ersetzen keine Validierung: Prüfen Sie zusätzlich, ob der Erfolg auch wirklich erreicht wurde (Service antwortet, DB-Objekt existiert).

Locking gegen Parallelstarts

flock ist unter Linux robust: Kernel-Locks lösen sich beim Prozessende. Für verteilte Dateisysteme oder Multi-Host-Koordination braucht es externe Koordinatoren (DB, Redis, Consul).

Shell
LOCKFILE="/var/lock/myjob.lock"
exec 9>"$LOCKFILE"
if ! flock -n 9; then
  log "WARN" "Job läuft bereits, beende"
  exit 0
fi
log "INFO" "Lock erhalten"

Robuste Bash-Skripte: Signal-Handling und PID 1 im Container

In Containern übernimmt die ENTRYPOINT-Shell oft PID 1. PID 1 hat besondere Verantwortung: Es muss Signale korrekt weiterleiten und Kinderprozesse reapen (Zombies aufräumen). Wird die Shell nicht durch exec ersetzt, bleibt sie PID 1 und kann Signalweiterleitung verhindern—Shutdowns verzögern sich oder Orchestrierer wie Kubernetes erhalten falsche Exit-Codes. Ersatz: tini oder direkt exec verwenden.

Signal-Forwarding und Reaping

Traps für SIGTERM/SIGINT leiten Signale weiter, stoppen Hintergrundprozesse kontrolliert und warten mit wait auf das Beenden der Kinder.

Shell
term_handler() {
  log "INFO" "SIGTERM empfangen, leite an Kinder weiter"
  # Beispiel: pids enthält PIDs von Background-Prozessen
  for pid in "${pids[@]:-}"; do
    kill -TERM "$pid" 2>/dev/null || true
  done
  # Auf Kinder warten, damit keine Zombies bleiben
  wait
  exit 143
}

trap 'term_handler' SIGTERM SIGINT

# Beispiel: Dienst im Hintergrund starten
/usr/local/bin/myworker &
pids+=("$!")

# Hauptprozess wartet
wait -n || true

Alternativ: Verwenden Sie in Dockerfile ENTRYPOINT ["/sbin/tini", "--"] oder starten Sie Container mit --init, damit ein kleiner PID-1-Reaper die Aufgabe übernimmt.

Verteilte Locks: wenn flock nicht reicht

Für Single-Host ist flock oft ausreichend. Bei verteilten Systemen (NFS, mehrere Hosts) sind zentrale Koordination oder Datenbank-Locks die richtige Wahl. Beispiele:

Postgres Advisory Lock

Postgres bietet Advisory Locks (Anwendungs-gesichert) über einfache SQL-Aufrufe. Das ist nützlich, wenn Sie bereits eine relationale DB im Stack haben.

Shell
# Versucht Lock zu setzen und prüft Rückgabe
if psql -qAt -c "SELECT pg_try_advisory_lock(12345)" | grep -qx "t"; then
  log "INFO" "Advisory lock erhalten"
else
  log "WARN" "Lock konnte nicht gesetzt werden"
  exit 0
fi
# Später: Lock freigeben
psql -c "SELECT pg_advisory_unlock(12345)" || true

Hinweis: Advisory-Locks hängen an der DB-Session; bei Verbindungsabbruch werden sie freigegeben – das ist oft erwünscht.

Redis SETNX mit TTL

Redis kann mit SET resource value NX PX einfache Leader- oder Lock-Muster umsetzen. Achtung: Netzwerkpartitionen können zu stale Locks führen—TTL setzen und Lock-Owner prüfen.

Sichere Datei-Operationen, Rechte und Tempdirs

Fehler bei Dateioperationen führen oft zu Sicherheitsproblemen. Best Practices:

  • Erstellen temporärer Verzeichnisse mit mktemp -d und sichere Berechtigungen setzen (umask/ chmod).
  • Atomare Writes: in temporäre Datei schreiben und mit mv ersetzen.
  • Umgang mit Rechten: umask setzen oder Zielrechte explizit mit chmod korrigieren.
Shell
old_umask=$(umask)
umask 027
TMPDIR=$(mktemp -d -p /var/tmp myjob.XXXXXX)
chmod 0700 "$TMPDIR"
# ... arbeiten ...
umask "$old_umask"

Monitoring, Metriken und Logs praxisnah integrieren

Exit-Codes sind das primäre Signalisierungsmedium; ergänzend liefert strukturiertes Logging und Export von Metriken greifbare Vorteile für SRE/Monitoring (z. B. Prometheus Node Exporter Textfile Collector).

Shell
# Metrik als Textfile für node_exporter
METRIC_DIR="/var/lib/node_exporter/textfile_collector"
mkdir -p "$METRIC_DIR"
cat > "$METRIC_DIR/myjob.prom.tmp" <<EOF
# HELP myjob_last_run_seconds Unix timestamp des letzten Laufs
# TYPE myjob_last_run_seconds gauge
myjob_last_run_seconds $(date +%s)
EOF
mv -f "$METRIC_DIR/myjob.prom.tmp" "$METRIC_DIR/myjob.prom"

Zentral: Schreiben Sie nur in das vorgesehene Verzeichnis, nutzen atomische Moves, damit Exporter konsistente Files sehen.

Tests, CI-Integration und statische Analyse

Testen Sie Skripte nicht nur manuell: ShellCheck identifiziert Stil- und Sicherheitsfehler, und Unit-Tests für Shell-Funktionen (z. B. mit bats-core) fangen logische Fehler. Integrationstests simulieren fehlende Abhängigkeiten und Parallelstarts.

Yaml
# GitHub Actions: ShellCheck und einfache Linting-Checks
name: Shell CI
on: [push, pull_request]
jobs:
  shellcheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: ludeeus/action-shellcheck@v1
        with:
          files: "scripts/**/*.sh"

Troubleshooting: typische Fehlerbilder und Prüfsequenz

Wenn ein Job fehlschlägt, arbeiten Sie strukturiert:

  1. Prüfen Sie Logs (stderr/stdout) für ERR-Trap-Output. Suchen Sie nach der Zeilennummer und dem BASH_COMMAND, die die ERR-Trap liefert.
  2. Überprüfen Sie Umgebung: PATH, IFS, Variablen mit declare -p.
  3. Lock-Status prüfen: Existiert Lockfile, wer hält ihn? (ps aux | grep)
  4. In Container: War PID 1 die Shell? Prüfen Sie Proc-Tree und Signal-Handling.

Rollback- und Recovery-Strategie

Für riskante Änderungen (DB-Migration, Dateioperationen) definieren Sie ein klares Vorgehen:

  • Vorher: Vollständiges Backup (Datei-Backup, DB-Dump) und Prüfsummen, um Recovery zu ermöglichen.
  • Migrationsskript idempotent schreiben: prüft Vorzustand, führt Änderungen nur einmal aus, schreibt Marker und validiert Ergebnis.
  • Rollback-Skript bereitstellen und testen – automatisches Rollback nur bei klar definierten Fehlschlägen.
Shell
# Beispiel: Migration mit Backup und Marker
if [[ -f "/var/lib/myjob/.migration_v2_done" ]]; then
  log "INFO" "Migration v2 bereits ausgeführt"
  exit 0
fi
# Backup
pg_dump -Fc mydb -f /var/backups/mydb-pre-v2.dump || die 20 "DB Backup fehlgeschlagen"
# Migration
run ./migrate_v2.sh || die 30 "Migration fehlgeschlagen"
mark_done "/var/lib/myjob/.migration_v2_done"

Kurze Checkliste für Produktionsreife

  • Bewusst gesetzte set-Flags; Ausnahmen dokumentiert.
  • Definierte Umgebung: PATH, IFS, Locale.
  • Klare Exit-Codes, Logging auf stderr, strukturierte Logs optional.
  • Locking mit flock oder zentraler Koordination für Multi-Host.
  • Idempotenz: Check-then-Do, atomische Marker, kontrolliertes Cleanup.
  • Docker: ENTRYPOINT mit exec, Healthchecks ohne Seiteneffekte.
  • Tests: ShellCheck, Negativtests, Parallelstart-Tests, Dry-Run.
  • Monitoring: Export von Laufzeiten und Errors als Textfile für Exporter.
  • Runbook: Exit-Code-Definitionen, Log-Locations, Restore- und Rollback-Schritte.

Fazit

Robustheit ist kein Einzeiler, sondern das Ergebnis kleiner, konsistenter Entscheidungen: passende set-Flags, Traps für Diagnose und Cleanup, idempotente Patterns, sinnvolles Locking und klares Logging. Diese Praktiken reduzieren Betriebsrisiken erheblich und liefern reproduzierbare Fehlerbilder für Monitoring, Support und Incident-Handling. Pflegen Sie zusätzlich ein kleines Runbook mit Exit-Code-Definitionen, Log-Locations und Recovery-Schritten – so werden Bash-Skripte verlässliche Bausteine Ihrer Automatisierung.

Appendix: kurzes Incident-Runbook (Template)

Ein knapper Leitfaden, den Sie in Ihr Runbook kopieren können:

  • 1) Log-Exam: /var/log/job.stderr und -stdout prüfen, ERR-Trap-Zeile notieren.
  • 2) Lock prüfen: ls -l /var/lock/, ps -ef | grep <pid>.
  • 3) Environment snapshot: env | sort, declare -p der relevanten Variablen.
  • 4) Try safe rerun: DRY_RUN=1 prüfen, dann mit Backup einen echten Run durchführen.
  • 5) Falls Migration betroffen: Backup wieder einspielen, Marker löschen, Tests lokal durchlaufen.

Robuste Bash-Skripte: Sicherheit, Audit und Deployment im Betrieb

Neben Fehlerbehandlung und Idempotenz sind drei Aspekte entscheidend für den produktiven Einsatz: Geheimnishandling, Versionskontrolle und kontrollierte Ausrollprozesse. Skripte laufen oft mit weitreichender Systemberechtigung; deshalb gilt das Prinzip der geringsten Rechte: Ausführen als dedizierter Service-User, gezielte Capabilities statt Root, und enge Dateiberechtigungen für temporäre Dateien und Marker.

Geheimnisse niemals unmaskiert loggen oder per set -x ausgeben. Nutzt Container- oder Orchestrator-Mechanismen (Docker Secrets, Kubernetes Secrets) oder eine zentrale Vault-Integration. Verifiziert heruntergeladene Artefakte mit Prüfsummen, damit ein kompromittierter Mirror nicht zur Lieferkette wird.

Shell
# Debug nur optional und ohne Secrets
if [[ "${DEBUG:-0}" -eq 1 ]]; then
  set -x
fi
mask_log() { sed 's/(password=)[^[:space:]]+/1***REDACTED***/g'; }
# Beispiel: stdout | mask_log | logger

Versionieren Sie Skripte im Git, bauen Sie CI-Pipelines, die Linting, Unit-Tests und Signierung ausliefern. Verpacken Sie kritische Logik – vor allem bei komplexer Fehlerkorrektur oder hohem Durchsatz – lieber in ein kleines, typgeprüftes Binärwerkzeug (Go/Rust) und nutzen das Script als Orchestrator. Das verbessert Testbarkeit und reduziert Unfallflächen.

Für Deployment und Betrieb: führen Sie Rollouts gestaffelt durch (Canary), überwachen Fehler-Rate und Laufzeit-Metriken und implementieren automatische Quarantäne bei wiederholten Fehlern (z. B. Markierung, Alerting, vorübergehendes Backoff). Dokumentieren Sie jede Änderung in Runbook und Release-Notes, damit Audits nachvollziehbar sind und ein schneller, getesteter Rückrollpfad vorliegt.

Für dieses Thema sind auch Bash Fehlerhandling und Set -Euo Pipefail wichtig. Der Beitrag ordnet diese Aspekte verständlich ein und zeigt, worauf es im Alltag ankommt.

Weiterfuehrend

Passende weitere Inhalte