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.
#!/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 EXITSpiegazione: 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.
# 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"
fiset -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.
: "${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.
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.
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=1Pattern 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
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).
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"
fiI 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).
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.
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 || trueIn 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.
# 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)" || trueHinweis: 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 -de impostare permessi sicuri (umask/ chmod). - Scritture atomiche: scrivere in un file temporaneo e sostituire tramite
mv. - Gestione dei permessi: impostare
umasko correggere esplicitamente i permessi di destinazione conchmod.
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).
# 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.
# 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:
- Controllare i log (stderr/stdout) per l’output della ERR-Trap. Cercare il numero di riga e il BASH_COMMAND forniti dalla ERR-Trap.
- Verificare l’ambiente: PATH, IFS, variabili con
declare -p. - Controllare lo stato del lock: esiste il lockfile, chi lo detiene? (ps aux | grep)
- 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.
# 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 -pdelle 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.
# 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.