Scripts Bash robustos son una característica operativa: en Cron, timers de systemd o Docker-Entrypoints los scripts deben manejar errores de forma clara, limpiar correctamente y poder ejecutarse repetidamente sin provocar efectos secundarios. Esta entrada ofrece un conjunto compacto y práctico de patrones con set-Flags (opciones que cambian el comportamiento del shell), Traps (funciones que se ejecutan ante señales o errores) y patrones idempotentes (repetibilidad sin efectos colaterales modificadores).
Por qué los scripts simples fallan en producción
Los errores rara vez provienen de lógica compleja: normalmente son suposiciones incorrectas sobre el entorno, los códigos de retorno o la concurrencia. Causas frecuentes:
- Los errores no son visibles o quedan enmascarados (pipelines, grep sin coincidencias).
- Entorno inesperado en Cron/contenedores (PATH mínimo, IFS, herramientas ausentes).
- Tareas que se ejecutan en paralelo escriben en los mismos recursos.
- Limpieza incompleta tras una interrupción (archivos temporales, montajes, bloqueos).
El objetivo es robustez práctica: códigos de salida inequívocos, logs rastreables, pasos idempotentes y concurrencia controlada.
Estructura base robusta para scripts Bash
Un esqueleto consistente reduce trampas. Adáptelo a sus requisitos, pero mantenga los elementos centrales: set-Flags, entorno definido, logging, Traps y limpieza.
#!/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 EXITExplicación: set -e termina ante errores no capturados, -u advierte sobre variables unset, pipefail hace que las pipelines informen errores y -E permite traps ERR dentro de funciones. IFS reduce divisiones de palabra no deseadas; un PATH definido evita versiones distintas de herramientas en Cron/contenedor.
set-Flags: beneficios, riesgos, práctica
set -e: útil con excepciones explícitas
set -e es útil cuando un error hace peligroso continuar. Surgen problemas cuando herramientas habituales usan el código de salida 1 para “sin resultado” (p. ej. grep). Resuelva esto con comprobaciones explícitas.
# 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: protege contra errores por variables no definidas
set -u evita errores silenciosos al usar variables vacías, pero exige valores por defecto o mensajes de error claros.
: "${BACKUP_DIR:?BACKUP_DIR ist nicht gesetzt}"
RETENTION_DAYS="${RETENTION_DAYS:-14}"pipefail y -E: mejorar diagnósticos
pipefail hace las pipelines más fiables; -E y una ERR-Trap proporcionan línea y comando, lo que vuelve los registros de Cron mucho más informativos.
Manejo de errores: códigos de salida, Traps y rutas de recuperación
Estandarizar códigos de salida
Los códigos de salida son la interfaz más sencilla con la monitorización y la orquestación. Defina reglas de equipo (p. ej. 2 = uso/parámetros, 10+ = dependencias externas) y documentelas en el Runbook.
usage() { cat <<'EOF'
Usage: job.sh --source DIR --target DIR
EOF
}
# Beispiel: Parameterprüfung
[[ -n "$SOURCE" ]] || die 2 "--source fehlt"Limpieza con marcas de estado
El trap de EXIT se ejecuta en cada salida. Para recursos complejos, utilice variables de estado simples para que la limpieza sea 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=1Patrones idempotentes: repetibles y seguros
Idempotencia significa: ejecutar varias veces produce el mismo estado objetivo. Esto es fundamental en despliegues, migraciones o scripts de inicialización en contenedores.
Check-then-Do con comprobaciones robustas
ensure_dir() {
local dir="$1" mode="$2"
[[ -d "$dir" ]] || mkdir -p -- "$dir"
chmod "$mode" -- "$dir"
}
ensure_dir "/var/lib/myjob" "0750"Lo importante es qué verifica: la mera existencia rara vez basta; compruebe contenido, permisos o respuestas del servicio, si es necesario.
Archivos marcador y escrituras atómicas
Los archivos marcador son prácticos, pero sólo son fiables con escrituras atómicas (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"
fiLos marcadores no sustituyen la validación: compruebe además que el éxito se ha alcanzado realmente (el servicio responde, el objeto en la base de datos existe).
Bloqueo frente a arranques paralelos
flock ist unter Linux robust: Kernel-Locks lösen sich beim Prozessende. Für verteilte Dateisysteme oder Multi-Host-Koordination braucht es externe Koordinatoren (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 robustos: manejo de señales y PID 1 en contenedores
En contenedores, la shell ENTRYPOINT suele asumir el PID 1. PID 1 tiene una responsabilidad especial: debe reenviar señales correctamente y recoger procesos hijos (evitar procesos zombis). Si la shell no se reemplaza mediante exec, permanece como PID 1 y puede impedir el reenvío de señales—los apagados se retrasan o el orquestador, como Kubernetes, recibe códigos de salida incorrectos. Recomendación: usar tini o ejecutar directamente con exec.
Reenvío de señales y recolección de procesos hijos
Los traps para SIGTERM/SIGINT reenvían las señales, detienen los procesos en segundo plano de forma controlada y esperan con wait a que terminen los hijos.
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 || trueAlternativamente: use en el Dockerfile ENTRYPOINT ["/sbin/tini", "--"] o inicie el contenedor con --init, para que un pequeño PID-1-Reaper asuma la tarea.
Bloqueos distribuidos: cuando flock no es suficiente
Para un único host, flock suele ser suficiente. En sistemas distribuidos (NFS, varios hosts) la coordinación centralizada o los bloqueos a nivel de base de datos son la elección correcta. Ejemplos:
Advisory Lock de Postgres
Postgres ofrece Advisory Locks (gestionados por la aplicación) mediante sencillas llamadas SQL. Eso es útil si ya tiene una base de datos relacional en el 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)" || trueNota: los Advisory Locks están ligados a la sesión de la base de datos; en caso de corte de conexión se liberan – lo cual suele ser deseable.
Redis SETNX con TTL
Redis puede implementar patrones sencillos de leader o lock con SET resource value NX PX. Atención: las particiones de red pueden provocar locks obsoletos—establezca TTL y verifique el propietario del lock.
Operaciones de archivos seguras, permisos y directorios temporales
Los errores en operaciones de archivos suelen derivar en problemas de seguridad. Buenas prácticas:
- Crear directorios temporales con
mktemp -dy establecer permisos seguros (umask/ chmod). - Escrituras atómicas: escribir en un archivo temporal y reemplazarlo con
mv. - Gestión de permisos: establecer
umasko corregir explícitamente los permisos de destino conchmod.
old_umask=$(umask)
umask 027
TMPDIR=$(mktemp -d -p /var/tmp myjob.XXXXXX)
chmod 0700 "$TMPDIR"
# ... arbeiten ...
umask "$old_umask"
Integración práctica de monitoring, métricas y logs
Los códigos de salida son el medio primario de señalización; además, el logging estructurado y la exportación de métricas aportan ventajas tangibles para SRE/monitoring (p. ej., 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"
Clave: escriba únicamente en el directorio previsto, use movimientos atómicos para que los exportadores vean ficheros consistentes.
Pruebas, integración CI y análisis estático
No pruebe los scripts solo de forma manual: ShellCheck identifica errores de estilo y de seguridad, y los tests unitarios para funciones de shell (p. ej. con bats-core) detectan errores lógicos. Las pruebas de integración simulan dependencias ausentes y arranques paralelos.
# 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"
Resolución de problemas: cuadros de error típicos y secuencia de verificación
Cuando un job falla, proceda de forma estructurada:
- Revise los logs (stderr/stdout) en busca de la salida de la ERR-Trap. Busque el número de línea y el BASH_COMMAND que proporciona la ERR-Trap.
- Compruebe el entorno: PATH, IFS, variables con
declare -p. - Verifique el estado del lock: ¿existe el archivo de lock, quién lo mantiene? (ps aux | grep)
- En contenedores: ¿era la shell PID 1? Revise el árbol de procesos y el manejo de señales.
Estrategia de rollback y recuperación
Para cambios riesgosos (migración de BD, operaciones sobre ficheros) defina un procedimiento claro:
- Antes: copia de seguridad completa (backup de archivos, volcado de la BD) y sumas de comprobación para permitir la recuperación.
- Escribir el script de migración idempotente: comprueba el estado previo, aplica los cambios solo una vez, escribe un marcador y valida el resultado.
- Proveer y probar un script de rollback – rollback automático solo ante fallos claramente definidos.
# 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"
Lista de verificación breve para producción
- set-Flags establecidos de forma deliberada; excepciones documentadas.
- Entorno definido: PATH, IFS, Locale.
- Códigos de salida claros, registro en stderr, logs estructurados opcionales.
- Bloqueo con flock o coordinación central para Multi-Host.
- Idempotencia: Check-then-Do, marcadores atómicos, limpieza controlada.
- Docker: ENTRYPOINT con exec, Healthchecks sin efectos secundarios.
- Pruebas: ShellCheck, pruebas negativas, pruebas de arranque en paralelo, Dry-Run.
- Monitoring: exportación de tiempos de ejecución y errores como archivo de texto para exporters.
- Runbook: definiciones de Exit-Code, ubicaciones de logs, pasos de RESTore y rollback.
Conclusión
La robustez no es una línea de código, sino el resultado de decisiones pequeñas y coherentes: flags de set adecuados, traps para diagnóstico y limpieza, patrones idempotentes, bloqueo sensato y registro claro. Estas prácticas reducen significativamente los riesgos operativos y proporcionan patrones de error reproducibles para monitoring, soporte y gestión de incidentes. Mantenga además un pequeño runbook con definiciones de Exit-Code, ubicaciones de logs y pasos de recovery: así los scripts Bash se convierten en componentes fiables de su automatización.
Apéndice: breve Incident-Runbook (Template)
Una guía concisa que puede copiar en su runbook:
- 1) Examen de logs: comprobar /var/log/job.stderr y -stdout, anotar la línea del trap ERR.
- 2) Comprobar bloqueo:
ls -l /var/lock/,ps -ef | grep <pid>. - 3) Instantánea del entorno:
env | sort,declare -pde las variables relevantes. - 4) Intentar reejecución segura: comprobar DRY_RUN=1, luego realizar una ejecución real con backup.
- 5) Si la migración está implicada: RESTaurar el backup, eliminar el marcador, ejecutar las pruebas localmente.
Scripts Bash robustos: seguridad, auditoría y despliegue en operación
Además de la gestión de errores e idempotencia, tres aspectos son determinantes para el uso productivo: manejo de secretos, control de versiones y procesos de despliegue controlados. Los scripts suelen ejecutarse con privilegios amplios; por eso se aplica el principio de mínimos privilegios: ejecutar como un Service-User dedicado, capacidades específicas en lugar de root y permisos de fichero estrictos para archivos temporales y marcadores.
Nunca registre secretos sin enmascarar ni los exponga con set -x. Utilice mecanismos del contenedor u orquestador (Docker Secrets, Kubernetes Secrets) o una integración con un Vault central. Verifique los artefactos descargados con sumas de comprobación para que un mirror comprometido no afecte la cadena de suministro.
# 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
Versionieren Sie Skripte im Git, bauen Sie CI-Pipelines, die Linting, Unit-Tests und Signierung ausliefern. Verpacken Sie kritische Logik – vor allem bei komplexer Fehlerkorrektur oder hohem Durchsatz – lieber in ein kleines, typgeprüftes Binärwerkzeug (Go/Rust) und nutzen das Script als Orchestrator. Das verbessert Testbarkeit und reduziert Unfallflächen.
Für Deployment und Betrieb: führen Sie Rollouts gestaffelt durch (Canary), überwachen Fehler-Rate und Laufzeit-Metriken und implementieren automatische Quarantäne bei wiederholten Fehlern (z. B. Markierung, Alerting, vorübergehendes Backoff). Dokumentieren Sie jede Änderung in Runbook und Release-Notes, damit Audits nachvollziehbar sind und ein schneller, getesteter Rückrollpfad vorliegt.
Für dieses Thema sind auch Bash Fehlerhandling und Set -Euo Pipefail wichtig. Der Beitrag ordnet diese Aspekte verständlich ein und zeigt, worauf es im Alltag ankommt.