## 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 :

<div style="overflow-x:auto;margin:1.5rem 0;"><table style="width:100%;border-collapse:collapse;font-size:0.95rem;line-height:1.5;"><thead><tr><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">Étape</th><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">Script</th><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">Objectif</th><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">Échec si…</th></tr></thead><tbody><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Lint</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">scripts/lint.sh</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Détecter les erreurs syntaxe/style</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Une erreur bash -n ou shellcheck survient</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Test</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">scripts/test.sh</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Vérifier le comportement clé</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Des assertions échouent</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Build</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">scripts/build.sh</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Produire image/archives</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Échec du build Docker ou de la création tar</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Validate</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">scripts/validate.sh</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Dry-run des manifests/config</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Schéma invalide ou références erronées</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Deploy</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">scripts/deploy.sh</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Appliquer + attendre + health check</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Rollout ou endpoint de santé en échec</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Promote</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">rollback.sh promote</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Enregistrer la version courante</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">dist/image.tag manquant</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Rollback</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">rollback.sh rollback</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Rétablir rapidement</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Aucune version précédente ou erreur de plateforme</td></tr></tbody></table></div>
Approches de rollback :

<div style="overflow-x:auto;margin:1.5rem 0;"><table style="width:100%;border-collapse:collapse;font-size:0.95rem;line-height:1.5;"><thead><tr><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">Plateforme</th><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">Méthode</th><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">Avantages</th><th style="border:1px solid rgba(15,23,42,.18);padding:.75rem;text-align:left;background:#f8fafc;font-weight:700;">Limites</th></tr></thead><tbody><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Kubernetes</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">kubectl rollout undo</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Rapide, exploite le ReplicaSet précédent</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Limité au dernier ReplicaSet</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Kubernetes</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Définir l’image sur le tag enregistré</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Explicite, traçable, portable</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Nécessite l’enregistrement/poussée des tags</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Docker Compose</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Redéployer avec variables IMAGE/TAG</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Simple, pas d’outil additionnel</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Le compose.yaml doit utiliser des variables</td></tr></tbody></table></div>

## 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 pipefail et 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.