Chapitre 1017 minIntermédiaire
Environnements : développement, recette, production
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- décrire le rôle de chaque environnement et les données qu'il contient ;
- séparer l'artefact de la configuration de chaque environnement ;
- protéger la production par une approbation dans GitHub ;
- faire évoluer une base sans casser l'ancienne version ;
- expliquer ce qu'est l'infrastructure as code.
4 exercices corrigés
Un environnement est une installation complète de l'application (serveurs, base de données, configuration) réservée à un usage : le développement, les tests automatiques, la recette par les utilisateurs, la production. Le pipeline fait passer la même version d'un environnement à l'autre ; seule la configuration change. Ce chapitre décrit les environnements classiques, la gestion de leur configuration, les environnements de GitHub et de GitLab avec leurs règles de protection, les migrations de base de données, puis l'infrastructure as code.
Les environnements classiques
| Environnement | Rôle | Qui l'utilise | Données |
|---|---|---|---|
| Développement | Écrire et essayer le code | Le développeur, sur son poste ou dans un conteneur | Jeu de données fictif |
| Intégration (CI) | Lancer les tests automatiques | Le pipeline, dans des conteneurs jetables | Créées et détruites par les tests |
| Recette (staging, UAT) | Valider les fonctionnalités avant la mise en production : tests d'acceptation, démonstrations | L'équipe, les utilisateurs référents, le client | Fictives ou anonymisées |
| Préproduction | Répéter la mise en production dans des conditions identiques : même configuration, même volume | L'exploitation | Copie anonymisée de la production |
| Production | Servir les vrais utilisateurs | Tout le monde | Réelles |
Un projet de BTS a souvent trois environnements : le poste du développeur, la recette et la production. La préproduction existe surtout dans les organisations où une panne coûte cher.
Ce qui change d'un environnement à l'autre : la configuration
La règle « construire une fois, déployer partout » impose que l'artefact ne change pas d'un environnement à l'autre. Tout ce qui diffère est de la configuration, fournie au démarrage par des variables d'environnement :
| Réglage | Recette | Production |
|---|---|---|
URL_PUBLIQUE | https://recette.sorbier.example | https://frais.sorbier.example |
DB_HOST, DB_NAME | Serveur et base de recette | Serveur et base de production |
DB_PASSWORD | Secret de l'environnement recette | Secret de l'environnement production |
LOG_LEVEL | debug | info |
SMTP_HOST | Un faux serveur qui capture les courriels (Mailpit) | Le vrai serveur d'envoi |
PAIEMENT_MODE | test (aucun virement réel) | production |
L'application lit ces valeurs au démarrage (process.env.DB_HOST). Le code ne contient jamais de test du genre if (environnement === 'production') pour changer de serveur : ce serait du code jamais testé en recette.
Les environnements de GitHub
Dans Settings, Environments, on crée un environnement par cible : recette, production. Chacun peut avoir :
| Réglage | Effet |
|---|---|
| Required reviewers | Jusqu'à six personnes ou équipes ; le job attend qu'une d'elles approuve |
| Prevent self-review | La personne qui a déclenché le déploiement ne peut pas l'approuver elle-même |
| Wait timer | Un délai d'attente avant le déploiement, de 1 minute à 30 jours |
| Deployment branches and tags | Seules certaines branches ou certains tags peuvent déployer (main, v*) |
| Environment secrets | Des secrets réservés aux jobs de cet environnement, transmis seulement après l'approbation |
| Environment variables | Des variables propres à l'environnement (vars.URL_PUBLIQUE) |
Un job se rattache à un environnement par la clé environment :
jobs:
deploy-production:
runs-on: ubuntu-latest
environment:
name: production
url: https://frais.sorbier.example
concurrency:
group: deploiement-production
cancel-in-progress: false
steps:
- name: Déployer
env:
DEPLOY_HOOK: ${{ secrets.DEPLOY_HOOK }} # secret de l'environnement production
run: curl --fail --silent --show-error -X POST "$DEPLOY_HOOK"
Quand ce job démarre, GitHub le met en attente et prévient les relecteurs. L'un d'eux ouvre l'exécution, lit ce qui va être déployé, puis clique sur Approve and deploy (ou Reject). Le secret DEPLOY_HOOK de l'environnement n'est transmis qu'à ce moment-là. L'historique des déploiements s'affiche sur la page d'accueil du dépôt, avec le lien url vers l'application.
C'est exactement la livraison continue : tout est automatique jusqu'à la production, et une personne décide du moment.
Disponibilité. Avec l'offre gratuite, les relecteurs obligatoires, le délai et les secrets d'environnement ne sont disponibles que pour les dépôts publics ; pour un dépôt privé, GitHub Pro (offert aux étudiants) ajoute les restrictions de branches, les variables et les secrets d'environnement, mais pas les relecteurs obligatoires ni le délai, réservés aux dépôts publics et aux offres Enterprise. Pour un projet de BTS, un dépôt public est la solution la plus simple.
Utiliser un environnement sans déployer. Depuis mars 2026, la clé deployment: false, placée sous environment à côté de name, donne au job les secrets et les variables de l'environnement sans créer de déploiement dans l'historique : pratique pour un job de vérification qui lit une variable de recette mais ne déploie rien. Ce réglage n'est pas compatible avec les règles de protection personnalisées (celles fournies par une application tierce).
Les environnements de GitLab
Dans GitLab, la clé environment d'un job enregistre chaque déploiement dans Operate, Environments (environnements et déploiement manuel). La page d'un environnement liste ses déploiements successifs, avec un bouton pour redéployer une version précédente. Les variables peuvent être limitées à un environnement (Environment scope), et les environnements protégés, dans les offres payantes, restreignent qui peut lancer le déploiement ou l'approuver.
Les environnements éphémères
Un environnement éphémère (review app, preview) est créé automatiquement pour une pull request et détruit à sa fusion ou sa fermeture. Chaque fonctionnalité a ainsi sa propre adresse, comme pr-42.recette.sorbier.example, où l'utilisatrice référente peut l'essayer avant la fusion.
Les plateformes d'hébergement d'applications Web (Vercel, Netlify, Render…) le proposent en un réglage. GitLab le prévoit avec les review apps : environment:url contient le nom de la branche, et environment:on_stop désigne le job qui détruit l'environnement. Pour un projet de BTS, c'est une excellente démonstration : on montre la fonctionnalité au jury sans toucher à la production.
Les migrations de base de données
Le code d'une nouvelle version attend souvent une base différente : une colonne de plus, une table renommée. Ces changements de schéma s'écrivent dans des migrations : des scripts numérotés, versionnés avec le code, que l'outil applique dans l'ordre et note dans une table de suivi.
| Écosystème | Outil de migration |
|---|---|
| Node.js | Knex, Prisma Migrate, Sequelize |
| PHP | Doctrine Migrations (Symfony), migrations Laravel |
| C# | Entity Framework Core (dotnet ef database update) |
| Indépendant du langage | Flyway, Liquibase |
Dans le pipeline, la migration s'exécute juste avant le déploiement du nouveau code, dans chaque environnement, recette d'abord. En production, une sauvegarde la précède toujours (sauvegarder une base MySQL).
Le piège : pendant quelques secondes ou quelques minutes, l'ancien et le nouveau code tournent en même temps, et un retour arrière remet l'ancien code face au nouveau schéma. La méthode expansion, puis contraction (expand and contract) évite les cassures :
- Expansion : on ajoute sans rien casser. Nouvelle colonne facultative, nouvelle table. L'ancien code fonctionne toujours.
- On déploie le nouveau code, qui utilise la nouvelle structure (et remplit éventuellement la nouvelle colonne à partir de l'ancienne).
- Contraction, dans une version ultérieure, quand plus aucun code n'utilise l'ancienne structure : on supprime l'ancienne colonne.
L'infrastructure as code en bref
L'infrastructure as code (IaC) consiste à décrire les serveurs, les réseaux et les services dans des fichiers texte versionnés, appliqués par un outil, plutôt que de les configurer à la main. Les fichiers passent en revue dans des pull requests et sont appliqués par le pipeline, comme le code de l'application.
Deux idées la caractérisent :
- Déclaratif : on décrit l'état voulu (« ce serveur a Docker et ce fichier Compose »), pas la suite de commandes pour y arriver ; l'outil calcule les actions nécessaires.
- Idempotent : appliquer deux fois la même description donne le même résultat ; la seconde fois, l'outil constate que tout est déjà conforme et ne change rien.
| Outil | Ce qu'il décrit | Exemple |
|---|---|---|
| Terraform, OpenTofu | Les ressources : machines virtuelles, réseaux, DNS, bases managées chez un fournisseur | « deux VM, un réseau, un enregistrement DNS » |
| Ansible | La configuration des serveurs existants : paquets, fichiers, services, comptes | « Docker installé, compte deploy créé, fichier Compose copié » |
| Docker Compose | Les conteneurs d'une application et leurs liens | « l'API et sa base MySQL, avec un volume » |
| Kubernetes (manifestes, Helm) | Les applications sur un cluster | « trois répliques de l'API derrière un service » |
| cloud-init | La configuration d'une VM à son premier démarrage | « créer l'utilisateur, installer les clés SSH » |
Terraform est passé sous une licence non libre en 2023 ; OpenTofu, son dérivé libre maintenu par la Linux Foundation, reprend la même syntaxe.
Le fichier Compose du serveur de recette de sorbier-frais, versionné dans le dépôt :
services:
api:
image: ghcr.io/sorbier/sorbier-frais:${VERSION:?VERSION manquante}
restart: unless-stopped
env_file: .env
ports:
- "127.0.0.1:3000:3000"
depends_on:
- mysql
mysql:
image: mysql:8.4
restart: unless-stopped
env_file: .env.mysql
volumes:
- donnees-mysql:/var/lib/mysql
volumes:
donnees-mysql:
La version à déployer arrive par la variable VERSION : VERSION=sha-3f9c2e1 docker compose up -d démarre exactement cette image, et refuse de démarrer si la variable manque. Les fichiers .env, qui contiennent les secrets, restent sur le serveur et ne sont pas versionnés. Le port est publié sur 127.0.0.1 seulement : un serveur web frontal (Nginx, Caddy) reçoit le trafic HTTPS et le transmet à l'API.
Une tâche Ansible équivalente, pour préparer le serveur :
- name: Préparer le serveur de recette de sorbier-frais
hosts: recette
become: true
tasks:
- name: Créer le dossier de l'application
ansible.builtin.file:
path: /opt/sorbier-frais
state: directory
owner: deploy
mode: "0750"
- name: Copier le fichier Compose
ansible.builtin.copy:
src: compose.yaml
dest: /opt/sorbier-frais/compose.yaml
owner: deploy
mode: "0640"
GitOps pousse l'idée jusqu'au bout : un dépôt Git décrit l'état voulu de l'infrastructure, et un agent installé dans l'environnement (Argo CD, Flux pour Kubernetes) le compare en permanence à l'état réel et corrige les écarts. On ne déploie plus en se connectant au serveur : on fusionne une pull request.
En SISR, ces outils prolongent les scripts que tu écris en Bash et en PowerShell (scripting) : la différence est qu'ils décrivent un état à atteindre plutôt qu'une suite d'ordres, et qu'on peut les rejouer sans risque.
Exercices
Exercice 1 sur 4 Quel environnement ? Réussi À revoir
Dans quel environnement chaque activité doit-elle avoir lieu ?
- La comptable de Sorbier Logistique vérifie que le nouvel écran de validation des notes de frais correspond à sa demande.
- Le pipeline lance les tests d'intégration contre une base MySQL.
- Lina essaie une requête SQL pour un nouvel export.
- L'équipe d'exploitation répète la migration de la base sur un volume de données identique à celui de la production, avant le grand jour.
- Les 140 salariés saisissent leurs notes de frais.
Voir la réponseMasquer la réponse de l'exercice 1
- Recette : validation fonctionnelle par une utilisatrice référente, sur des données fictives ou anonymisées.
- Intégration (CI) : une base jetable dans un conteneur du job.
- Développement : son poste, avec un jeu de données fictif.
- Préproduction : mêmes conditions que la production, données anonymisées.
- Production.
Tu avais trouvé ?
Exercice 2 sur 4 Un déploiement approuvé Réussi À revoir
Écris un job deploy-production qui :
- attend la réussite du job
deploy-recette; - ne tourne que pour un
pushsurmain; - utilise l'environnement GitHub
production(adressehttps://frais.sorbier.example), configuré avec un relecteur obligatoire ; - appelle le script
./scripts/deployer.shavec le secret d'environnementPROD_SSH_KEYpassé dans la variableSSH_KEY; - ne peut jamais tourner en double.
Voir la réponseMasquer la réponse de l'exercice 2
deploy-production:
needs: deploy-recette
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: production
url: https://frais.sorbier.example
concurrency:
group: deploiement-production
cancel-in-progress: false
steps:
- uses: actions/checkout@v7
- name: Déployer
env:
SSH_KEY: ${{ secrets.PROD_SSH_KEY }}
run: ./scripts/deployer.sh
Le relecteur obligatoire ne s'écrit pas dans le YAML : il se règle dans Settings, Environments, production. Le checkout est nécessaire, car le script est un fichier du dépôt. Le secret n'est transmis qu'après l'approbation.
Tu avais trouvé ?
Exercice 3 sur 4 Renommer une colonne sans casser Réussi À revoir
La table note a une colonne montant. La version 3.0 doit l'appeler montant_ttc. Le déploiement se fait sans interruption : pendant une minute, l'ancienne et la nouvelle version de l'API tournent ensemble. Décris les étapes, réparties sur plusieurs versions, avec la méthode expansion puis contraction.
Voir la réponseMasquer la réponse de l'exercice 3
- Version 2.9 (expansion) : une migration ajoute la colonne
montant_ttc, facultative, et copie les valeurs demontant. Le code écrit dans les deux colonnes et lit encoremontant. L'ancienne version 2.8 fonctionne toujours, puisquemontantexiste. - Version 3.0 : le code lit et écrit
montant_ttc(et continue d'écriremontant, pour qu'un retour arrière vers la 2.9 reste possible). - Version 3.1 (contraction), une fois la 3.0 stable et le retour arrière vers la 2.x écarté : le code n'écrit plus
montant, puis une migration supprime la colonne.
À aucun moment une version en service ne lit une colonne absente.
Tu avais trouvé ?
Exercice 4 sur 4 Idempotent ou pas ? Réussi À revoir
Ces deux façons d'ajouter une ligne au fichier de configuration d'un serveur sont-elles idempotentes ? Que se passe-t-il si on les exécute trois fois ?
- La commande
echo "LOG_LEVEL=info" >> /opt/sorbier-frais/.env - La tâche Ansible
ansible.builtin.lineinfileavecpath: /opt/sorbier-frais/.envetline: LOG_LEVEL=info
Voir la réponseMasquer la réponse de l'exercice 4
- Non :
>>ajoute la ligne à chaque exécution. Après trois exécutions, le fichier contient trois foisLOG_LEVEL=info. - Oui :
lineinfilevérifie si la ligne existe et ne l'ajoute que si elle manque. Après trois exécutions, la ligne est présente une seule fois ; Ansible indiquechangedla première fois, puisok.
C'est tout l'intérêt de l'infrastructure as code : on peut rejouer la description autant de fois qu'on veut, elle amène le serveur dans l'état décrit sans effet de bord.
Tu avais trouvé ?