infra / linux-bash
Bash en production : robustesse, idempotence, CI
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à.
| Situation | Sans idempotence | Avec idempotence |
|---|---|---|
| Dossier déjà créé | mkdir échoue avec une erreur | if [ ! -d ... ] passe silencieusement |
| Utilisateur déjà créé | useradd échoue | Vérifié avant (id ... || useradd) |
| Script relancé après échec | Résultat imprévisible | Mê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
#!/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."# 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). flockempê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
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.