Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 1125 minIntermédiaire

Déployer automatiquement une application (CD)

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • choisir une méthode de déploiement adaptée à la cible ;
  • écrire un script de déploiement qui vérifie et revient en arrière ;
  • assembler un pipeline complet jusqu'à la production ;
  • vérifier un déploiement par des tests de fumée.

4 exercices corrigés1 schéma

Déployer automatiquement, c'est laisser le pipeline installer lui-même la nouvelle version dans un environnement, une fois les vérifications passées, par une procédure écrite et rejouable : récupérer l'artefact, appliquer les migrations, redémarrer le service, vérifier qu'il répond. Ce chapitre présente les façons de déployer (plateforme, serveur en SSH, site statique), écrit le script de déploiement du serveur de sorbier-frais, assemble le pipeline complet jusqu'à la production, et montre comment vérifier un déploiement.

Les façons de déployer

CibleComment le pipeline déploieExemplePour qui
Plateforme d'hébergement (PaaS)Elle surveille le dépôt, ou le pipeline appelle une URL de déclenchement (deploy hook)Render, Railway, Scalingo, Clever CloudUne application Web sans serveur à administrer
Serveur (VPS ou machine virtuelle interne)Connexion SSH, puis docker compose pull et upLe serveur de recette de Sorbier LogistiqueLe cas le plus formateur en BTS, SLAM comme SISR
Hébergement statiqueEnvoi des fichiers construitsGitHub Pages, NetlifyDocumentation, site vitrine, application React sans serveur
Cluster Kuberneteskubectl apply, Helm, ou un agent GitOpsArgo CD, FluxLes grandes infrastructures
Serveur Windows avec IISWeb Deploy, ou copie et redémarrage du site par PowerShell distantUne application ASP.NET interneLes environnements Microsoft

Scalingo et Clever Cloud sont des plateformes françaises : les données restent hébergées en France, un argument qui compte pour une administration ou une entreprise attentive à la souveraineté de ses données.

Le script de déploiement du serveur

Sur le serveur de recette, le dossier /opt/sorbier-frais contient le fichier Compose (l'infrastructure as code en bref), les fichiers de secrets .env, et un script deployer.sh que le pipeline appelle avec l'étiquette de l'image :

bash
#!/usr/bin/env bash
# Déploie une version de sorbier-frais : ./deployer.sh sha-3f9c2e1
set -euo pipefail

VERSION="${1:?Usage : deployer.sh <étiquette>}"
if [[ ! "$VERSION" =~ ^(sha-[0-9a-f]{7}|[0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
  echo "Étiquette invalide : $VERSION" >&2
  exit 2
fi
cd /opt/sorbier-frais

PRECEDENTE=$(cat version-courante 2>/dev/null || true)
export VERSION

echo "Déploiement de $VERSION (précédente : ${PRECEDENTE:-aucune})"
docker compose pull api
if [[ "${MIGRER:-oui}" == "oui" ]]; then
  docker compose run --rm api npm run migrate
fi
docker compose up -d api

for _ in $(seq 1 20); do
  if curl --fail --silent --output /dev/null http://127.0.0.1:3000/health; then
    echo "$VERSION" > version-courante
    echo "Version $VERSION en service"
    exit 0
  fi
  sleep 3
done

echo "La version $VERSION ne répond pas : retour à ${PRECEDENTE:-aucune}" >&2
if [[ -n "$PRECEDENTE" ]]; then
  VERSION="$PRECEDENTE" docker compose up -d api
fi
exit 1
PartieRôle
set -euo pipefailLe script s'arrête à la première commande en échec, à la première variable non définie, et sur une erreur au milieu d'un tube
Le test =~N'accepte qu'une étiquette de la forme sha- suivie de 7 caractères hexadécimaux, ou un numéro de version : rien d'autre ne peut être injecté dans les commandes
version-couranteUn fichier qui mémorise la version en service, pour pouvoir y revenir
docker compose pull apiTélécharge l'image de la nouvelle version, sans arrêter l'ancienne
docker compose run --rm api npm run migrateApplique les migrations avec le code de la nouvelle version, dans un conteneur temporaire ; si elles échouent, le script s'arrête et l'ancienne version continue de tourner. MIGRER=non saute cette étape, pour un retour arrière (chapitre 12)
docker compose up -d apiRemplace le conteneur par la nouvelle version
La boucle curlAttend jusqu'à 60 secondes que la route de santé réponde
La fin du scriptSi la nouvelle version ne répond pas, redémarre la précédente et renvoie un code d'erreur : le job du pipeline devient rouge

Ce script est versionné dans le dépôt (dossier deploy/) et installé sur le serveur avec le fichier Compose, par Ansible ou par une étape du pipeline. Il fait une courte coupure, le temps du redémarrage du conteneur : quelques secondes, acceptables pour une application interne. Le chapitre 12 montre comment l'éviter.

Le pipeline complet de sorbier-frais

Ce workflow réunit tout le cours : vérifications en parallèle, construction de l'image, déploiement automatique en recette avec test de fumée, puis production après approbation.

yaml
name: CI/CD

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

concurrency:
  group: ci-cd-${{ github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:
  IMAGE: ghcr.io/sorbier/sorbier-frais

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npm run lint

  tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npm test -- --ci

  image:
    needs: [lint, tests]
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    outputs:
      tag: ${{ steps.etiquette.outputs.tag }}
    steps:
      - uses: actions/checkout@v7
      - id: etiquette
        run: echo "tag=sha-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
      - uses: docker/setup-buildx-action@v4
      - if: github.event_name == 'push'
        uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v7
        with:
          context: .
          push: ${{ github.event_name == 'push' }}
          tags: ${{ env.IMAGE }}:${{ steps.etiquette.outputs.tag }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy-recette:
    needs: image
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    environment:
      name: recette
      url: https://recette.sorbier.example
    concurrency:
      group: deploiement-recette
      cancel-in-progress: false
    steps:
      - name: Préparer la connexion SSH
        env:
          SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
          KNOWN_HOSTS: ${{ vars.DEPLOY_KNOWN_HOSTS }}
        run: |
          install -m 700 -d ~/.ssh
          printf '%s\n' "$SSH_KEY" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          printf '%s\n' "$KNOWN_HOSTS" > ~/.ssh/known_hosts
      - name: Déployer
        run: ssh deploy@recette.sorbier.example "/opt/sorbier-frais/deployer.sh ${{ needs.image.outputs.tag }}"
      - name: Test de fumée
        run: curl --fail --silent --show-error --retry 5 --retry-delay 3 --retry-all-errors https://recette.sorbier.example/health

  deploy-production:
    needs: [image, deploy-recette]
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://frais.sorbier.example
    concurrency:
      group: deploiement-production
      cancel-in-progress: false
    steps:
      - name: Préparer la connexion SSH
        env:
          SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
          KNOWN_HOSTS: ${{ vars.DEPLOY_KNOWN_HOSTS }}
        run: |
          install -m 700 -d ~/.ssh
          printf '%s\n' "$SSH_KEY" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          printf '%s\n' "$KNOWN_HOSTS" > ~/.ssh/known_hosts
      - name: Déployer
        run: ssh deploy@frais.sorbier.example "/opt/sorbier-frais/deployer.sh ${{ needs.image.outputs.tag }}"
      - name: Test de fumée
        run: curl --fail --silent --show-error --retry 5 --retry-delay 3 --retry-all-errors https://frais.sorbier.example/health

Ce qui se passe :

  • Pour une pull request : lint et tests en parallèle, puis image construit l'image sans la pousser. deploy-recette est ignoré (if), donc deploy-production aussi. Un nouveau push sur la branche annule l'exécution précédente.
  • Après une fusion dans main : mêmes vérifications, puis l'image sha-… est poussée, déployée en recette et testée. deploy-production attend alors l'approbation d'un relecteur de l'environnement production. Sur main, une exécution en cours n'est jamais interrompue ; si plusieurs fusions arrivent pendant ce temps, seule la plus récente attend son tour, ce qui revient à déployer le dernier état de main.
  • Les secrets DEPLOY_SSH_KEY et la variable DEPLOY_KNOWN_HOSTS existent dans chaque environnement, avec des valeurs différentes : la clé de recette ne permet pas de déployer en production.

Promouvoir la même image

Job imageconstruitune seule foisRegistre ghcr.iosha-3f9c2e1Recettesha-3f9c2e1test de fumée vertautomatiquepuisapprobationProductionsha-3f9c2e1même imagepromueJamaisreconstruite pourla production
Une image construite une fois, déployée en recette, puis promue telle quelle en production

La production reçoit la même étiquette que la recette, transmise par la sortie needs.image.outputs.tag. L'image n'est jamais reconstruite : ce qui a été testé en recette est exactement ce qui tourne en production. On parle de promotion d'un artefact d'un environnement à l'autre.

Pour une garantie encore plus forte, on déploie l'image par son digest (ghcr.io/sorbier/sorbier-frais@sha256:…), que docker/build-push-action fournit dans sa sortie digest : une étiquette pourrait en théorie être réaffectée, un digest jamais.

Vérifier après le déploiement

Un déploiement réussi n'est pas une commande qui rend la main sans erreur : c'est un service qui fonctionne. Plusieurs niveaux de vérification se complètent :

VérificationCe qu'elle prouveExemple
Route de santé (liveness)Le processus tourne et répondGET /health renvoie 200
Route de disponibilité (readiness)Le service peut travailler : base joignable, configuration chargéeGET /health/ready vérifie la connexion MySQL
Route de versionLa bonne version est en ligneGET /version renvoie {"commit": "3f9c2e1"}, comparé à l'étiquette déployée
Tests de fumée fonctionnelsLes parcours essentiels marchentSe connecter avec un compte de test, lister les notes
SupervisionLe service reste sain dans la duréeTaux d'erreurs, temps de réponse, alertes (supervision)

Le pipeline exécute les trois premières ; la supervision prend le relais. Si le taux d'erreurs monte dans les minutes qui suivent un déploiement, le premier suspect est ce déploiement, et la première action est le retour arrière (chapitre 12).

Déployer sur une plateforme (PaaS)

Les plateformes d'hébergement simplifient le déploiement : on relie le dépôt, et la plateforme construit et déploie à chaque push sur la branche choisie. Pour garder la main sur le moment du déploiement, on désactive le déploiement automatique de la plateforme et on laisse le pipeline appeler l'URL de déclenchement après les tests :

yaml
  deploy:
    needs: [lint, tests]
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: recette
    steps:
      - name: Déclencher le déploiement
        env:
          DEPLOY_HOOK: ${{ secrets.DEPLOY_HOOK }}
        run: curl --fail --silent --show-error -X POST "$DEPLOY_HOOK"

L'URL de déclenchement est un secret : quiconque la connaît peut lancer un déploiement. La plateforme fait ensuite son propre contrôle de santé, et garde l'ancienne version si la nouvelle ne répond pas.

Publier un site statique sur GitHub Pages

Pour une documentation ou un site sans serveur, GitHub Pages héberge gratuitement les fichiers construits. Dans Settings, Pages, choisis la source GitHub Actions, puis :

yaml
name: Documentation

on:
  push:
    branches: [main]

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: false

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npm run docs:build
      - uses: actions/configure-pages@v6
      - uses: actions/upload-pages-artifact@v5
        with:
          path: docs/dist

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deploiement.outputs.page_url }}
    steps:
      - id: deploiement
        uses: actions/deploy-pages@v5

upload-pages-artifact empaquette le dossier construit, deploy-pages le publie ; id-token: write permet à l'action de prouver son identité à GitHub Pages par OIDC, sans secret. Pour l'E6, publier ainsi la documentation technique de ton projet à chaque fusion montre qu'elle suit le code.

Aller jusqu'au déploiement continu

Retirer l'approbation avant la production, c'est passer de la livraison continue au déploiement continu. Ce n'est raisonnable que si :

  • les tests automatiques couvrent les parcours essentiels et l'équipe leur fait confiance ;
  • chaque déploiement est petit (une pull request, pas trois semaines de travail) ;
  • la supervision détecte vite une dégradation, et le retour arrière est automatique ou se fait en une commande ;
  • les fonctionnalités risquées sont cachées derrière des interrupteurs de fonctionnalité (feature flags) qu'on peut couper sans redéployer (chapitre 12).

Pour un projet de BTS, la livraison continue (recette automatique, production approuvée) est le bon niveau : c'est ce que tu sauras expliquer et justifier.

Exercices

Exercice 1 sur 4 Lire un déploiement en échec Réussi À revoir

Après la fusion de la pull request « Export PDF des notes de frais », le job deploy-recette échoue :

text
Run ssh deploy@recette.sorbier.example "/opt/sorbier-frais/deployer.sh sha-b7e21f4"
Déploiement de sha-b7e21f4 (précédente : sha-3f9c2e1)
 api Pulled
Batch 5 run: 1 migrations
 Container sorbier-frais-api-1  Recreate
 Container sorbier-frais-api-1  Recreated
 Container sorbier-frais-api-1  Starting
 Container sorbier-frais-api-1  Started
La version sha-b7e21f4 ne répond pas : retour à sha-3f9c2e1
 Container sorbier-frais-api-1  Recreate
 Container sorbier-frais-api-1  Recreated
 Container sorbier-frais-api-1  Starting
 Container sorbier-frais-api-1  Started
Error: Process completed with exit code 1.
  1. Que s'est-il passé, étape par étape ? Quelle version tourne maintenant en recette ?
  2. Le job deploy-production va-t-il s'exécuter ?
  3. Sur le serveur, docker compose logs api montre, pour la version sha-b7e21f4 : Error: Cannot find module 'pdfkit'. Pourtant, les tests de la pull request passaient, y compris ceux de l'export PDF. Explique.
  4. Quel job ajouté au pipeline aurait détecté le problème avant le déploiement ?
Voir la réponseMasquer la réponse de l'exercice 1
  1. L'image sha-b7e21f4 a été téléchargée, une migration a été appliquée, le conteneur a été recréé avec la nouvelle version. Pendant 60 secondes, la route /health n'a jamais répondu : le script a redémarré la version précédente, sha-3f9c2e1, et renvoyé le code 1. La recette fait tourner sha-3f9c2e1, l'ancienne version, avec la nouvelle migration déjà appliquée (d'où l'intérêt des migrations par expansion, compatibles avec l'ancien code).
  2. Non : deploy-production dépend de deploy-recette, qui a échoué ; il est ignoré.
  3. pdfkit a été installé comme dépendance de développement (npm install --save-dev pdfkit). Dans la CI, npm ci installe toutes les dépendances : les tests passent. Mais l'image de production est construite avec npm ci --omit=dev : pdfkit en est absent, et l'application plante au démarrage. Correction : npm install --save pdfkit (dépendance de production), nouvelle pull request.
  4. Un job qui construit l'image et la démarre pour un test de fumée, avant tout déploiement (construire et tester l'image dans la CI). Il aurait échoué dès la pull request.

Tu avais trouvé ?

Exercice 2 sur 4 Un déploiement trop pressé Réussi À revoir

yaml
jobs:
  tests:
    runs-on: ubuntu-latest
    steps: [...]
  deploy:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - run: ./scripts/deployer.sh
  1. Un commit dont les tests échouent est poussé sur main. Est-il déployé ?
  2. Corrige le workflow.
Voir la réponseMasquer la réponse de l'exercice 2
  1. Oui : sans needs, deploy démarre en même temps que tests, sans l'attendre. Le code est déployé pendant que les tests échouent. De plus, il manque le checkout : le script n'est pas sur le runner, et le job échouerait de toute façon.
  2. On chaîne les jobs et on récupère le code :
yaml
jobs:
  tests:
    runs-on: ubuntu-latest
    steps: [...]
  deploy:
    needs: tests
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: recette
    steps:
      - uses: actions/checkout@v7
      - run: ./scripts/deployer.sh

needs: tests chaîne « tester, puis déployer ». La condition sur l'événement est une précaution : avec un déclencheur workflow_dispatch ajouté plus tard, un lancement manuel sur main déploierait sans passer par une fusion.

Tu avais trouvé ?

Exercice 3 sur 4 Un test de fumée qui ne teste rien Réussi À revoir

Le test de fumée de Hugo est curl https://recette.sorbier.example/health. Après un déploiement raté, l'API renvoyait une erreur 502 du serveur frontal, et l'étape était pourtant verte. Explique et corrige.

Voir la réponseMasquer la réponse de l'exercice 3

Sans option, curl renvoie le code 0 dès qu'il reçoit une réponse HTTP, quel que soit le statut : une page d'erreur 502 est un succès pour lui. Il faut --fail (ou -f), qui renvoie un code d'erreur pour un statut 4xx ou 5xx :

curl --fail --silent --show-error --retry 5 --retry-delay 3 --retry-all-errors https://recette.sorbier.example/health

--retry et --retry-all-errors laissent au service le temps de démarrer. Une vérification plus forte compare aussi la version renvoyée par /version à l'étiquette déployée.

Tu avais trouvé ?

Exercice 4 sur 4 Quelle version tourne ? Réussi À revoir

Le pipeline a déployé sha-3f9c2e1 en recette lundi, puis sha-b7e21f4 mardi (échec et retour arrière, comme dans le premier exercice), puis sha-c90a2d8 mercredi (succès). Que contient le fichier version-courante après chaque déploiement ? Si le déploiement de jeudi échoue, vers quelle version le script reviendra-t-il ?

Voir la réponseMasquer la réponse de l'exercice 4
  • Lundi : sha-3f9c2e1.
  • Mardi : toujours sha-3f9c2e1 ; le script n'écrit le fichier qu'après un contrôle de santé réussi, et la version de mardi n'en a pas eu.
  • Mercredi : sha-c90a2d8.
  • Jeudi, en cas d'échec : retour à sha-c90a2d8, la dernière version qui a fonctionné.

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Stratégies de déploiement : blue-green, canary et rollbackRevoir le sommaire du cours