IT-Admin.tech

Scripts Bash robustos: manejo de errores, flags de set y patrones idempotentes

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

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

Explicació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.

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

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

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

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

Patrones 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

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

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

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

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

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

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

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

Nota: 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 -d y establecer permisos seguros (umask/ chmod).
  • Escrituras atómicas: escribir en un archivo temporal y reemplazarlo con mv.
  • Gestión de permisos: establecer umask o corregir explícitamente los permisos de destino con chmod.
Shell
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).

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"

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.

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"

Resolución de problemas: cuadros de error típicos y secuencia de verificación

Cuando un job falla, proceda de forma estructurada:

  1. 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.
  2. Compruebe el entorno: PATH, IFS, variables con declare -p.
  3. Verifique el estado del lock: ¿existe el archivo de lock, quién lo mantiene? (ps aux | grep)
  4. 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.
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"

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 -p de 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.

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

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.