IT-Admin.tech

Scripts Bash robustes : gestion des erreurs, options « set » et patrons idempotents

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.

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.

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

Explication : 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.

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 : 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.

Shell
: "${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.

Shell
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.

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

Patrons 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

Shell
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).

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

Les 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).

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"

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.

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

En 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.

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

Remarque : 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 -d et 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 umask ou corriger explicitement les permissions cibles avec chmod.
Shell
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).

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"

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.

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"

Dépannage : scénarios d’erreur typiques et séquence de vérification

Si un job échoue, travaillez de manière structurée :

  1. 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.
  2. Contrôlez l’environnement : PATH, IFS, variables avec declare -p.
  3. Vérifiez le statut des verrous : le fichier de verrou existe-t-il, qui le détient ? (ps aux | grep)
  4. 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.
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

  • 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 -p des 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.

Shell
# 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.