09 — Gestion des environnements#

La gestion rigoureuse des environnements est l’une des disciplines fondamentales du DevOps. Un pipeline qui déploie du code non testé en production, ou dont le staging ne ressemble pas à la prod, génère des incidents évitables et érode la confiance dans le système de déploiement. Ce chapitre couvre les patterns éprouvés pour maintenir des environnements cohérents, reproductibles et contrôlés.

Le modèle dev / staging / prod#

Rôles de chaque environnement#

Un projet mature distingue au minimum trois environnements avec des responsabilités clairement séparées :

Développement (dev) L’environnement de développement est personnel ou partagé par une équipe restreinte. Il tolère l’instabilité : bases de données éphémères, mocks de services externes, données synthétiques. L’objectif est la vélocité, pas la fiabilité.

Staging (pré-production) Le staging est le miroir de la production. Il doit recevoir les mêmes versions de services, la même topologie réseau, les mêmes volumes de données représentatifs (anonymisés). C’est ici que les tests d’intégration, de charge et de smoke tests s’exécutent avant toute promotion. Un staging non représentatif de la prod est plus dangereux qu’utile : il génère une fausse confiance.

Production (prod) L’environnement qui sert les utilisateurs réels. Toute modification doit être validée en staging. Le principe de moindre surprise s’applique : rien ne doit apparaître en prod sans avoir transité par le pipeline complet.

Parité staging / prod#

Les divergences entre staging et prod sont la source principale des « works on staging, breaks in prod ». Les causes communes :

  • Versions de services différentes (BDD, cache, broker de messages)

  • Ressources allouées non représentatives (staging sur 1 nœud, prod sur 10)

  • Données de test trop simplistes (pas de cas limites)

  • Variables d’environnement manquantes ou incorrectes

  • Certificats TLS autosignés en staging vs certificats valides en prod

La parité se maintient par l’Infrastructure as Code : si staging et prod sont décrites dans le même code Terraform/Helm avec des paramètres différents uniquement pour la taille et les secrets, la dérive structurelle est impossible.

Promotion d’artefacts : « build once, deploy many »#

Le principe#

L’artefact (image Docker, bundle JAR, archive npm) doit être construit une seule fois et promu à travers les environnements. Reconstruire en staging puis en prod introduit un risque : deux builds du même commit peuvent produire des binaires légèrement différents si les dépendances transitives ont changé entre-temps.

Git commit SHA → build → image:sha256-abc123
                             │
                    push registry
                             │
              ┌──────────────┼──────────────┐
              ▼              ▼              ▼
            dev           staging         prod
         (same image)  (same image)  (same image)

L’image Docker taguée avec le SHA du commit est la source de vérité. Les tags latest ou main ne doivent jamais être utilisés en déploiement automatisé : ils masquent quelle version exacte tourne.

Registre et promotion#

# Workflow de promotion — GitHub Actions
on:
  workflow_dispatch:
    inputs:
      image_tag:
        description: "SHA de l'image à promouvoir"
        required: true
      target_env:
        type: choice
        options: [staging, prod]

jobs:
  promote:
    runs-on: ubuntu-latest
    steps:
      - name: Re-tag image pour l'environnement cible
        run: |
          docker pull ghcr.io/myorg/myapp:${{ inputs.image_tag }}
          docker tag ghcr.io/myorg/myapp:${{ inputs.image_tag }} \
            ghcr.io/myorg/myapp:${{ inputs.target_env }}-stable
          docker push ghcr.io/myorg/myapp:${{ inputs.target_env }}-stable

Immuabilité des artefacts

Ne jamais modifier une image après sa construction. Si un correctif est nécessaire, créer un nouveau build avec un nouveau SHA. L’immuabilité garantit la traçabilité : on peut toujours savoir exactement ce qui tourne en prod.

Hide code cell source

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
import numpy as np
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

fig, ax = plt.subplots(figsize=(12, 5))
ax.set_xlim(0, 12)
ax.set_ylim(0, 6)
ax.axis("off")
ax.set_title("Promotion d'artefacts : build once, deploy many", fontsize=14, fontweight="bold", pad=20)

# Couleurs
c_build   = "#4C72B0"
c_reg     = "#DD8452"
c_dev     = "#55A868"
c_staging = "#C44E52"
c_prod    = "#8172B2"

def draw_box(ax, x, y, w, h, label, sublabel, color):
    box = FancyBboxPatch((x, y), w, h, boxstyle="round,pad=0.1",
                          facecolor=color, edgecolor="white", linewidth=2, alpha=0.88)
    ax.add_patch(box)
    ax.text(x + w/2, y + h*0.62, label, ha="center", va="center",
            fontsize=10, fontweight="bold", color="white")
    ax.text(x + w/2, y + h*0.25, sublabel, ha="center", va="center",
            fontsize=8, color="white", style="italic")

def arrow(ax, x1, y1, x2, y2):
    ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
                arrowprops=dict(arrowstyle="->", color="#444444", lw=2))

# Git + Build
draw_box(ax, 0.3, 2.5, 1.8, 1.2, "Git commit", "sha: abc123", c_build)
draw_box(ax, 2.6, 2.5, 1.8, 1.2, "CI Build", "docker build", c_build)
draw_box(ax, 5.0, 2.5, 2.0, 1.2, "Registry", "image:abc123", c_reg)

# Envs
draw_box(ax, 8.0, 4.2, 1.6, 1.0, "Dev", "auto-déployé", c_dev)
draw_box(ax, 8.0, 2.6, 1.6, 1.0, "Staging", "promotion manuelle", c_staging)
draw_box(ax, 8.0, 1.0, 1.6, 1.0, "Prod", "promotion validée", c_prod)

# Flèches
arrow(ax, 2.1, 3.1, 2.6, 3.1)
arrow(ax, 4.4, 3.1, 5.0, 3.1)
# Registry → envs
arrow(ax, 7.0, 3.5, 8.0, 4.7)
arrow(ax, 7.0, 3.1, 8.0, 3.1)
arrow(ax, 7.0, 2.7, 8.0, 1.5)

# Labels flèches
ax.text(5.95, 4.35, "même image", fontsize=8, color="#666666", rotation=25)
ax.text(7.05, 3.2,  "même image", fontsize=8, color="#666666")
ax.text(5.95, 1.85, "même image", fontsize=8, color="#666666", rotation=-25)

plt.savefig("_static/09_promotion_artefacts.png", dpi=120, bbox_inches="tight")
plt.show()
_images/e56492d7c140da15ac61efa4993748ed3e6b7589fed562530949d4bc70575b76.png

Les 12 facteurs appliqués à la configuration#

L’application des 12 Factor App reste la référence pour les applications cloud-native. Les facteurs les plus pertinents pour la gestion d’environnements sont :

Facteur III — Configuration Toute configuration qui varie entre les environnements (URLs de bases de données, clés API, flags de fonctionnalités) doit provenir de variables d’environnement, jamais du code source. Cela inclut les modes debug, les timeouts, les seuils.

Facteur IV — Services externes (Backing services) Traiter les bases de données, queues, caches comme des ressources attachées via URL/credentials. Passer de MySQL local à RDS en production ne doit nécessiter qu’un changement de variable d’environnement.

Facteur X — Parité dev/prod Minimiser les écarts entre dev et prod : temps (déployer souvent), personnes (les devs déploient), outils (même stack).

ConfigMaps Kubernetes#

# configmap-prod.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp-config
  namespace: production
data:
  DATABASE_POOL_SIZE: "20"
  CACHE_TTL_SECONDS: "300"
  LOG_LEVEL: "warn"
  FEATURE_NEW_DASHBOARD: "false"
  API_RATE_LIMIT: "1000"
---
# configmap-staging.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp-config
  namespace: staging
data:
  DATABASE_POOL_SIZE: "5"
  CACHE_TTL_SECONDS: "60"
  LOG_LEVEL: "debug"
  FEATURE_NEW_DASHBOARD: "true"
  API_RATE_LIMIT: "100"

Secrets et SOPS#

Les secrets ne doivent jamais apparaître en clair dans Git. SOPS (Secrets OPerationS) chiffre les valeurs sensibles tout en laissant le fichier versionnable :

# Chiffrer avec une clé Age ou KMS
sops --encrypt --age age1abc123... secrets.yaml > secrets.enc.yaml

# Déchiffrer dans le pipeline CI
sops --decrypt secrets.enc.yaml | kubectl apply -f -

Helm values par environnement#

# values-prod.yaml
replicaCount: 3

image:
  repository: ghcr.io/myorg/myapp
  tag: "sha256-abc123"  # épinglé, jamais "latest"
  pullPolicy: IfNotPresent

resources:
  requests:
    cpu: "500m"
    memory: "512Mi"
  limits:
    cpu: "2000m"
    memory: "2Gi"

autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 20

ingress:
  enabled: true
  host: api.myapp.fr
  tls: true

config:
  logLevel: warn
  dbPoolSize: 20

postgresql:
  enabled: false         # RDS externe en prod
  externalHost: prod-db.rds.amazonaws.com

Feature flags#

Définition et cas d’usage#

Un feature flag (ou feature toggle) est un mécanisme qui permet d’activer ou désactiver une fonctionnalité sans déployer de nouveau code. Les cas d’usage principaux :

  • Déploiement progressif : activer pour 5% des utilisateurs, observer, étendre

  • Trunk-based development : fusionner du code incomplet caché derrière un flag

  • Tests A/B : exposer deux variantes à des segments d’utilisateurs

  • Kill switch : désactiver d’urgence une fonctionnalité défaillante sans rollback

  • Canary de fonctionnalité : activer uniquement pour les utilisateurs internes d’abord

Implémentation Python pédagogique#

import hashlib
import random
from dataclasses import dataclass, field
from typing import Optional

@dataclass
class FeatureFlag:
    """
    Feature flag avec rollout progressif basé sur le hash de l'identifiant utilisateur.
    Garantit la cohérence : le même utilisateur voit toujours le même résultat.
    """
    name: str
    enabled: bool = False
    rollout_percentage: float = 0.0          # 0.0 → 100.0
    allowed_user_ids: list = field(default_factory=list)  # liste blanche explicite
    environment_overrides: dict = field(default_factory=dict)

    def is_active(self, user_id: str, environment: str = "prod") -> bool:
        """Détermine si le flag est actif pour un utilisateur donné."""
        # Override par environnement (ex: toujours actif en staging)
        if environment in self.environment_overrides:
            return self.environment_overrides[environment]

        if not self.enabled:
            return False

        # Liste blanche explicite
        if user_id in self.allowed_user_ids:
            return True

        # Rollout déterministe basé sur le hash
        if self.rollout_percentage >= 100.0:
            return True
        if self.rollout_percentage <= 0.0:
            return False

        # Hachage stable : même user_id → même bucket → même résultat
        bucket = int(hashlib.md5(f"{self.name}:{user_id}".encode()).hexdigest(), 16) % 100
        return bucket < self.rollout_percentage


# --- Registre de flags ---
class FeatureFlagRegistry:
    def __init__(self):
        self._flags: dict[str, FeatureFlag] = {}

    def register(self, flag: FeatureFlag):
        self._flags[flag.name] = flag

    def is_active(self, flag_name: str, user_id: str, environment: str = "prod") -> bool:
        flag = self._flags.get(flag_name)
        if flag is None:
            return False  # flag inconnu = désactivé par défaut
        return flag.is_active(user_id, environment)


# Démonstration
registry = FeatureFlagRegistry()
registry.register(FeatureFlag(
    name="new_dashboard",
    enabled=True,
    rollout_percentage=20.0,
    environment_overrides={"staging": True, "dev": True}
))
registry.register(FeatureFlag(
    name="ai_recommendations",
    enabled=True,
    rollout_percentage=5.0,
    allowed_user_ids=["user_beta_001", "user_beta_002"]
))

# Simuler 1000 utilisateurs
users = [f"user_{i:04d}" for i in range(1000)]

for flag_name in ["new_dashboard", "ai_recommendations"]:
    active_count = sum(
        1 for u in users if registry.is_active(flag_name, u, "prod")
    )
    print(f"[{flag_name}] actif pour {active_count}/1000 utilisateurs en prod "
          f"({active_count/10:.1f}%)")

# Vérification cohérence : même user → même résultat
u = "user_0042"
results = [registry.is_active("new_dashboard", u, "prod") for _ in range(5)]
print(f"\nCohérence pour {u} : {results}  (doit être constant)")
[new_dashboard] actif pour 194/1000 utilisateurs en prod (19.4%)
[ai_recommendations] actif pour 51/1000 utilisateurs en prod (5.1%)

Cohérence pour user_0042 : [False, False, False, False, False]  (doit être constant)

LaunchDarkly et Unleash

Pour les projets d’envergure, des plateformes comme LaunchDarkly ou Unleash (open-source) offrent une interface graphique, des règles de ciblage avancées, des audits et des intégrations avec les outils de monitoring. L’implémentation maison suffit pour débuter, mais devient rapidement un fardeau de maintenance au-delà d’une vingtaine de flags.

Épinglage de version#

Pourquoi épingler ?#

La reproductibilité est le fondement de la fiabilité. Un déploiement doit produire exactement le même résultat aujourd’hui et dans six mois. Cela impose d’épingler :

  • Images Docker : image: nginx:1.25.3 et non nginx:latest

  • Dépendances Python : requirements.txt avec versions exactes (pip freeze), ou pyproject.toml avec contraintes + uv.lock

  • Charts Helm : version: 1.4.2 dans Chart.yaml et dans helmfile.yaml

  • Modules Terraform : source = "hashicorp/kubernetes" version = "~> 2.25"

  • Actions GitHub : uses: actions/checkout@v4.1.1 (et non @v4)

SHA plutôt que tag pour les actions critiques

Un tag Git peut être réécrit. Pour les workflows CI/CD critiques, épingler sur le SHA du commit : uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 Cela immunise contre les attaques de supply chain sur les dépendances GitHub Actions.

Dérive de configuration (configuration drift)#

Causes et risques#

La dérive (drift) se produit quand l’état réel d’un environnement diverge de l’état décrit dans le code. Causes fréquentes :

  • Modifications manuelles « d’urgence » en production sans mise à jour du code IaC

  • Mises à jour automatiques de packages OS non maîtrisées

  • Opérateurs humains qui ajustent des paramètres directement via kubectl/console AWS

  • Exécution partielle d’un playbook Ansible interrompue

La dérive est insidieuse : les environnements fonctionnent, mais la prochaine application du code IaC peut tout casser.

Détection#

  • terraform plan : signale toute différence entre l’état souhaité et l’état réel

  • kubectl diff -f manifests/ : diff des manifestes K8s vs cluster

  • ansible --check : dry-run qui liste les changements qui seraient appliqués

  • Outils spécialisés : Driftctl (cloud resources vs Terraform), Config Connector (GCP)

Prévention#

La stratégie la plus efficace est d’interdire les modifications manuelles :

  • Droits IAM en lecture seule sur la prod pour tous sauf le pipeline CI/CD

  • Alertes sur toute modification hors pipeline (CloudTrail, audit logs K8s)

  • GitOps (voir chapitre 22) : l’état du cluster est en permanence réconcilié avec Git

import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

# Modèle probabiliste de divergence de configuration dans le temps
# Hypothèse : chaque modification manuelle augmente la dérive
# Les runs IaC (terraform apply) la réduisent (réconciliation)

np.random.seed(42)
days = np.arange(0, 91)  # 3 mois

def simulate_drift(iac_apply_days, manual_change_rate=0.15, drift_reduction=0.7):
    """
    Simule la dérive de configuration sur 90 jours.
    - iac_apply_days : jours où un `terraform apply` est exécuté (reset partiel)
    - manual_change_rate : probabilité journalière de modification manuelle
    """
    drift = np.zeros(len(days))
    for i in range(1, len(days)):
        # Accumulation de dérive par modifications manuelles
        manual = np.random.binomial(1, manual_change_rate) * np.random.uniform(0.5, 2.0)
        drift[i] = drift[i-1] + manual

        # Réconciliation par IaC
        if i in iac_apply_days:
            drift[i] *= (1 - drift_reduction)

        drift[i] = max(0, drift[i])
    return drift

# Scénario 1 : Sans discipline IaC (apply rare)
drift_no_iac = simulate_drift(iac_apply_days=[30, 60, 90], manual_change_rate=0.18)

# Scénario 2 : IaC hebdomadaire
drift_weekly = simulate_drift(
    iac_apply_days=list(range(7, 91, 7)), manual_change_rate=0.18
)

# Scénario 3 : GitOps continu (réconciliation quasi-quotidienne)
drift_gitops = simulate_drift(
    iac_apply_days=list(range(1, 91)), manual_change_rate=0.10
)

fig, ax = plt.subplots(figsize=(11, 5))

ax.plot(days, drift_no_iac, label="Sans IaC régulier (apply mensuel)", color="#C44E52", lw=2.2)
ax.plot(days, drift_weekly,  label="IaC hebdomadaire",                  color="#DD8452", lw=2.2)
ax.plot(days, drift_gitops,  label="GitOps continu",                    color="#55A868", lw=2.2)

# Marquer les apply manuels (scénario 1)
for d in [30, 60, 90]:
    ax.axvline(x=d, color="#C44E52", linestyle="--", alpha=0.4, lw=1)

ax.set_xlabel("Jours depuis le dernier état clean")
ax.set_ylabel("Indice de dérive de configuration (u.a.)")
ax.set_title("Évolution de la dérive de configuration selon la stratégie IaC", fontsize=13)
ax.legend(loc="upper left")
ax.set_xlim(0, 90)
ax.set_ylim(bottom=0)

plt.savefig("_static/09_drift_configuration.png", dpi=120, bbox_inches="tight")
plt.show()
_images/6573d2041adb5dad40e71b88a10f777f96623069f7294d176eb89caa9c502ea1.png

Stratégie de branches et d’environnements#

Le mapping branche ↔ environnement automatise la promotion et réduit les ambiguïtés :

Branche

Environnement

Déclencheur de déploiement

feature/*

Éphémère (preview env)

Push → déploiement auto

main

Dev

Merge → déploiement auto

release/*

Staging

Création de tag RC → déploiement auto

v*.*.* (tag)

Prod

Tag annoté → déploiement après approbation

Environnements éphémères (Preview environments)#

Les environnements éphémères (preview apps chez Heroku/Render, Review Apps chez GitLab) créent un environnement complet pour chaque pull request. Avantages :

  • Tests d’intégration isolés par feature

  • Validation UX sur URL partageable avant merge

  • Destruction automatique à la fermeture de la PR (économies cloud)

Coût des environnements éphémères

Sur un projet avec 20 développeurs actifs, les preview environments peuvent multiplier les coûts cloud par 3 à 5. Il faut implémenter des mécanismes de mise en veille automatique (scale to zero avec Knative ou Karpenter) et des TTL stricts.

Résumé#

  1. Les trois environnements (dev, staging, prod) ont des rôles distincts ; la parité staging/prod est non négociable et s’obtient par l’IaC.

  2. Le principe « build once, deploy many » garantit que l’artefact promu en prod est identique à celui testé en staging — les images Docker sont taguées avec le SHA du commit.

  3. Les 12 facteurs, en particulier le facteur III (config via variables d’environnement) et le facteur X (parité dev/prod), structurent la gestion de configuration.

  4. Les ConfigMaps Kubernetes externalisent la configuration non sensible ; SOPS chiffre les secrets tout en les versionnant dans Git.

  5. Les feature flags permettent de séparer le déploiement de l’activation : trunk-based development, rollout progressif, kill switch d’urgence.

  6. L’implémentation d’un feature flag déterministe (hash de l’identifiant) garantit la cohérence de l’expérience utilisateur pendant le rollout.

  7. L’épinglage de version (images, dépendances, modules IaC, actions CI) est le fondement de la reproductibilité des builds et déploiements.

  8. La dérive de configuration est inévitable sans discipline : terraform plan, kubectl diff et le GitOps permettent de la détecter et de la prévenir.

  9. Le mapping branche ↔ environnement automatise la promotion et clarifie le flux de livraison ; les environnements éphémères par PR amplifient l’isolation des tests.

  10. La convergence vers le GitOps (chapitre 22) est la conclusion naturelle de la gestion d’environnements : l’état souhaité est dans Git, la réconciliation est continue et automatique.