0Pricing
DevOps Bootcamp · Lección

Scripts idempotentes y lógica de reintento con backoff

Diseñe operaciones que puedan ejecutarse de nuevo sin riesgos y añada backoff exponencial para llamadas externas inestables.

Scripts idempotentes y lógica de reintento con backoff es una lección gratuita de DevOps Bootcamp en CoddyKit. Esta es la lección 4 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de DevOps Bootcamp, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de DevOps Bootcamp incluye 4 lecciones en total.

Qué es la idempotencia y por qué es importante

La idempotencia significa que ejecutar varias veces la misma operación produce el mismo resultado que ejecutarla una sola vez. En los scripts de Bash, esto es fundamental porque los scripts pueden fallar, las redes pueden interrumpirse y las personas pueden volver a ejecutar algo por accidente.

  • Un script no idempotente que crea dos veces un usuario puede fallar o duplicar datos.
  • Un script idempotente comprueba primero: ¿ya existe esto?
  • Los scripts idempotentes se pueden utilizar de forma segura en trabajos de Cron, pipelines de CI y bucles de reintento.

La regla de oro es: compruebe antes de actuar. Toda operación destructiva o de creación debe estar protegida por una comprobación de sus precondiciones.

Protección de la creación de archivos y directorios

El patrón de idempotencia más común consiste en comprobar si un recurso ya existe antes de crearlo. Bash ofrece expresiones concisas para hacerlo.

  • [ -d dir ]: verdadero si existe el directorio
  • [ -f file ]: verdadero si existe el archivo normal
  • mkdir -p: crea el directorio únicamente si no existe (idempotencia integrada)

Cuando estén disponibles, prefiera opciones integradas como -p y --no-clobber a las comprobaciones manuales: son atómicas y seguras frente a condiciones de carrera.

#!/usr/bin/env bash
set -euo pipefail

CONFIG_DIR="$HOME/.myapp"
CONFIG_FILE="$CONFIG_DIR/config.ini"

# Idempotent: mkdir -p never fails if dir already exists
mkdir -p "$CONFIG_DIR"

# Idempotent: only write config if it doesn't exist yet
if [ ! -f "$CONFIG_FILE" ]; then
  echo '[defaults]' > "$CONFIG_FILE"
  echo 'timeout=30' >> "$CONFIG_FILE"
  echo "Created $CONFIG_FILE"
else
  echo "Config already exists, skipping."
fi

Gestión idempotente de usuarios y grupos

Las tareas de administración del sistema, como añadir usuarios o grupos, deben ser idempotentes: volver a ejecutar el script en la misma máquina no debería producir errores ni crear duplicados.

  • id username devuelve 0 si el usuario existe
  • getent group groupname comprueba si existe un grupo
  • Proteja cada operación con una comprobación para que el script se pueda volver a ejecutar de forma segura

Este patrón es la base de herramientas de gestión de configuración como Ansible: cada tarea es una operación protegida e idempotente.

#!/usr/bin/env bash
set -euo pipefail

APP_USER="apprunner"
APP_GROUP="appgroup"

# Idempotent group creation
if ! getent group "$APP_GROUP" &>/dev/null; then
  groupadd "$APP_GROUP"
  echo "Group '$APP_GROUP' created."
else
  echo "Group '$APP_GROUP' already exists."
fi

# Idempotent user creation
if ! id "$APP_USER" &>/dev/null; then
  useradd -m -g "$APP_GROUP" -s /bin/bash "$APP_USER"
  echo "User '$APP_USER' created."
else
  echo "User '$APP_USER' already exists."
fi

Uso de archivos de bloqueo para impedir ejecuciones simultáneas

Incluso un script idempotente puede causar problemas si dos instancias se ejecutan simultáneamente. Un archivo de bloqueo garantiza que solo una instancia se ejecute a la vez.

  • Cree un archivo de bloqueo al iniciar y elimínelo al terminar.
  • Use un trap para limpiar el bloqueo incluso si se interrumpe el script.
  • Ejecutar mkdir sobre una única ruta es atómico en la mayoría de los sistemas de archivos de Linux, por lo que es más seguro que touch para crear bloqueos.

Sin un bloqueo, un trabajo de Cron lento y una ejecución manual repetida pueden interferir entre sí y corromper el estado compartido.

#!/usr/bin/env bash
set -euo pipefail

LOCKFILE="/tmp/myapp_deploy.lock"

# Atomic lock acquisition using mkdir
if ! mkdir "$LOCKFILE" 2>/dev/null; then
  echo "ERROR: Another instance is running (lock: $LOCKFILE). Exiting." >&2
  exit 1
fi

# Guarantee lock removal on any exit
trap 'rmdir "$LOCKFILE"; echo "Lock released."' EXIT

echo "Lock acquired. Running deployment..."
sleep 2   # simulate work
echo "Deployment complete."

Seguimiento de los pasos completados con un archivo de estado

En scripts con varios pasos (migraciones, instalaciones, aprovisionamiento), puede registrar qué pasos ya se han completado mediante un archivo de estado. Cada paso comprueba el archivo de estado antes de ejecutarse y escribe en él al terminar.

  • Es económico y portable: no requiere una base de datos.
  • Permite que un script fallido continúe desde el punto en el que se detuvo.
  • Almacene el estado en una ubicación predecible, como /var/lib/myapp/ o ~/.myapp/state/.

Este patrón se utiliza en herramientas importantes como apt, cloud-init y frameworks de migración de bases de datos.

#!/usr/bin/env bash
set -euo pipefail

STATE_DIR="/tmp/myapp_state"
mkdir -p "$STATE_DIR"

run_step() {
  local step_name="$1"
  local step_cmd="$2"
  local marker="$STATE_DIR/${step_name}.done"

  if [ -f "$marker" ]; then
    echo "[SKIP] $step_name already completed."
    return 0
  fi

  echo "[RUN]  $step_name ..."
  eval "$step_cmd"
  touch "$marker"
  echo "[DONE] $step_name"
}

run_step "install_deps"   "echo 'Installing dependencies...'"
run_step "configure_db"   "echo 'Configuring database...'"
run_step "start_service"  "echo 'Starting service...'"

Introducción a la lógica de reintentos

Las llamadas externas —solicitudes HTTP, búsquedas DNS y llamadas a API de servicios en la nube— son inherentemente poco fiables. Un solo fallo no debería abortar todo el script. La lógica de reintentos vuelve a intentar automáticamente un comando fallido.

  • Reintento ingenuo: repetir el bucle N veces hasta que el comando se ejecute correctamente.
  • Establezca siempre un número máximo de reintentos para evitar bucles infinitos.
  • Registre cada intento para que los fallos se puedan diagnosticar.

La función de reintento más sencilla envuelve cualquier comando y lo reintenta hasta un número fijo de veces, con un retraso constante. Esto es suficiente para muchos casos de uso, pero presenta un defecto crítico bajo carga, que veremos a continuación.

#!/usr/bin/env bash
set -euo pipefail

# Simple fixed-delay retry (3 attempts, 2s apart)
retry() {
  local max_attempts=3
  local delay=2
  local attempt=1

  until "$@"; do
    if (( attempt >= max_attempts )); then
      echo "ERROR: Command failed after $max_attempts attempts: $*" >&2
      return 1
    fi
    echo "Attempt $attempt failed. Retrying in ${delay}s..." >&2
    sleep "$delay"
    (( attempt++ ))
  done
}

# Example: retry a curl call
retry curl --silent --fail --max-time 5 https://httpbin.org/get -o /dev/null
echo "Request succeeded."

El problema de la estampida

Cuando muchos clientes vuelven a intentarlo simultáneamente después de un fallo, crean una estampida: todos reintentan en el mismo intervalo fijo, saturan el servidor exactamente en el mismo momento y hacen imposible la recuperación.

  • 100 scripts reintentan cada 5 segundos → 100 solicitudes simultáneas cada 5 segundos.
  • El servidor ya tiene dificultades; la carga sincronizada empeora la situación.
  • La solución es el retroceso exponencial: duplicar el tiempo de espera después de cada fallo.
  • Añada jitter (ruido aleatorio) para desincronizar los reintentos entre los clientes.

El retroceso exponencial con jitter es el estándar del sector utilizado por los SDK de AWS, los clientes de Google Cloud y los principales sistemas distribuidos.

Implementación de la espera exponencial

La espera exponencial aumenta exponencialmente el tiempo de espera después de cada fallo: 1 s, 2 s, 4 s, 8 s, 16 s... Esto da tiempo al sistema remoto para recuperarse y, al mismo tiempo, reduce la carga total.

La fórmula es: delay = base * (2 ^ attempt)

  • Establezca un límite (retraso máximo) para que los tiempos de espera no crezcan indefinidamente.
  • Parámetros que puede ajustar: base_delay, max_delay, max_attempts.

Esta función se puede reutilizar: pase cualquier comando como argumento.

#!/usr/bin/env bash
set -euo pipefail

retry_with_backoff() {
  local max_attempts="${RETRY_MAX_ATTEMPTS:-5}"
  local base_delay="${RETRY_BASE_DELAY:-1}"
  local max_delay="${RETRY_MAX_DELAY:-30}"
  local attempt=0
  local delay="$base_delay"

  until "$@"; do
    (( attempt++ ))
    if (( attempt >= max_attempts )); then
      echo "ERROR: '$*' failed after $max_attempts attempts." >&2
      return 1
    fi
    echo "Attempt $attempt failed. Backing off for ${delay}s..." >&2
    sleep "$delay"
    # Double the delay, but cap it
    delay=$(( delay * 2 ))
    (( delay > max_delay )) && delay=$max_delay
  done

  echo "Command succeeded on attempt $(( attempt + 1 ))."
}

retry_with_backoff echo "Simulated success"

Añadir jitter a la espera

El jitter añade aleatoriedad al retraso de la espera. Incluso con una espera exponencial, si todos los clientes comienzan al mismo tiempo, seguirán reintentando de forma sincronizada. El jitter rompe esta sincronización.

Dos estrategias de jitter habituales:

  • Jitter completo: sleep random(0, cap) — dispersión máxima y menor carga máxima.
  • Jitter equilibrado: sleep cap/2 + random(0, cap/2) — garantiza una espera mínima y evita saturar el sistema inmediatamente.

En Bash, use $RANDOM (0–32767) para generar números aleatorios. Escálelos al rango de retraso mediante aritmética modular.

#!/usr/bin/env bash
set -euo pipefail

retry_with_jitter() {
  local max_attempts="${1:-5}"; shift
  local base_delay=1
  local max_delay=32
  local attempt=0
  local cap=$base_delay

  until "$@"; do
    (( attempt++ ))
    if (( attempt >= max_attempts )); then
      echo "ERROR: Giving up after $max_attempts attempts." >&2
      return 1
    fi

    # Full jitter: random value in [0, cap]
    local jitter=$(( RANDOM % (cap + 1) ))
    echo "Attempt $attempt failed. Sleeping ${jitter}s (cap=${cap}s)..." >&2
    sleep "$jitter"

    # Grow cap exponentially, bounded by max_delay
    cap=$(( cap * 2 ))
    (( cap > max_delay )) && cap=$max_delay
  done
}

retry_with_jitter 4 curl --silent --fail --max-time 3 https://httpbin.org/get -o /dev/null
echo "Done."

Combinar idempotencia y reintentos en un flujo de trabajo real

En los scripts de producción, la idempotencia y la lógica de reintentos trabajan conjuntamente. Un flujo de implementación habitual podría:

  1. Adquirir un bloqueo (para evitar ejecuciones simultáneas)
  2. Comprobar los archivos de estado (para omitir los pasos completados)
  3. Usar reintentos con espera para las llamadas externas (descargas, API, DNS)
  4. Marcar los pasos como completados solo después de confirmar que se ejecutaron correctamente
  5. Liberar el bloqueo mediante trap

Esta combinación hace que los scripts sean seguros para volver a ejecutarlos en cualquier momento, ya sea después de un bloqueo, un tiempo de espera agotado o una cancelación manual, sin dejar el sistema en un estado defectuoso.

#!/usr/bin/env bash
set -euo pipefail

STATE_DIR="/tmp/deploy_state" && mkdir -p "$STATE_DIR"
LOCK="/tmp/deploy.lock"

mkdir "$LOCK" 2>/dev/null || { echo "Already running."; exit 1; }
trap 'rmdir "$LOCK"' EXIT

step_done() { [ -f "$STATE_DIR/$1.done" ]; }
mark_done() { touch "$STATE_DIR/$1.done"; }

retry_backoff() {
  local attempt=0 delay=1
  until "${@:2}"; do
    (( ++attempt >= $1 )) && { echo "Failed after $1 attempts."; return 1; }
    echo "Retry $attempt in ${delay}s..."; sleep $delay; delay=$(( delay * 2 ))
  done
}

if ! step_done "download_artifact"; then
  retry_backoff 4 curl -fsSL https://httpbin.org/get -o /tmp/artifact.json
  mark_done "download_artifact"
  echo "[DONE] download_artifact"
else
  echo "[SKIP] download_artifact"
fi

echo "Deployment finished successfully."

Gestionar errores que no admiten reintentos

No todos los errores deben reintentarse. Reintentar un error 404 Not Found o 403 Forbidden es inútil: nunca se resolverán sin intervención humana. La lógica de reintentos debe distinguir entre:

  • Errores transitorios: tiempo de espera agotado de red, 503 Service Unavailable, fallo de DNS → reintentar
  • Errores permanentes: 401 Unauthorized, 404 Not Found, entrada no válida → fallar inmediatamente

Con curl, compruebe el código de estado HTTP y omita los reintentos para las respuestas 4xx. Use --write-out '%{http_code}' para capturar el estado por separado del cuerpo.

#!/usr/bin/env bash
set -euo pipefail

fetch_with_retry() {
  local url="$1"
  local max_attempts=4
  local delay=1
  local attempt=0
  local http_code

  until http_code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$url"); do
    : # curl itself failed (network error)
    (( ++attempt >= max_attempts )) && { echo "Network error, giving up."; return 1; }
    sleep $delay; delay=$(( delay * 2 ))
  done

  # Permanent client errors — do not retry
  if [[ "$http_code" =~ ^4 ]]; then
    echo "ERROR: HTTP $http_code for $url — not retrying." >&2
    return 1
  fi

  # Transient server errors — retry
  if [[ "$http_code" =~ ^5 ]]; then
    (( ++attempt >= max_attempts )) && { echo "Server error, giving up."; return 1; }
    echo "HTTP $http_code — backing off ${delay}s..."
    sleep $delay; delay=$(( delay * 2 ))
    fetch_with_retry "$url"
    return
  fi

  echo "HTTP $http_code — success."
}

fetch_with_retry "https://httpbin.org/status/200"

Comprobación de conocimientos: elegir una estrategia de espera

Un script de implementación descarga un artefacto de lanzamiento de un bucket de S3. Durante un incidente reciente, las 80 instancias de la canalización fallaron simultáneamente debido a una breve interrupción de S3. Cuando S3 se recuperó después de 10 segundos, las 80 instancias reintentaron la operación en el mismo momento, lo que provocó otra sobrecarga y prolongó la interrupción durante 3 minutos.

¿Qué estrategia de reintentos evitaría mejor esta cascada de solicitudes simultáneas en futuros incidentes?

Repaso: idempotencia y reintentos con espera

En esta lección ha aprendido a diseñar scripts de Bash que sean seguros para volver a ejecutarlos y resistentes a fallos transitorios.

Patrones de idempotencia:

  • Proteja cada operación con una comprobación de existencia ([ -f ], [ -d ], id, getent).
  • Use mkdir -p y otras opciones idempotentes integradas cuando estén disponibles.
  • Use archivos de bloqueo (mkdir atómico) para evitar ejecuciones simultáneas.
  • Use archivos de estado (archivos marcadores por paso) para poder reanudar la ejecución después de un fallo.

Patrones de reintentos con espera:

  • Establezca siempre un número máximo de intentos: nunca reintente indefinidamente.
  • Use una espera exponencial: duplique el retraso después de cada fallo.
  • Añada jitter (aleatoriedad) para evitar la sincronización de solicitudes simultáneas.
  • Distinga entre errores transitorios (reintentar) y permanentes (fallar rápidamente).

La combinación de estos patrones produce scripts preparados para producción: seguros, observables y capaces de recuperarse por sí mismos.

Preguntas frecuentes

¿La lección «Scripts idempotentes y lógica de reintento con backoff» es gratis?

Sí — el texto completo de «Scripts idempotentes y lógica de reintento con backoff» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de DevOps Bootcamp, actualiza a CoddyKit PRO. El curso de DevOps Bootcamp incluye 4 lecciones en total.

¿Qué aprenderé en «Scripts idempotentes y lógica de reintento con backoff»?

Diseñe operaciones que puedan ejecutarse de nuevo sin riesgos y añada backoff exponencial para llamadas externas inestables. Practicas DevOps Bootcamp con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.

¿Necesito experiencia previa para empezar DevOps Bootcamp?

No se requiere experiencia previa. DevOps Bootcamp en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 4 de 4.

¿Cuánto tiempo toma la lección «Scripts idempotentes y lógica de reintento con backoff»?

La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.

¿Puedo escribir y ejecutar código en esta lección de DevOps Bootcamp?

Sí. Cada lección de DevOps Bootcamp incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.

Todas las lecciones de este curso

  1. Modo estricto con set -euo pipefail
  2. Controladores trap para limpieza y señales
  3. Archivos temporales seguros y directorios de bloqueo
  4. Scripts idempotentes y lógica de reintento con backoff
← Volver a DevOps Bootcamp