Riskante Änderungen an Deployment- und Automatisierungsskripten kontrolliert ausrollen
Ein neuer Codepfad in einem produktiv genutzten Bash-Skript ist immer ein Risiko, egal wie sorgfältig er getestet wurde. Ein Feature Flag verwandelt dieses Alles-oder-Nichts-Risiko in eine steuerbare Entscheidung: Der neue Pfad existiert im Skript, ist aber standardmäßig aus, lässt sich gezielt für einzelne Läufe aktivieren und im Ernstfall mit einem einzigen Wert wieder abschalten.
Inhaltsverzeichnis
- 1. Warum Feature Flags auch in Shell-Skripten sinnvoll sind
- 2. Feature Flags über Umgebungsvariablen steuern
- 3. Feature Flags über eine zentrale Konfigurationsdatei steuern
- 4. Bedingte Codepfade sauber strukturieren statt verschachtelte if-Ketten
- 5. Abgestuften Rollout simulieren: nicht alle Server gleichzeitig
- 6. Ein Kill-Switch für den Notfall: sofort zurück zum alten Verhalten
- 7. Aktive Flags loggen: Nachvollziehbarkeit bei Fehlersuche
- 8. Flags wieder entfernen: technische Schulden aktiv abbauen
- 9. Umgebungsvariable vs. Konfigurationsdatei vs. externer Flag-Dienst
- 10. Zusammenfassung
- 11. FAQ
1. Warum Feature Flags auch in Shell-Skripten sinnvoll sind
In Anwendungscode sind Feature Flags längst Standard, um neue Funktionen unabhängig vom Deployment schrittweise auszurollen. In Bash-Skripten, die Deployments, Backups oder Wartungsaufgaben automatisieren, fehlt dieses Muster oft völlig, obwohl das Risiko dort mindestens genauso hoch ist: Ein fehlerhafter neuer Codepfad in einem Deployment-Skript kann eine ganze Produktionsumgebung lahmlegen, nicht nur eine einzelne Funktion in einer Anwendung.
Der Kerngedanke bleibt gleich: Neuer, riskanter Code wird nicht sofort für alle Ausführungen scharf geschaltet, sondern hinter einer Bedingung versteckt, die sich zentral umschalten lässt. Damit lässt sich ein neues Backup-Verfahren zunächst nur auf einem einzelnen Server testen, während alle anderen Server weiterhin den bewährten alten Pfad nutzen, ganz ohne zwei getrennte Skriptversionen pflegen zu müssen.
2. Feature Flags über Umgebungsvariablen steuern
Der einfachste Ansatz nutzt Umgebungsvariablen als Flags: Eine Variable wie FEATURE_NEW_BACKUP_STRATEGY wird auf 1 oder 0 gesetzt, und das Skript prüft diesen Wert an der Stelle, an der sich alter und neuer Codepfad trennen. Dieser Ansatz eignet sich besonders gut für Flags, die pro Ausführung unterschiedlich sein sollen, etwa um ein Flag nur für einen manuell gestarteten Testlauf zu aktivieren, ohne eine Datei anzufassen.
Wichtig ist eine konsistente Namenskonvention, damit Flags im Code sofort als solche erkennbar sind und nicht mit regulären Konfigurationsvariablen verwechselt werden. Ein Präfix wie FEATURE_ macht beim Lesen des Skripts sofort klar, dass diese Variable einen bedingten Codepfad steuert und nicht etwa einen Verbindungsparameter oder einen Pfad.
#!/usr/bin/env bash
set -euo pipefail
# Feature flag via environment variable, default OFF
readonly FEATURE_NEW_BACKUP_STRATEGY="${FEATURE_NEW_BACKUP_STRATEGY:-0}"
run_backup() {
if [[ "$FEATURE_NEW_BACKUP_STRATEGY" == "1" ]]; then
echo "Running NEW incremental backup strategy"
# new, still-risky code path
else
echo "Running established full backup strategy"
# old, proven code path
fi
}
run_backup
# Test run: FEATURE_NEW_BACKUP_STRATEGY=1 ./backup.sh
3. Feature Flags über eine zentrale Konfigurationsdatei steuern
Sobald mehrere Flags gleichzeitig existieren oder ein Flag für mehrere Skriptläufe konstant bleiben soll, wird eine zentrale Konfigurationsdatei praktikabler als einzelne Umgebungsvariablen bei jedem Aufruf. Eine einfache flags.conf mit SCHLUESSEL=WERT-Zeilen, geladen wie eine .env-Datei, macht den aktuellen Rollout-Status aller Flags auf einen Blick sichtbar und versionierbar, wenn die Datei im selben Repository wie das Skript liegt.
Diese Konfigurationsdatei sollte dabei getrennt von der eigentlichen Anwendungs-Konfiguration liegen, damit ein Flag-Wechsel niemals versehentlich andere Einstellungen wie Datenbank-Zugangsdaten mit verändert. Ein eigener Abschnitt oder eine eigene Datei ausschließlich für Flags macht außerdem im Code-Review sofort sichtbar, welche Änderung tatsächlich einen Rollout betrifft.
#!/usr/bin/env bash
set -euo pipefail
# flags.conf:
# FEATURE_NEW_BACKUP_STRATEGY=1
# FEATURE_PARALLEL_UPLOAD=0
load_flags() {
local flags_file="${1:-flags.conf}"
[[ -f "$flags_file" ]] || return 0
while IFS='=' read -r key value; do
[[ -z "$key" || "$key" == \#* ]] && continue
export "$key=$value"
done < "$flags_file"
}
load_flags "flags.conf"
echo "Backup strategy flag: ${FEATURE_NEW_BACKUP_STRATEGY:-0}"
4. Bedingte Codepfade sauber strukturieren statt verschachtelte if-Ketten
Ein einzelnes Feature Flag lässt sich problemlos mit einer if-Abfrage abbilden, aber sobald mehrere Flags dieselbe Funktion beeinflussen, wächst die Verschachtelung schnell zu unübersichtlichen if-in-if-Konstrukten, die schwer zu testen und noch schwerer wieder zu entfernen sind. Sauberer ist es, jeden Codepfad als eigene, klar benannte Funktion zu schreiben und nur an einer einzigen zentralen Stelle zu entscheiden, welche Funktion tatsächlich aufgerufen wird.
Dieses Muster hat einen entscheidenden Nebeneffekt: Wird ein Feature Flag später endgültig zum Standard, muss nur die Auswahl-Stelle angepasst werden, nicht der komplette Funktionskörper, und die alte Funktion lässt sich anschließend gefahrlos löschen, weil sie sauber isoliert ist. Verschachtelter Code dagegen lässt sich selten ohne Risiko wieder auseinanderziehen.
#!/usr/bin/env bash
set -euo pipefail
deploy_legacy() {
echo "Deploying via legacy rsync pipeline"
}
deploy_parallel() {
echo "Deploying via new parallel upload pipeline"
}
# single decision point -- easy to find, easy to remove later
if [[ "${FEATURE_PARALLEL_UPLOAD:-0}" == "1" ]]; then
deploy_parallel
else
deploy_legacy
fi
5. Abgestuften Rollout simulieren: nicht alle Server gleichzeitig
Auch ohne ein echtes Feature-Flag-Backend lässt sich ein abgestufter Rollout in reinem Bash nachbilden, indem das Flag nicht global, sondern pro Host oder pro Ausführung ausgewertet wird. Ein einfacher Ansatz nutzt den Hostnamen oder eine feste Liste erlaubter Zielsysteme, um den neuen Codepfad zunächst nur dort zu aktivieren, während der Rest der Flotte unverändert bleibt.
Für einen echten Prozent-Rollout über viele gleichartige Instanzen hinweg eignet sich ein deterministischer Hash des Hostnamens, der in einen Wertebereich von null bis neunundneunzig abgebildet wird. Liegt dieser Wert unter der gewünschten Rollout-Quote, aktiviert das Skript das neue Verhalten, sonst nicht, konsistent bei jedem Lauf auf demselben Host.
#!/usr/bin/env bash
set -euo pipefail
readonly ROLLOUT_PERCENT=20 # roll out to ~20% of hosts
# deterministic per-host bucket: same host always lands in the same bucket
host_bucket() {
local hash
hash=$(echo -n "$(hostname)" | md5sum | cut -c1-4)
echo $(( 16#$hash % 100 ))
}
if (( $(host_bucket) < ROLLOUT_PERCENT )); then
echo "Host $(hostname) is in the rollout group"
else
echo "Host $(hostname) stays on the stable path"
fi
6. Ein Kill-Switch für den Notfall: sofort zurück zum alten Verhalten
Der wichtigste Zweck eines Feature Flags in einem Automatisierungsskript ist nicht der geordnete Rollout, sondern der Notausstieg: Zeigt sich in Produktion ein Problem, muss das neue Verhalten sofort und ohne Skript-Änderung abschaltbar sein. Ein Kill-Switch, der einfach denselben Flag-Namen auf 0 zurücksetzt, sollte deshalb keine eigene Logik brauchen, sondern schlicht der Normalfall des Ausschaltens sein.
Damit ein Kill-Switch im Ernstfall tatsächlich sofort greift, darf das Flag nicht in irgendeiner Cache-Datei oder einem lang laufenden Prozess-Speicher hängen bleiben, sondern muss bei jedem Skriptaufruf neu aus der aktuellen Konfigurationsdatei oder Umgebungsvariable gelesen werden. Ein Skript, das ein Flag beim Start einmalig einliest und danach für Stunden weiterläuft, verzögert die Wirkung eines Notfall-Abschaltens unnötig.
7. Aktive Flags loggen: Nachvollziehbarkeit bei Fehlersuche
Wenn ein Skript mit aktiviertem Feature Flag ein unerwartetes Ergebnis liefert, ist die erste Frage bei der Fehlersuche fast immer, welche Flags zum Zeitpunkt des Laufs überhaupt aktiv waren. Ohne Logging dieser Information bleibt oft nur die Vermutung, weil sich der Zustand der Konfigurationsdatei zwischen dem fehlerhaften Lauf und der Untersuchung längst geändert haben kann.
Ein robustes Skript protokolliert deshalb zu Beginn jedes Laufs, mit welchen Flag-Werten es gestartet ist, idealerweise in derselben Logdatei wie der Rest der Ausgabe. Diese eine Zeile am Anfang des Logs macht spätere Fehlersuche erheblich schneller, weil sich die Ursache eines Problems sofort auf einen konkreten Flag-Zustand zurückführen lässt, statt ihn nachträglich rekonstruieren zu müssen.
#!/usr/bin/env bash
set -euo pipefail
log_active_flags() {
local flag
for flag in "${!FEATURE_@}"; do
echo "[FLAGS] $flag=${!flag}"
done
}
log_active_flags
# [FLAGS] FEATURE_NEW_BACKUP_STRATEGY=1
# [FLAGS] FEATURE_PARALLEL_UPLOAD=0
8. Flags wieder entfernen: technische Schulden aktiv abbauen
Ein Feature Flag, das ein Jahr nach dem vollständigen Rollout noch im Code steht, ist reine technische Schuld: Es zwingt jeden, der die Funktion liest, den inzwischen toten alten Codepfad mitzudenken, obwohl er nie mehr ausgeführt wird. Sobald ein neues Verhalten sich in Produktion bewährt hat und dauerhaft auf hundert Prozent steht, gehört der alte Pfad entfernt und das Flag selbst gelöscht.
Praktisch bewährt sich, jedes neu eingeführte Flag mit einem Ablaufdatum oder einem Ticket zu versehen, das explizit das Aufräumen einfordert, statt sich auf spätere Erinnerung zu verlassen. Ohne diese Disziplin sammeln sich in langlebigen Deployment-Skripten über die Jahre Dutzende toter Flags an, die niemand mehr zu entfernen wagt, weil unklar ist, ob sie noch irgendwo gebraucht werden.
9. Umgebungsvariable vs. Konfigurationsdatei vs. externer Flag-Dienst
Für ein einzelnes Skript oder wenige Flags reichen Umgebungsvariablen oder eine einfache Konfigurationsdatei völlig aus und lassen sich ohne zusätzliche Infrastruktur betreiben. Erst wenn viele Skripte auf vielen Servern dieselben Flags konsistent und in Echtzeit sehen müssen, lohnt sich der Blick auf einen externen Flag-Dienst, der zentrale Änderungen sofort an alle Ausführungsorte verteilt.
| Mechanismus | Reichweite | Änderung wirkt | Typischer Einsatz |
|---|---|---|---|
| Umgebungsvariable | Ein Skriptaufruf | Sofort beim nächsten Aufruf | Einmaliger Testlauf, manuelle Aktivierung |
| Konfigurationsdatei | Alle Läufe auf einem Host | Beim nächsten Laden der Datei | Dauerhafter Rollout-Status pro Server |
| Hostnamen-Bucket | Prozentualer Anteil der Flotte | Deterministisch, konsistent pro Host | Schrittweiser Rollout über viele Server |
| Externer Flag-Dienst | Alle Server in Echtzeit | Sofort, ohne Deployment | Große Flotten, zentrale Steuerung |
Mironsoft
Shell-Automatisierung, DevOps-Tooling und Deployment-Infrastruktur
Shell-Skripte, die in der Produktion zuverlässig laufen?
Wir analysieren bestehende Bash-Skripte, erkennen fragile Muster und ersetzen sie durch robuste Bash-Patterns: mit vollständiger Fehlerbehandlung, Logging und sicherer Parallelisierung für euren Deployment-Stack.
Code-Review
ShellCheck-Analyse und manuelle Prüfung auf kritische Bash-Pattern-Verstöße.
Refactoring
Fehlerbehandlung, Logging und sichere Dateioperationen nachrüsten.
CI-Integration
ShellCheck und BATS in Pipelines integrieren und Regressionstests aufbauen.
10. Zusammenfassung
Feature Flags in Bash-Skripten: Das Wichtigste auf einen Blick
Flags klar benennen
Ein Präfix wie FEATURE_ macht sofort sichtbar, dass eine Variable einen bedingten Codepfad steuert und keine reguläre Konfiguration ist.
Ein zentraler Entscheidungspunkt
Codepfade als eigene Funktionen schreiben und nur an einer Stelle auswählen, statt if-in-if-Verschachtelung über das ganze Skript zu verteilen.
Kill-Switch ohne Verzögerung
Flags bei jedem Lauf neu einlesen, nicht einmalig cachen, damit ein Notfall-Abschalten sofort wirkt statt erst beim nächsten Neustart.
Flags aktiv aufräumen
Jedes Flag mit einem Ablaufdatum oder Ticket versehen und nach vollständigem Rollout inklusive altem Codepfad wieder entfernen.