0Pricing
DevOps Bootcamp · Leçon

Scripts idempotents et logique de nouvelle tentative avec temporisation

Concevez des opérations pouvant être relancées sans danger et ajoutez une temporisation exponentielle aux appels externes instables.

Scripts idempotents et logique de nouvelle tentative avec temporisation est une leçon DevOps Bootcamp gratuite sur CoddyKit. Ceci est la leçon 4 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage DevOps Bootcamp, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours DevOps Bootcamp comprend 4 leçons au total.

Qu'est-ce que l'idempotence et pourquoi est-elle importante

L'idempotence signifie que l'exécution répétée d'une même opération produit le même résultat que son exécution unique. Dans les scripts Bash, c'est essentiel, car les scripts peuvent planter, les réseaux peuvent tomber en panne et les utilisateurs peuvent relancer des opérations par erreur.

  • Un script non idempotent qui crée deux fois un utilisateur peut échouer ou dupliquer des données.
  • Un script idempotent vérifie d'abord : cela existe-t-il déjà ?
  • Les scripts idempotents peuvent être utilisés sans danger dans les tâches cron, les chaînes d'intégration continue et les boucles de nouvelle tentative.

La règle d'or : vérifiez avant d'agir. Toute opération destructive ou créatrice doit être protégée par un test de précondition.

Protéger la création de fichiers et de répertoires

Le modèle d'idempotence le plus courant consiste à vérifier si une ressource existe déjà avant de la créer. Bash fournit des commandes concises pour cela.

  • [ -d dir ] — vrai si le répertoire existe
  • [ -f file ] — vrai si le fichier ordinaire existe
  • mkdir -p — crée le répertoire uniquement s'il est absent (idempotence intégrée)

Privilégiez les options intégrées comme -p et --no-clobber aux vérifications manuelles lorsqu'elles sont disponibles — elles sont atomiques et protégées contre les conditions de concurrence.

#!/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

Gérer les utilisateurs et les groupes de manière idempotente

Les tâches d'administration système, comme l'ajout d'utilisateurs ou de groupes, doivent être idempotentes : la réexécution du script sur la même machine ne doit ni provoquer d'erreur ni créer de doublons.

  • id username renvoie 0 si l'utilisateur existe
  • getent group groupname vérifie l'existence d'un groupe
  • Encadrez chaque opération par une condition de garde afin que le script puisse être rejoué sans danger

Ce modèle constitue le fondement des outils de gestion de configuration comme Ansible : chaque tâche est une opération protégée et 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

Utiliser des fichiers de verrouillage pour empêcher les exécutions simultanées

Même un script idempotent peut poser problème si deux instances s'exécutent simultanément. Un fichier de verrouillage garantit qu'une seule instance s'exécute à la fois.

  • Créez un fichier de verrouillage au démarrage et supprimez-le à la fin.
  • Utilisez une trap pour nettoyer le verrou même si le script est interrompu.
  • mkdir sur un chemin unique est atomique sur la plupart des systèmes de fichiers Linux — et plus sûr que touch pour le verrouillage.

Sans verrou, une tâche cron lente et une relance manuelle peuvent entrer en collision et corrompre l'état partagé.

#!/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."

Suivre les étapes terminées avec un fichier d'état

Pour les scripts comportant plusieurs étapes (migrations, installations, mise à disposition), vous pouvez suivre les étapes déjà terminées à l'aide d'un fichier d'état. Chaque étape vérifie le fichier d'état avant de s'exécuter et y écrit une fois terminée.

  • Économique et portable — aucune base de données n'est requise.
  • Permet à un script ayant échoué de reprendre là où il s'est arrêté.
  • Stockez l'état dans un emplacement prévisible comme /var/lib/myapp/ ou ~/.myapp/state/.

Ce modèle est utilisé par des outils majeurs comme apt, cloud-init et les structures de migration de bases de données.

#!/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...'"

Introduction à la logique de nouvelle tentative

Les appels externes — requêtes HTTP, recherches DNS, appels d'API cloud — sont intrinsèquement peu fiables. Un seul échec ne devrait pas interrompre l'intégralité de votre script. La logique de nouvelle tentative réessaie automatiquement une commande qui a échoué.

  • Nouvelle tentative naïve : répéter la commande N fois jusqu'à sa réussite.
  • Définissez toujours un nombre maximal de tentatives pour éviter les boucles infinies.
  • Journalisez chaque tentative afin de pouvoir diagnostiquer les échecs.

La fonction de nouvelle tentative la plus simple encapsule n'importe quelle commande et la réessaie jusqu'à un nombre fixé de fois, avec un délai constant. Cela suffit pour de nombreux cas d'utilisation, mais présente une faille critique en situation de forte charge — abordée dans la suite.

#!/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."

Le problème de l'effet de troupeau

Lorsque de nombreux clients effectuent une nouvelle tentative simultanément après un échec, ils créent un effet de troupeau — tous réessaient au même intervalle fixe, surchargeant le serveur exactement au même moment et rendant la récupération impossible.

  • 100 scripts réessaient toutes les 5 secondes → 100 requêtes simultanées toutes les 5 secondes.
  • Le serveur rencontre déjà des difficultés ; la charge synchronisée aggrave la situation.
  • La solution est le recul exponentiel : doubler le temps d'attente après chaque échec.
  • Ajoutez une gigue (variation aléatoire) pour désynchroniser les nouvelles tentatives entre les clients.

Le recul exponentiel avec gigue est la norme du secteur, utilisée par les kits de développement AWS, les clients Google Cloud et tous les grands systèmes distribués.

Implémenter une temporisation exponentielle

La temporisation exponentielle augmente le temps d’attente de façon exponentielle après chaque échec : 1 s, 2 s, 4 s, 8 s, 16 s... Le système distant a ainsi le temps de récupérer, tout en réduisant la charge totale.

La formule : delay = base * (2 ^ attempt)

  • Définissez une limite (délai maximal) afin que les temps d’attente n’augmentent pas indéfiniment.
  • Paramètres à ajuster : base_delay, max_delay, max_attempts.

Cette fonction est réutilisable : transmettez-lui n’importe quelle commande comme argument.

#!/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"

Ajouter une variation aléatoire à la temporisation

La variation aléatoire ajoute une part d’aléatoire au délai de temporisation. Même avec une temporisation exponentielle, si tous les clients démarrent en même temps, ils réessaieront encore de manière synchronisée. La variation aléatoire rompt cette synchronisation.

Deux stratégies courantes de variation aléatoire :

  • Variation aléatoire complète : sleep random(0, cap) — dispersion maximale et charge de pointe minimale.
  • Variation aléatoire égale : sleep cap/2 + random(0, cap/2) — garantit un temps d’attente minimal et évite de réessayer immédiatement de façon intensive.

Dans Bash, utilisez $RANDOM (0–32767) pour générer des nombres aléatoires. Adaptez-les à votre plage de délais à l’aide de l’arithmétique modulo.

#!/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."

Combiner l’idempotence et les nouvelles tentatives dans un flux de travail réel

Dans les scripts utilisés en production, l’idempotence et la logique de nouvelle tentative fonctionnent ensemble. Un flux de déploiement typique peut :

  1. Acquérir un verrou (pour empêcher les exécutions simultanées)
  2. Vérifier les fichiers d’état (pour ignorer les étapes terminées)
  3. Utiliser une nouvelle tentative avec temporisation pour les appels externes (téléchargement, API, DNS)
  4. Marquer les étapes comme terminées uniquement après confirmation de leur réussite
  5. Libérer le verrou avec trap

Cette combinaison rend les scripts sûrs à réexécuter à tout moment — après un plantage, un dépassement de délai ou un abandon manuel — sans laisser le système dans un état défectueux.

#!/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."

Gérer les erreurs qui ne doivent pas faire l’objet de nouvelles tentatives

Toutes les erreurs ne doivent pas faire l’objet d’une nouvelle tentative. Réessayer après une erreur 404 Not Found ou 403 Forbidden est inutile : ces requêtes n’aboutiront jamais sans intervention humaine. Votre logique de nouvelle tentative doit distinguer :

  • Erreurs temporaires — dépassement de délai réseau, 503 Service Unavailable, échec DNS → réessayer
  • Erreurs permanentes — 401 Unauthorized, 404 Not Found, entrée non valide → échouer immédiatement

Avec curl, vérifiez le code d’état HTTP et ignorez les nouvelles tentatives pour les réponses 4xx. Utilisez --write-out '%{http_code}' pour récupérer l’état séparément du corps de la réponse.

#!/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"

Vérification des connaissances : choisir une stratégie de temporisation

Un script de déploiement télécharge un artefact de version depuis un compartiment S3. Lors d’un incident récent, 80 instances du flux de traitement ont toutes échoué simultanément en raison d’une brève interruption de S3. Lorsque S3 a récupéré après 10 secondes, les 80 instances ont réessayé au même moment, provoquant une nouvelle surcharge et prolongeant l’interruption de 3 minutes.

Quelle stratégie de nouvelle tentative permettrait le mieux d’éviter cette cascade provoquée par un afflux massif de requêtes lors de futurs incidents ?

Récapitulatif : idempotence et nouvelles tentatives avec temporisation

Dans cette leçon, vous avez appris à concevoir des scripts Bash sûrs à réexécuter et résistants aux défaillances temporaires.

Schémas d’idempotence :

  • Protégez chaque opération par une vérification d’existence ([ -f ], [ -d ], id, getent).
  • Utilisez mkdir -p et les autres options intégrées idempotentes lorsqu’elles sont disponibles.
  • Utilisez des fichiers de verrouillage (mkdir atomique) pour empêcher les exécutions simultanées.
  • Utilisez des fichiers d’état (fichiers marqueurs pour chaque étape) afin de permettre la reprise après un échec.

Schémas de nouvelles tentatives avec temporisation :

  • Définissez toujours un nombre maximal de tentatives — ne réessayez jamais indéfiniment.
  • Utilisez une temporisation exponentielle : doublez le délai après chaque échec.
  • Ajoutez une variation aléatoire pour éviter la synchronisation des nombreuses requêtes simultanées.
  • Distinguez les erreurs temporaires (réessayer) des erreurs permanentes (échouer rapidement).

La combinaison de ces schémas produit des scripts adaptés à la production : sûrs, observables et capables de se rétablir automatiquement.

Questions Fréquemment Posées

La leçon « Scripts idempotents et logique de nouvelle tentative avec temporisation » est-elle gratuite ?

Oui — le texte complet de « Scripts idempotents et logique de nouvelle tentative avec temporisation » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours DevOps Bootcamp, passe à CoddyKit PRO. Le cours DevOps Bootcamp comprend 4 leçons au total.

Qu'est-ce que j'apprendrai dans « Scripts idempotents et logique de nouvelle tentative avec temporisation » ?

Concevez des opérations pouvant être relancées sans danger et ajoutez une temporisation exponentielle aux appels externes instables. Tu pratiques DevOps Bootcamp avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.

Dois-je avoir de l'expérience pour commencer DevOps Bootcamp ?

Aucune expérience préalable n'est requise. DevOps Bootcamp sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 4 sur 4.

Combien de temps prend la leçon « Scripts idempotents et logique de nouvelle tentative avec temporisation » ?

La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.

Peux-tu écrire et exécuter du code dans cette leçon DevOps Bootcamp ?

Oui. Chaque leçon DevOps Bootcamp inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.

Toutes les leçons de ce cours

  1. Mode strict avec set -euo pipefail
  2. Gestionnaires trap pour le nettoyage et les signaux
  3. Fichiers temporaires sûrs et répertoires de verrouillage
  4. Scripts idempotents et logique de nouvelle tentative avec temporisation
← Retour à DevOps Bootcamp