Retour au cours

infra / linux-bash

Bash en production : robustesse, idempotence, CI

Leçon 251 exercice

Explication

Ce que vous allez apprendre

  • Écrire un script de déploiement idempotent, qui peut être relancé sans effet de bord
  • Empêcher deux exécutions concurrentes du même script avec flock
  • Absorber une instabilité réseau ponctuelle avec un retry à délai exponentiel
  • Déployer via des releases numérotées et un symlink, pour un rollback instantané
  • Systématiser un health check automatique après chaque déploiement

Dans quel contexte ?

Ton pipeline CI vient de planter à 2h du matin en pleine release, et GitLab relance automatiquement le job. Si ton script n'est pas idempotent, cette deuxième exécution peut entrer en collision avec ce que la première avait déjà commencé à faire, et transformer un simple raté réseau en incident sérieux.

D'abord, le concept central : l'idempotence

Un script de déploiement sera, tôt ou tard, relancé plusieurs fois : après un échec, pour corriger un problème, ou par erreur. Un script est dit idempotent quand le relancer plusieurs fois de suite ne cause aucun problème, parce qu'il vérifie l'état actuel avant d'agir plutôt que de supposer un point de départ.

Voici à quoi ça ressemble concrètement

Plutôt que de faire mkdir "$APP_DIR" directement, le script vérifie d'abord if [ ! -d "$APP_DIR" ]; then mkdir -p "$APP_DIR"; fi. Relancé dix fois, ce bloc ne provoque jamais d'erreur, contrairement à un mkdir brut qui échouerait si le dossier existe déjà.

SituationSans idempotenceAvec idempotence
Dossier déjà créémkdir échoue avec une erreurif [ ! -d ... ] passe silencieusement
Utilisateur déjà crééuseradd échoueVérifié avant (id ... || useradd)
Script relancé après échecRésultat imprévisibleMême résultat final garanti

Une fois l'idempotence acquise, il reste un risque : deux exécutions en même temps

Si deux déploiements sont lancés simultanément par erreur, ils peuvent écrire dans les mêmes fichiers en même temps et se marcher dessus. flock -n 200 sur un fichier de verrou (exec 200>"$LOCK_FILE") garantit qu'une seule exécution est active, la seconde s'arrête immédiatement avec un message clair.

Maintenant, un problème différent : le réseau n'est jamais parfait

Un curl vers un registre distant peut échouer ponctuellement sans que ce soit grave, à cause d'une latence temporaire. Réessayer immédiatement en boucle peut aggraver la situation ; la fonction retry() de l'exemple double le délai entre chaque tentative (delai=$(( delai * 2 ))), un backoff exponentiel qui laisse le temps au système distant de se rétablir.

Il reste une dernière question : comment revenir en arrière si le déploiement échoue

Déployer une nouvelle version par-dessus l'ancienne rend le retour en arrière compliqué. La technique des releases numérotées (releases/20260905120000/) avec un lien symbolique current qui pointe dessus permet de basculer instantanément : un rollback devient juste ln -sfn "$precedente" "$APP_DIR/current".

Piège classique

Écrire une procédure de rollback qu'on ne teste jamais avant d'en avoir réellement besoin. Simule un rollback en environnement de test avant la mise en prod, pas le jour où ça casse pour de vrai.

Le piège à connaître

Déployer sans health check automatique après coup, c'est découvrir un service cassé quand un utilisateur se plaint, plutôt qu'immédiatement via curl -fsS "http://localhost:8000/health". Maintenant que tu sais écrire un script de déploiement fiable, la prochaine leçon montre comment orchestrer ce type de script sur plusieurs serveurs à la fois avec Ansible.

Commandes & code

Bash en production : robustesse, idempotence, CI

bash
#!/usr/bin/env bash
# Script de déploiement idempotent : peut être relancé plusieurs fois sans effet de bord
set -euo pipefail
trap 'echo "[FATAL] Échec ligne $LINENO" >&2' ERR

readonly APP_DIR="/opt/mon-app"
readonly RELEASE="$(date +%Y%m%d%H%M%S)"
readonly LOCK_FILE="/tmp/deploy.lock"

# --- Verrou pour empêcher deux déploiements simultanés ---
exec 200>"$LOCK_FILE"
if ! flock -n 200; then
    echo "Un déploiement est déjà en cours, abandon." >&2
    exit 1
fi

# --- Idempotence : vérifier l'état avant d'agir, plutôt que supposer ---
if [ ! -d "$APP_DIR" ]; then
    mkdir -p "$APP_DIR"
fi

if ! id "deploy" &>/dev/null; then
    useradd -r -s /usr/sbin/nologin deploy
fi

# --- Retry avec backoff exponentiel pour les opérations réseau instables ---
retry() {
    local max_tentatives=5 tentative=1 delai=2
    until "$@"; do
        if (( tentative == max_tentatives )); then
            echo "Échec définitif après $max_tentatives tentatives : $*" >&2
            return 1
        fi
        echo "Tentative $tentative échouée, nouvel essai dans ${delai}s..." >&2
        sleep "$delai"
        delai=$(( delai * 2 ))
        (( tentative++ ))
    done
}
retry curl -fsS https://registry.exemple.com/mon-app/latest.tar.gz -o "$APP_DIR/release.tar.gz"

# --- Déploiement en releases + symlink, permet un rollback instantané ---
mkdir -p "$APP_DIR/releases/$RELEASE"
tar -xzf "$APP_DIR/release.tar.gz" -C "$APP_DIR/releases/$RELEASE"
ln -sfn "$APP_DIR/releases/$RELEASE" "$APP_DIR/current"       # bascule atomique du symlink
systemctl restart mon-app

# --- Health check post-déploiement avec rollback automatique ---
sleep 3
if ! curl -fsS "http://localhost:8000/health" > /dev/null; then
    echo "Health check KO, rollback vers la release précédente" >&2
    precedente=$(ls -1dt "$APP_DIR"/releases/*/ | sed -n 2p)
    ln -sfn "$precedente" "$APP_DIR/current"
    systemctl restart mon-app
    exit 1
fi

# --- Nettoyage : ne garder que les 5 dernières releases ---
ls -1dt "$APP_DIR"/releases/*/ | tail -n +6 | xargs -r rm -rf

echo "Déploiement de la release $RELEASE terminé avec succès."
yaml
# Exemple d'intégration dans une pipeline CI (GitLab CI) qui appelle ce script via SSH
deploy_prod:
  stage: deploy
  script:
    - ssh -o StrictHostKeyChecking=yes deploy@prod "bash -s" < ./scripts/deploy.sh
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

Résumé

  • Idempotence : chaque étape vérifie l'état avant d'agir (if [ ! -d ... ], id ... || useradd).
  • flock empêche les exécutions concurrentes, retry/backoff absorbe l'instabilité réseau.
  • Releases + symlink atomique = rollback instantané ; toujours coupler un déploiement à un health check.

Exercices pratiques

1 disponible
1

Mission : survivre à un relancement automatique de la CI

Objectif : Rendre un script de déploiement idempotent, protégé contre les exécutions concurrentes, avec un rollback fiable.

Contexte

Ton pipeline CI vient de planter à 2h du matin en pleine release, et GitLab relance automatiquement le job. Le script de déploiement actuel n'a pas été pensé pour survivre à une deuxième exécution.

Résoudre l’exercice →