IT-Admin.tech

Script Bash robusti: gestione degli errori, flag di set e pattern idempotenti

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.

Script Bash robusti sono una caratteristica operativa: in Cron, nei timer systemd o negli entrypoint di Docker gli script devono gestire gli errori in modo esplicito, pulire correttamente e poter essere rieseguiti senza generare effetti collaterali. Questo articolo fornisce una raccolta compatta e pratica di set-Flags (opzioni che modificano il comportamento della shell), Traps (funzioni eseguite su segnali o errori) e pattern idempotenti (ripetibilità senza effetti collaterali modificanti).

Perché script semplici falliscono in produzione

Gli errori raramente derivano da logica complessa – più spesso sono assunzioni errate sull’ambiente, sui valori di ritorno o sulla concorrenza. Cause comuni:

  • Gli errori non sono visibili o vengono mascherati (pipeline, grep senza risultati).
  • Ambiente inatteso in Cron/container (PATH minimo, IFS, strumenti mancanti).
  • Job eseguiti in parallelo scrivono sulle stesse risorse.
  • Pulizia incompleta dopo un’interruzione (file temporanei, mount, lock).

L’obiettivo è robustezza pratica: codici di exit univoci, log tracciabili, passi idempotenti e concorrenza controllata.

Struttura di base robusta per script Bash

Una struttura coerente riduce le trappole. Adattatela alle vostre esigenze, ma mantenete i componenti centrali: set-Flags, ambiente definito, logging, Traps e 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" "Errore rc=${rc} nella riga ${1:-?}: ${2:-?}"; exit "$rc"; }
cleanup() { :; }

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

Spiegazione: set -e termina in caso di errori non gestiti, -u avvisa per variabili unset, pipefail fa sì che le pipeline riportino errori, e -E permette che le ERR-Traps siano eseguite nelle funzioni. IFS riduce la separazione accidentale delle parole, un PATH definito evita versioni diverse degli strumenti in Cron/container.

set-Flags: benefici, rischi, pratica

set -e: utile con eccezioni esplicite

set -e è utile quando un errore rende pericoloso proseguire. Problemi sorgono quando tool tipici usano il codice di exit 1 per „nessun risultato“ (ad es. grep). Risolvete questo con verifiche esplicite.

Shell
# Rimozione opzionale, errore tollerato
rm -f -- "/var/tmp/maybe-there" || true

# Gestire grep senza corrispondenze in modo esplicito
if grep -q "pattern" file; then
  echo "trovato"
else
  echo "non trovato"
fi

set -u: protegge da errori di battitura

set -u impedisce errori silenziosi nell’uso di variabili vuote, ma richiede valori di default o messaggi di errore chiari.

Shell
: "${BACKUP_DIR:?BACKUP_DIR non è impostato}"
RETENTION_DAYS="${RETENTION_DAYS:-14}"

pipefail e -E: migliorano la diagnostica

pipefail rende le pipeline più affidabili; -E e una ERR-Trap forniscono riga e comando, rendendo i log di Cron molto più informativi.

Gestione degli errori: codici di exit, Traps e percorsi di fallback

Standardizzare i codici di exit

I codici di exit sono l’interfaccia più semplice per monitoring e orchestrazione. Stabilite regole di team (ad es. 2 = uso/parametri, 10+ = dipendenze esterne) e documentatele nel Runbook.

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

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

Cleanup con marcatori di stato

Il trap EXIT viene eseguito a ogni exit. Per risorse complesse usi variabili di stato semplici, in modo che il Cleanup sia idempotente.

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

Pattern idempotenti: ripetibili e sicuri

Idempotenza significa: esecuzioni ripetute producono lo stesso stato finale. Questo è centrale per deployment, migrazioni o init-skript nei container.

Check-then-Do con verifiche affidabili

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

Importante è cosa verificare: l’esistenza da sola raramente basta; verifichi contenuto, permessi o risposte del servizio, se necessario.

File marker e scritture atomiche

I file marker sono pratici, ma affidabili solo con scrittura atomica (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

I marker non sostituiscono la validazione: verifichi inoltre che il successo sia effettivamente raggiunto (il servizio risponde, l’oggetto in DB esiste).

Locking per evitare avvii paralleli

flock è robusto su Linux: i lock del kernel si rilasciano alla terminazione del processo. Per filesystem distribuiti o coordinazione multi-host servono coordinator esterni (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"

Script Bash robusti: gestione dei segnali e PID 1 nel container

Nei container la shell ENTRYPOINT spesso assume PID 1. PID 1 ha responsabilità particolari: deve inoltrare i segnali correttamente e gestire i processi figli terminati (ripulire gli zombie). Se la shell non viene sostituita con exec, rimane PID 1 e può impedire l’inoltro dei segnali — gli shutdown si ritardano o orchestratori come Kubernetes ricevono codici di uscita errati. Soluzione: usare tini o eseguire direttamente exec.

Inoltro dei segnali e gestione dei figli terminati

I trap per SIGTERM/SIGINT inoltrano i segnali, fermano i processi in background in modo controllato e attendono con wait la terminazione dei figli.

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

In alternativa: usi nel Dockerfile ENTRYPOINT ["/sbin/tini", "--"] o avvii i container con --init, così un piccolo PID-1-reaper assume il compito.

Lock distribuiti: quando flock non basta

Per host singolo, flock è spesso sufficiente. In sistemi distribuiti (NFS, più host) la scelta corretta è la coordinazione centrale o i lock a livello di database. Esempi:

Advisory Lock di Postgres

Postgres offre Advisory Locks (protetti dall’applicazione) tramite semplici chiamate SQL. Questo è utile se è già presente un DB relazionale nello 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

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

Redis SETNX con TTL

Redis può implementare semplici pattern di leader o lock con SET resource value NX PX. Attenzione: partizioni di rete possono portare a lock obsoleti — impostare TTL e verificare il proprietario del lock.

Operazioni sui file sicure, permessi e directory temporanee

Errori nelle operazioni sui file spesso causano problemi di sicurezza. Buone pratiche:

  • Creare directory temporanee con mktemp -d e impostare permessi sicuri (umask/ chmod).
  • Scritture atomiche: scrivere in un file temporaneo e sostituire tramite mv.
  • Gestione dei permessi: impostare umask o correggere esplicitamente i permessi di destinazione con chmod.
Shell
old_umask=$(umask)
umask 027
TMPDIR=$(mktemp -d -p /var/tmp myjob.XXXXXX)
chmod 0700 "$TMPDIR"
# ... arbeiten ...
umask "$old_umask"

Monitoraggio, metriche e log: integrazione pratica

I codici di uscita sono il principale mezzo di segnalazione; in aggiunta, il logging strutturato e l’export delle metriche offrono vantaggi concreti per SRE/monitoring (ad es. 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"

Fondamentale: scrivere solo nella directory prevista, usare move atomici affinché gli exporter vedano file consistenti.

Test, integrazione CI e analisi statica

Non testate gli script solo manualmente: ShellCheck individua errori di stile e di sicurezza, e unit test per funzioni shell (es. con bats-core) catturano errori logici. I test di integrazione simulano dipendenze mancanti e avvii paralleli.

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: scenari d’errore tipici e sequenza di verifica

Quando un job fallisce, procedere in modo strutturato:

  1. Controllare i log (stderr/stdout) per l’output della ERR-Trap. Cercare il numero di riga e il BASH_COMMAND forniti dalla ERR-Trap.
  2. Verificare l’ambiente: PATH, IFS, variabili con declare -p.
  3. Controllare lo stato del lock: esiste il lockfile, chi lo detiene? (ps aux | grep)
  4. Nel container: PID 1 era la shell? Controllare l’albero dei processi (proc-tree) e la gestione dei segnali.

Strategia di rollback e ripristino

Per modifiche a rischio (migrazione DB, operazioni su file) definite una procedura chiara:

  • Prima: backup completo (backup dei file, DB-Dump) e checksum, per consentire il recovery.
  • Scrivere lo script di migrazione idempotente: verifica lo stato iniziale, applica le modifiche una sola volta, scrive un marker e valida il risultato.
  • Fornire e testare uno script di rollback – rollback automatico solo per errori chiaramente definiti.
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"

Breve checklist per la prontezza alla produzione

  • Opzioni set impostate in modo consapevole; eccezioni documentate.
  • Ambiente definito: PATH, IFS, Locale.
  • Codici di uscita chiari, logging su stderr, log strutturati opzionali.
  • Locking con flock o coordinamento centrale per scenari multi-host.
  • Idempotenza: check-then-do, marker atomici, cleanup controllato.
  • Docker: ENTRYPOINT con exec, healthchecks senza effetti collaterali.
  • Test: ShellCheck, test negativi, test di avvio parallelo, dry-run.
  • Monitoring: esportazione di durate ed errori come file di testo per gli exporter.
  • Runbook: definizioni dei codici di uscita, posizioni dei log, passi di RESTore e rollback.

Conclusione

La robustezza non è una riga di codice, ma il risultato di piccole decisioni coerenti: opzioni set adeguate, trap per diagnostica e cleanup, pattern idempotenti, locking sensato e logging chiaro. Queste pratiche riducono significativamente i rischi operativi e forniscono scenari di errore riproducibili per monitoring, support e gestione degli incidenti. Mantenete inoltre un piccolo runbook con definizioni dei codici di uscita, posizioni dei log e passi di recovery – così gli script Bash diventano elementi affidabili della vostra automazione.

Appendice: breve runbook per incidenti (modello)

Una guida breve che potete copiare nel vostro runbook:

  • 1) Esame dei log: controllare /var/log/job.stderr e -stdout, annotare la riga dell’ERR-Trap.
  • 2) Verificare il lock: ls -l /var/lock/, ps -ef | grep <pid>.
  • 3) Snapshot dell’ambiente: env | sort, declare -p delle variabili rilevanti.
  • 4) Provate una riesecuzione sicura: verificare DRY_RUN=1, quindi eseguire un’esecuzione reale con backup.
  • 5) Se la migrazione è coinvolta: ripristinare il backup, cancellare i marker, eseguire i test in locale.

Script Bash robusti: sicurezza, audit e deployment in produzione

Oltre alla gestione degli errori e all’idempotenza, tre aspetti sono decisivi per l’uso produttivo: gestione dei segreti, controllo delle versioni e processi di rollout controllati. Gli script spesso girano con ampi privilegi di sistema; perciò vale il principio del minimo privilegio: esecuzione come service user dedicato, capability mirate invece di root, e permessi stretti sui file per file temporanei e marker.

Non registrate mai i segreti in chiaro né esponeteli con set -x. Usate i meccanismi del container o dell’orchestrator (Docker Secrets, Kubernetes Secrets) o un’integrazione centralizzata con Vault. Verificate gli artefatti scaricati con checksum, in modo che un mirror compromesso non comprometta la catena di fornitura.

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

Versionate gli script in Git, costruite CI-Pipelines che eseguono linting, test unitari e firma. Incapsulate la logica critica — soprattutto per correzioni di errori complesse o per elevato throughput — in un piccolo strumento binario fortemente tipizzato (Go/Rust) e usate lo script come orchestratore. Questo migliora la testabilità e riduce la superficie di errore.

Per il deployment e l’operatività: eseguite i rollout in modo graduato (Canary), monitorate il tasso di errori e le metriche di runtime e implementate una quarantena automatica in caso di errori ripetuti (ad es. marcatura, alerting, backoff temporaneo). Documentate ogni modifica nel runbook e nelle note di rilascio, in modo che gli audit siano tracciabili e sia disponibile una procedura di rollback rapida e testata.

Per questo ambito sono inoltre importanti la gestione degli errori in Bash e l’uso di Set -Euo Pipefail. Il contributo contestualizza questi aspetti in modo comprensibile e mostra cosa conta nella pratica quotidiana.