Scripts Bash robustes sont une caractéristique opérationnelle : dans Cron, les timers systemd ou les points d’entrée Docker, les scripts doivent traiter les erreurs de façon explicite, nettoyer proprement et pouvoir être réexécutés sans provoquer d’effets de bord. Cet article fournit un corpus compact et pragmatique de set-Flags (options qui modifient le comportement du shell), de Traps (fonctions exécutées lors de signaux ou d’erreurs) et de patterns idempotents (répétabilité sans effets secondaires modifiants).
Pourquoi des scripts simples échouent en production
Les erreurs proviennent rarement d’une logique complexe – il s’agit le plus souvent de mauvaises hypothèses sur l’environnement, les codes de retour ou la concurrence. Causes fréquentes :
- Les erreurs restent invisibles ou sont masquées (pipelines, grep sans correspondance).
- Environnement inattendu dans cron/conteneurs (PATH minimal, IFS, outils manquants).
- Des tâches s’exécutant en parallèle écrivent dans les mêmes ressources.
- Nettoyage incomplet après interruption (fichiers temporaires, points de montage, verrous).
L’objectif est une robustesse praticable : codes de sortie univoques, journaux traçables, étapes idempotentes et concurrence contrôlée.
Structure de base robuste pour les scripts Bash
Un socle cohérent réduit les pièges. Adaptez-le à vos besoins, mais conservez les éléments centraux : set-Flags, environnement défini, journalisation, Traps et nettoyage.
#!/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 EXITExplication : set -e termine en cas d’erreurs non interceptées, -u signale l’utilisation de variables non définies, pipefail veille à ce que les pipelines remontent les erreurs, et -E permet aux ERR-Traps d’agir dans les fonctions. IFS réduit les séparations de mots involontaires, un PATH défini évite des versions d’outils différentes dans cron/conteneur.
set-Flags : utilité, risques, pratique
set -e : utile avec des exceptions explicites
set -e est utile lorsque continuer après une erreur serait dangereux. Des problèmes surviennent quand des outils courants utilisent le code de sortie 1 pour „aucun résultat“ (p. ex. grep). Résolvez cela par des vérifications explicites.
# 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"
fiset -u : protège contre les fautes de frappe
set -u empêche les erreurs silencieuses dues à l’utilisation de variables non définies, mais exige des valeurs par défaut ou des messages d’erreur explicites.
: "${BACKUP_DIR:?BACKUP_DIR ist nicht gesetzt}"
RETENTION_DAYS="${RETENTION_DAYS:-14}"pipefail et -E : améliorer le diagnostic
pipefail rend les pipelines plus fiables ; -E et un trap ERR affichent la ligne et la commande, ce qui rend les journaux cron beaucoup plus explicites.
Gestion des erreurs : codes de sortie, Traps et chemins de repli
Standardiser les codes de sortie
Les codes de sortie sont l’interface la plus simple pour le monitoring et l’orchestration. Fixez des règles d’équipe (p. ex. 2 = usage/paramètres, 10+ = dépendances externes) et documentez-les dans le runbook.
usage() { cat <<'EOF'
Usage: job.sh --source DIR --target DIR
EOF
}
# Beispiel: Parameterprüfung
[[ -n "$SOURCE" ]] || die 2 "--source fehlt"Nettoyage avec marqueurs d’état
Le trap EXIT s’exécute à chaque sortie. Pour des ressources complexes, utilisez des variables d’état simples afin que le nettoyage soit idempotent.
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=1Patrons idempotents : répétables et sûrs
L’idempotence signifie : l’exécution répétée aboutit au même état cible. Cela est central pour les déploiements, les migrations ou les scripts d’initialisation dans des conteneurs.
Check-then-Do avec vérifications fiables
ensure_dir() {
local dir="$1" mode="$2"
[[ -d "$dir" ]] || mkdir -p -- "$dir"
chmod "$mode" -- "$dir"
}
ensure_dir "/var/lib/myjob" "0750"Ce qui compte est ce que vous vérifiez : l’existence seule est rarement suffisante ; contrôlez le contenu, les permissions ou les réponses des services, si nécessaire.
Fichiers marqueurs et écritures atomiques
Les fichiers marqueurs sont pratiques, mais fiables uniquement avec des écritures atomiques (tmp + mv).
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"
fiLes marqueurs ne remplacent pas la validation : vérifiez en complément que le succès a réellement été atteint (service répond, objet en base de données existe).
Verrouillage pour éviter les démarrages parallèles
flock est fiable sur Linux : les verrous du noyau se libèrent à la fin du processus. Pour les systèmes de fichiers distribués ou la coordination multi-hôte, il faut des coordonnateurs externes (DB, Redis, Consul).
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"Scripts Bash robustes : gestion des signaux et PID 1 dans le conteneur
Dans les conteneurs, la shell d’ENTRYPOINT assume souvent PID 1. PID 1 a des responsabilités spécifiques : il doit transmettre correctement les signaux et récupérer les processus enfants (nettoyage des zombies). Si la shell n’est pas remplacée via exec, elle reste PID 1 et peut empêcher la propagation des signaux — les arrêts se prolongent ou des orchestrateurs comme Kubernetes reçoivent des codes de sortie erronés. Solution : utiliser tini ou exécuter directement avec exec.
Transmission des signaux et récupération des processus enfants
Les traps pour SIGTERM/SIGINT transmettent les signaux, arrêtent de manière contrôlée les processus en arrière-plan et attendent la fin des enfants avec wait.
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 || trueEn alternative : utilisez dans le Dockerfile ENTRYPOINT ["/sbin/tini", "--"] ou lancez les conteneurs avec --init, afin qu’un petit reaper PID 1 prenne en charge la tâche.
Verrous distribués : quand flock ne suffit pas
Pour un hôte unique, flock est souvent suffisant. Pour des systèmes distribués (NFS, plusieurs hôtes), une coordination centrale ou des verrous au niveau de la base de données sont le bon choix. Exemples :
Postgres Advisory Lock
Postgres propose des Advisory Locks (verrou géré par l’application) via des appels SQL simples. Cela est utile si vous avez déjà une base de données relationnelle dans la stack.
# 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)" || trueRemarque : les Advisory Locks sont attachés à la session DB ; en cas de rupture de connexion, ils sont relâchés — ce comportement est souvent souhaité.
Redis SETNX avec TTL
Redis peut implémenter des schémas simples de leader ou de verrou avec SET resource value NX PX. Attention : les partitions réseau peuvent entraîner des verrous obsolètes — définir un TTL et vérifier le propriétaire du verrou.
Opérations de fichiers sécurisées, permissions et répertoires temporaires
Les erreurs lors d’opérations sur les fichiers entraînent souvent des problèmes de sécurité. Bonnes pratiques :
- Créer des répertoires temporaires avec
mktemp -det définir des permissions sécurisées (umask/ chmod). - Écritures atomiques : écrire dans un fichier temporaire puis remplacer avec
mv. - Gestion des permissions : définir
umaskou corriger explicitement les permissions cibles avecchmod.
old_umask=$(umask)
umask 027
TMPDIR=$(mktemp -d -p /var/tmp myjob.XXXXXX)
chmod 0700 "$TMPDIR"
# ... arbeiten ...
umask "$old_umask"
Intégration pragmatique du monitoring, des métriques et des logs
Les codes de sortie sont le principal moyen de signalisation ; en complément, le logging structuré et l’export de métriques fournissent des avantages concrets pour le SRE/monitoring (p. ex. Prometheus Node Exporter Textfile Collector).
# 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"
Point central : n’écrivez que dans le répertoire prévu, utilisez des déplacements atomiques pour que les exporters voient des fichiers cohérents.
Tests, intégration CI et analyse statique
Ne testez pas les scripts uniquement manuellement : ShellCheck identifie les problèmes de style et de sécurité, et les tests unitaires pour les fonctions shell (p. ex. avec bats-core) détectent les erreurs logiques. Les tests d’intégration simulent des dépendances manquantes et des démarrages parallèles.
# 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"
Dépannage : scénarios d’erreur typiques et séquence de vérification
Si un job échoue, travaillez de manière structurée :
- Vérifiez les logs (stderr/stdout) pour la sortie de l’ERR-Trap. Recherchez le numéro de ligne et le BASH_COMMAND fournis par l’ERR-Trap.
- Contrôlez l’environnement : PATH, IFS, variables avec
declare -p. - Vérifiez le statut des verrous : le fichier de verrou existe-t-il, qui le détient ? (ps aux | grep)
- Dans un conteneur : PID 1 était-il le shell ? Vérifiez l’arbre des processus (proc-tree) et la gestion des signaux.
Stratégie de rollback et de récupération
Pour les modifications risquées (migration de la base de données, opérations sur fichiers), définissez une procédure claire :
- Avant : sauvegarde complète (sauvegarde des fichiers, dump de la base) et sommes de contrôle, pour permettre la RESTauration.
- Écrire le script de migration de façon idempotente : vérifier l’état préalable, n’appliquer les changements qu’une seule fois, écrire un marqueur et valider le résultat.
- Fournir et tester un script de rollback — rollback automatique uniquement en cas d’échecs clairement définis.
# 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
- set-Flags bewusst gesetzt; Ausnahmen dokumentiert.
- Définition claire de l’environnement : PATH, IFS, Locale.
- Codes de sortie définis, journalisation sur stderr, logs structurés en option.
- Verrouillage avec flock ou coordination centrale pour environnements multi-hôtes.
- Idempotence : vérification avant action (check-then-do), marqueurs atomiques, nettoyage contrôlé.
- Docker : ENTRYPOINT avec exec, Healthchecks sans effets de bord.
- Tests : ShellCheck, tests négatifs, tests de démarrage parallèle, Dry-Run.
- Monitoring : export des durées d’exécution et des erreurs sous forme de fichier texte pour un exporter.
- Runbook : définitions des codes de sortie, emplacements des logs, étapes de RESTore et de rollback.
Fazit
La robustesse n’est pas une ligne de code, mais le résultat de petites décisions cohérentes : options set appropriées, traps pour le diagnostic et le nettoyage, patterns idempotents, verrouillage sensé et journalisation claire. Ces pratiques réduisent significativement les risques opérationnels et fournissent des traces d’erreur reproductibles pour le monitoring, le support et la gestion des incidents. Maintenez en complément un petit runbook avec les définitions des codes de sortie, les emplacements des logs et les étapes de récupération — ainsi les scripts Bash deviennent des composants fiables de votre automatisation.
Appendix: kurzes Incident-Runbook (Template)
Un guide succinct que vous pouvez copier dans votre runbook :
- 1) Examen des logs : vérifier /var/log/job.stderr et -stdout, noter la ligne ERR-Trap.
- 2) Vérifier le verrou :
ls -l /var/lock/,ps -ef | grep <pid>. - 3) Instantané de l’environnement :
env | sort,declare -pdes variables pertinentes. - 4) Tenter un relancement sûr : vérifier DRY_RUN=1, puis effectuer une exécution réelle avec une sauvegarde.
- 5) En cas de migration affectée : réappliquer la sauvegarde, supprimer les marqueurs, exécuter les tests localement.
Robuste Bash-Skripte: Sicherheit, Audit und Deployment im Betrieb
Outre la gestion des erreurs et l’idempotence, trois aspects sont déterminants pour l’exploitation en production : gestion des secrets, contrôle de version et processus d’arrêt/déploiement contrôlé. Les scripts s’exécutent souvent avec des privilèges étendus ; d’où le principe du moindre privilège : exécution en tant qu’utilisateur de service dédié, capacités ciblées plutôt que Root, et permissions strictes pour les fichiers temporaires et les marqueurs.
Ne consignez jamais les secrets en clair ni ne les affichez via set -x. Utilisez les mécanismes du conteneur ou de l’orchestrateur (Docker Secrets, Kubernetes Secrets) ou une intégration centralisée avec Vault. Vérifiez les artefacts téléchargés avec des sommes de contrôle afin qu’un miroir compromis ne mette pas en danger la chaîne d’approvisionnement.
# Debug uniquement optionnel et sans secrets
if [[ "${DEBUG:-0}" -eq 1 ]]; then
set -x
fi
mask_log() { sed 's/(password=)[^[:space:]]+/1***REDACTED***/g'; }
# Exemple : stdout | mask_log | logger
Versionnez les Skripte dans Git, construisez des pipelines CI qui fournissent linting, tests unitaires et signature. Implémentez la logique critique – surtout pour des corrections d’erreurs complexes ou un débit élevé – dans un petit binaire fortement typé (Go/Rust) et utilisez le Script comme orchestrateur. Cela améliore la testabilité et réduit la surface d’incidents.
Für Deployment und Betrieb: effectuez des déploiements échelonnés (Canary), surveillez le taux d’erreur et les métriques d’exécution et implémentez une quarantaine automatique en cas d’erreurs répétées (z. B. Markierung, Alerting, vorübergehendes Backoff). Documentez chaque modification dans Runbook und Release-Notes, afin que les audits soient traçables et qu’une procédure de rollback rapide et testée existe.
Für dieses Thema sind auch Bash Fehlerhandling und Set -Euo Pipefail wichtig. L’article situe ces aspects de manière compréhensible et montre ce qui compte au quotidien.