Intro
Bash reste la colle de nombreux pipelines CI/CD (Continuous Integration / Continuous Delivery) : intégration continue et livraison continue. Il est disponible partout, rapide à itérer, et idéal pour orchestrer des outils comme Git, Docker et Kubernetes. Ce guide propose une approche Bash prête pour la production en CI/CD : sécurité stricte du shell, journalisation claire, fonctions modulaires, reprises (retries) et timeouts, validation avant déploiement, vérification de santé, et rollbacks rapides. Vous verrez aussi des exemples GitHub Actions et GitLab CI que vous pouvez copier dans vos dépôts dès aujourd’hui.
Ce que vous allez construire :
- Une bibliothèque Bash réutilisable (modes stricts, logs, traps, reprises, timeouts, verrous)
- Des scripts pour lint, test, build, validate, deploy, health-check et rollback
- Des pipelines CI pour GitHub Actions et GitLab CI
- Des modèles de rollback pratiques pour Kubernetes et Docker Compose
Arborescence recommandée du dépôt :
.
├─ scripts/
│ ├─ lib.sh
│ ├─ lint.sh
│ ├─ test.sh
│ ├─ build.sh
│ ├─ validate.sh
│ ├─ deploy.sh
│ ├─ healthcheck.sh
│ └─ rollback.sh
├─ docker/
│ └─ compose.yaml
├─ k8s/
│ ├─ deployment.yaml
│ └─ service.yaml
└─ app/
└─ ...
1) Fondation Bash sécurisée (scripts/lib.sh)
Activez les modes stricts, un découpage des mots prévisible, une gestion d’erreurs défensive et une journalisation structurée.
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# Colors only if stdout is a TTY
if [ -t 1 ]; then
GREEN='\033[0;32m'; YELLOW='\033[0;33m'; RED='\033[0;31m'; NC='\033[0m'
else
GREEN=''; YELLOW=''; RED=''; NC=''
fi
log() { printf "%sINFO%s %s\n" "$GREEN" "$NC" "$*"; }
warn() { printf "%sWARN%s %s\n" "$YELLOW" "$NC" "$*"; }
err() { printf "%sERROR%s %s\n" "$RED" "$NC" "$*" 1>&2; }
fail(){ err "$*"; exit 1; }
require() { command -v "$1" >/dev/null 2>&1 || fail "Missing command: $1"; }
assert_env(){ for v in "$@"; do [ -n "${!v-}" ] || fail "Missing env var: $v"; done; }
# Basic error context
_on_error() {
local ec=$? line=${1:-}
err "Command failed at line $line (exit=$ec). Enable trace with: export TRACE=1"
}
# Optional tracing for debugging
[ "${TRACE-}" = "1" ] && set -x
trap '_on_error $LINENO' ERR
# Run CMD with retries
with_retry() {
local attempts=${1:-5} sleep_s=${2:-2}; shift 2
local n=1
until "$@"; do
if [ $n -ge "$attempts" ]; then
err "Retry failed after $attempts attempts: $*"; return 1
fi
warn "Attempt $n failed. Retrying in ${sleep_s}s..."; sleep "$sleep_s"; n=$((n+1))
done
}
# Timebox a command if 'timeout' exists
timebox() {
local seconds=$1; shift
if command -v timeout >/dev/null 2>&1; then
timeout "$seconds" "$@"
else
warn "timeout not found; running without time limit"
"$@"
fi
}
# Serialize critical sections
lock_run() {
local lockfile=$1; shift
require flock
exec 9>"$lockfile"
flock -n 9 || fail "Another process holds $lockfile"
"$@"
}
# Simple on-exit cleanup hook
_cleanup_funcs=()
on_exit() { _cleanup_funcs+=("$*"); }
trap 'for f in "${_cleanup_funcs[@]}"; do eval "$f" || true; done' EXIT
Points clés :
- set -euo pipefail et IFS limitent les pièges courants et les échecs silencieux.
- trap avec un gestionnaire personnalisé fournit un contexte d’erreur exploitable.
- with_retry et timebox protègent des opérations instables ou longues.
- lock_run sérialise les déploiements pour éviter les conditions de concurrence.
2) Scripts de lint et de tests
Lintez vos scripts Bash et exécutez des tests unitaires rapides. Remplacez l’extrait de test par le runner de tests de votre langage au besoin.
scripts/lint.sh
#!/usr/bin/env bash
set -euo pipefail
. "$(dirname "$0")/lib.sh"
log "Linting bash scripts"
find . -type f -name "*.sh" -print0 | xargs -0 -I{} bash -n {}
if command -v shellcheck >/dev/null 2>&1; then
log "Running shellcheck"
find . -type f -name "*.sh" -print0 | xargs -0 shellcheck -x
else
warn "shellcheck not found; skipping static analysis"
fi
scripts/test.sh
#!/usr/bin/env bash
set -euo pipefail
. "$(dirname "$0")/lib.sh"
log "Running unit tests"
add() { echo $(( $1 + $2 )); }
assert_eq(){ [ "$1" = "$2" ] || fail "assert_eq failed: got=$1 want=$2"; }
assert_eq "$(add 2 3)" 5
log "Tests passed"
3) Construire un artefact
Produisez une image Docker quand Docker est disponible; sinon créez une archive tarball pour que les étapes suivantes disposent quand même d’un artefact stable.
scripts/build.sh
#!/usr/bin/env bash
set -euo pipefail
. "$(dirname "$0")/lib.sh"
APP_NAME=${APP_NAME:-demoapp}
IMAGE_REPO=${IMAGE_REPO:-example/demoapp}
GIT_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo local)
TAG=${TAG:-$GIT_SHA}
ART_DIR=${ART_DIR:-dist}
mkdir -p "$ART_DIR"
if command -v docker >/dev/null 2>&1; then
log "Building Docker image $IMAGE_REPO:$TAG"
docker build -t "$IMAGE_REPO:$TAG" -f docker/Dockerfile .
echo "$IMAGE_REPO:$TAG" > "$ART_DIR/image.tag"
else
warn "docker not found; creating tarball artifact instead"
tar -czf "$ART_DIR/$APP_NAME-$TAG.tgz" app/
echo "$ART_DIR/$APP_NAME-$TAG.tgz" > "$ART_DIR/artifact.path"
fi
4) Valider avant de déployer
Validez les manifests et les configurations Compose en CI pour échouer tôt.
scripts/validate.sh
#!/usr/bin/env bash
set -euo pipefail
. "$(dirname "$0")/lib.sh"
STAGE=${STAGE:-staging}
COMPOSE_FILE=${COMPOSE_FILE:-docker/compose.yaml}
log "Validating deployment manifests for $STAGE"
if command -v kubectl >/dev/null 2>&1 && [ -d k8s ]; then
require kubectl
kubectl apply --dry-run=server -f k8s/
kubectl diff -f k8s/ || true
elif command -v docker >/dev/null 2>&1 && [ -f "$COMPOSE_FILE" ]; then
require docker
docker compose -f "$COMPOSE_FILE" config >/dev/null
else
warn "No k8s or compose config found to validate"
fi
5) Déployer en sécurité et vérifier la santé
Déployez avec un verrou pour éviter les exécutions qui se chevauchent, puis vérifiez un endpoint de santé avant de marquer la réussite.
scripts/deploy.sh
#!/usr/bin/env bash
set -euo pipefail
. "$(dirname "$0")/lib.sh"
STAGE=${STAGE:-staging}
HEALTH_URL=${HEALTH_URL:-http://localhost:8080/healthz}
LOCKFILE=${LOCKFILE:-/tmp/deploy.lock}
_do_deploy() {
log "Starting deploy to $STAGE"
if command -v kubectl >/dev/null 2>&1 && [ -d k8s ]; then
kubectl apply -f k8s/
if kubectl get deploy >/dev/null 2>&1; then
for d in $(kubectl get deploy -o name); do
log "Waiting for $d to roll out"
with_retry 30 2 kubectl rollout status "$d"
done
fi
elif command -v docker >/dev/null 2>&1 && [ -f docker/compose.yaml ]; then
docker compose -f docker/compose.yaml up -d --build
else
warn "No deploy target detected; skipping apply"
fi
"$(dirname "$0")/healthcheck.sh" "$HEALTH_URL" 60 2
log "Deployment verified"
}
lock_run "$LOCKFILE" _do_deploy
scripts/healthcheck.sh
#!/usr/bin/env bash
set -euo pipefail
. "$(dirname "$0")/lib.sh"
URL=${1:-http://localhost:8080/healthz}
ATTEMPTS=${2:-30}
SLEEP=${3:-2}
require curl
with_retry "$ATTEMPTS" "$SLEEP" bash -c "curl -fsS --max-time 2 '$URL' >/dev/null"
6) Promotion et rollback
Enregistrez la version déployée pour rendre les rollbacks prévisibles.
scripts/rollback.sh
#!/usr/bin/env bash
set -euo pipefail
. "$(dirname "$0")/lib.sh"
STAGE=${STAGE:-staging}
PREV_FILE=${PREV_FILE:-.previous_version}
CURR_FILE=${CURR_FILE:-dist/image.tag}
COMPOSE_FILE=${COMPOSE_FILE:-docker/compose.yaml}
promote() {
[ -f "$CURR_FILE" ] || fail "Missing current version file: $CURR_FILE"
cp "$CURR_FILE" "$PREV_FILE"
log "Promoted version $(cat "$CURR_FILE") for $STAGE"
}
rollback() {
[ -f "$PREV_FILE" ] || fail "No previous version recorded"
local prev repo tag
prev=$(cat "$PREV_FILE")
repo=${prev%:*}
tag=${prev#*:}
if command -v kubectl >/dev/null 2>&1 && kubectl get deploy >/dev/null 2>&1; then
for d in $(kubectl get deploy -o name); do
log "Rolling back $d to image $prev"
kubectl set image "$d" "*=$prev" --record
with_retry 30 2 kubectl rollout status "$d"
done
elif command -v docker >/dev/null 2>&1 && [ -f "$COMPOSE_FILE" ]; then
# Requires compose.yaml to use image: "${IMAGE_REPO}:${TAG}" or "${IMAGE}:${TAG}"
IMAGE_REPO="$repo" TAG="$tag" docker compose -f "$COMPOSE_FILE" up -d
log "Compose services updated to $repo:$tag"
else
fail "No supported platform for rollback"
fi
}
case "${1:-}" in
promote) promote ;;
rollback) rollback ;;
*) echo "Usage: $0 {promote|rollback}"; exit 2 ;;
esac
Astuce : Kubernetes prend aussi en charge kubectl rollout undo deploy/<nom> pour revenir au ReplicaSet précédent sans gérer les tags d’image. L’usage de tags explicites plus un fichier enregistré rend l’historique portable entre clusters et runners.
7) Intégration CI
GitHub Actions (/.github/workflows/ci.yaml) :
name: ci
on:
push:
branches: [ main ]
jobs:
build-test-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Lint
run: scripts/lint.sh
- name: Test
run: scripts/test.sh
- name: Build
run: scripts/build.sh
- name: Validate manifests
env:
STAGE: staging
run: scripts/validate.sh
- name: Deploy to staging and verify
env:
STAGE: staging
HEALTH_URL: https://example-staging.local/healthz
run: scripts/deploy.sh
- name: Record version for rollback
run: scripts/rollback.sh promote
GitLab CI (/.gitlab-ci.yml) :
stages: [lint, test, build, validate, deploy]
variables:
GIT_STRATEGY: fetch
lint:
stage: lint
image: alpine:3.20
before_script: ["apk add --no-cache bash findutils shellcheck"]
script:
- chmod +x scripts/*.sh
- scripts/lint.sh
unit_test:
stage: test
image: alpine:3.20
before_script: ["apk add --no-cache bash"]
script:
- chmod +x scripts/*.sh
- scripts/test.sh
build:
stage: build
image: docker:26
services:
- docker:26-dind
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
script:
- chmod +x scripts/*.sh
- scripts/build.sh
artifacts:
paths:
- dist/
validate:
stage: validate
image: bitnami/kubectl:1.29
script:
- chmod +x scripts/*.sh
- STAGE=staging scripts/validate.sh
deploy_staging:
stage: deploy
image: bitnami/kubectl:1.29
when: manual
script:
- chmod +x scripts/*.sh
- STAGE=staging HEALTH_URL=https://example-staging.local/healthz scripts/deploy.sh
- scripts/rollback.sh promote
8) Tableaux de comparaison
Étapes du pipeline et objectifs :
| Étape | Script | Objectif | Échec si… |
|---|---|---|---|
| Lint | scripts/lint.sh | Détecter les erreurs syntaxe/style | Une erreur bash -n ou shellcheck survient |
| Test | scripts/test.sh | Vérifier le comportement clé | Des assertions échouent |
| Build | scripts/build.sh | Produire image/archives | Échec du build Docker ou de la création tar |
| Validate | scripts/validate.sh | Dry-run des manifests/config | Schéma invalide ou références erronées |
| Deploy | scripts/deploy.sh | Appliquer + attendre + health check | Rollout ou endpoint de santé en échec |
| Promote | rollback.sh promote | Enregistrer la version courante | dist/image.tag manquant |
| Rollback | rollback.sh rollback | Rétablir rapidement | Aucune version précédente ou erreur de plateforme |
Approches de rollback :
| Plateforme | Méthode | Avantages | Limites |
|---|---|---|---|
| Kubernetes | kubectl rollout undo | Rapide, exploite le ReplicaSet précédent | Limité au dernier ReplicaSet |
| Kubernetes | Définir l’image sur le tag enregistré | Explicite, traçable, portable | Nécessite l’enregistrement/poussée des tags |
| Docker Compose | Redéployer avec variables IMAGE/TAG | Simple, pas d’outil additionnel | Le compose.yaml doit utiliser des variables |
9) Dépannage et durcissement
- Le script s’arrête sans contexte : export TRACE=1 pour activer le traçage set -x via lib.sh.
- Échecs cachés dans les pipelines : utilisez toujours
-euo pipefailet mettez les variables entre guillemets, par exemple"${var}". - Contrôle de santé instable : augmentez les tentatives de with_retry ou le délai SLEEP; assurez-vous que votre application renvoie 200 OK.
- kubectl non configuré : fournissez KUBECONFIG ou exécutez kubectl config set-context avant le déploiement.
- Permission Docker refusée : assurez-vous que le runner a accès au socket Docker ou utilisez DinD avec DOCKER_HOST.
- Secrets dans les logs : ne loggez jamais des variables d’environnement secrètes; préférez des fichiers ou des variables CI masquées.
- Déploiements concurrents : garantissez que lock_run utilise un chemin partagé (p. ex. /tmp/deploy.lock) entre les étapes.
- Rollback en échec sur Compose : confirmez que compose.yaml utilise
image: "${IMAGE_REPO}:${TAG}"et que IMAGE_REPO/TAG sont définies depuis le fichier enregistré. - Commandes longues : encapsulez avec
timebox 60 <cmd>; installez coreutils/timeout si absent.
10) Pilote local exécutable dès aujourd’hui
- Rendez les scripts exécutables : chmod +x scripts/*.sh
- .env optionnel pour les exécutions locales :
- APP_NAME=demoapp
- IMAGE_REPO=example/demoapp
- STAGE=staging
- HEALTH_URL=http://localhost:8080/healthz
- Enchaînez le flux :
- scripts/lint.sh
- scripts/test.sh
- scripts/build.sh
- scripts/validate.sh || true
- scripts/deploy.sh
- scripts/rollback.sh promote
- Tester le rollback :
- Déployez un mauvais tag (p. ex., modifiez k8s/deployment.yaml ou définissez TAG=bad et lancez deploy)
- Observez l’échec du health check
- Exécutez scripts/rollback.sh rollback et confirmez le rétablissement
Mesurez : temps total, taux de succès du health-check pendant le déploiement, et temps moyen de rollback.
Conclusion
Vous disposez maintenant d’une boîte à outils Bash pratique pour la CI/CD : sécurité stricte du shell, logs et utilitaires réutilisables, validation avant déploiement, rollouts vérifiés et rollbacks rapides et traçables. Commencez par le pilote local, connectez-le à GitHub Actions ou GitLab CI, et faites évoluer les mêmes scripts du staging à la production. Entraînez-vous régulièrement aux scénarios d’échec, conservez les versions précédentes dans un stockage pérenne, et votre automatisation Bash restera prévisible, inspectable et sûre à étendre à mesure que votre plateforme grandit.