Bonnes pratiques et production#

Vous avez maintenant une vision complète de Docker et Kubernetes. Ce dernier chapitre synthétise les bonnes pratiques qui distinguent une application « qui tourne » d’une application « prête pour la production ». Du Dockerfile au cluster multi-zones, en passant par le sizing des ressources, les probes et la gestion des coûts.

Hide code cell source

import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
import numpy as np
import pandas as pd
import seaborn as sns
import math
import random
from dataclasses import dataclass, field
from typing import List, Optional, Dict

sns.set_theme(style="whitegrid", palette="muted")
plt.rcParams.update({
    "figure.dpi": 120,
    "font.family": "DejaVu Sans",
    "axes.titlesize": 13,
    "axes.labelsize": 11,
})
random.seed(42)
np.random.seed(42)

Checklist Dockerfile de production#

Hide code cell source

fig, ax = plt.subplots(figsize=(13, 7))
ax.set_xlim(0, 13)
ax.set_ylim(0, 8)
ax.axis("off")
ax.set_title("Checklist Dockerfile — Prêt pour la production", fontsize=13, fontweight="bold", pad=12)

categories = [
    {
        "titre": "Taille & Performance",
        "x": 2.2, "y": 6.5, "color": "#e3f2fd", "ec": "#1565c0",
        "items": [
            ("Image de base minimale (slim ou Alpine)", True),
            ("Multi-stage build si compilation", True),
            (".dockerignore configuré", True),
            ("RUN combinés avec &&", True),
            ("--no-cache-dir pip / --no-cache apt", True),
        ]
    },
    {
        "titre": "Sécurité",
        "x": 6.5, "y": 6.5, "color": "#e8f5e9", "ec": "#2e7d32",
        "items": [
            ("Utilisateur non-root (USER appuser)", True),
            ("Pas de secrets dans ENV ou ARG", True),
            ("Secrets BuildKit (--secret)", True),
            ("Image de base mise à jour", True),
            ("Scanning CVE (Trivy) en CI/CD", True),
        ]
    },
    {
        "titre": "Fiabilité",
        "x": 10.8, "y": 6.5, "color": "#fff3e0", "ec": "#e65100",
        "items": [
            ("HEALTHCHECK configuré", True),
            ("ENTRYPOINT + CMD distincts", True),
            ("WORKDIR explicite", True),
            ("Version d'image épinglée (pas :latest)", True),
            ("LABEL avec métadonnées (auteur, version)", False),
        ]
    },
]

for cat in categories:
    x, y = cat["x"], cat["y"]
    # Boîte titre
    tb = FancyBboxPatch((x - 2.0, y - 0.3), 4.0, 0.7, boxstyle="round,pad=0.08",
                         facecolor=cat["ec"], edgecolor="white", linewidth=0)
    ax.add_patch(tb)
    ax.text(x, y + 0.05, cat["titre"], ha="center", va="center",
            fontsize=10, fontweight="bold", color="white")

    # Items
    for i, (item, ok) in enumerate(cat["items"]):
        iy = y - 0.65 - i * 0.55
        symbol = "✓" if ok else "○"
        color_sym = "#2e7d32" if ok else "#9e9e9e"
        item_bg = FancyBboxPatch((x - 2.0, iy - 0.2), 4.0, 0.45,
                                  boxstyle="round,pad=0.04",
                                  facecolor=cat["color"], edgecolor="#e0e0e0", linewidth=0.5)
        ax.add_patch(item_bg)
        ax.text(x - 1.8, iy + 0.02, symbol, ha="center", va="center",
                fontsize=11, color=color_sym, fontweight="bold")
        ax.text(x - 1.5, iy + 0.02, item, ha="left", va="center",
                fontsize=8, color="#37474f")

# Exemple de bon Dockerfile résumé
ax.text(6.5, 0.6,
        "Dockerfile minimal de production : FROM slim → RUN useradd → COPY req.txt → pip install "
        "→ COPY . → USER → HEALTHCHECK → CMD",
        ha="center", va="center", fontsize=8.5, color="#1b5e20",
        bbox=dict(boxstyle="round,pad=0.3", facecolor="#e8f5e9", edgecolor="#2e7d32"))

plt.tight_layout()
plt.savefig("_static/20_checklist_dockerfile.png", dpi=130, bbox_inches="tight")
plt.show()
_images/647642e099a6bdad5a2d0d88e95881ebec93618dd1158c86e795594936ab0c78.png

Les probes en détail : liveness, readiness, startup#

Les trois types de probes répondent à des questions différentes et ont des conséquences différentes en cas d’échec.

Probe

Question

Action si échec

Quand configurer

liveness

« Mon app est-elle encore vivante ? »

Redémarrer le conteneur

Toujours

readiness

« Mon app est-elle prête à recevoir du trafic ? »

Retirer du Service (load balancer)

Toujours

startup

« Mon app a-t-elle fini de démarrer ? »

Bloquer liveness et readiness

Apps lentes au démarrage

# Exemple complet des 3 probes
spec:
  containers:
    - name: app
      image: mon-app:1.0

      # Startup Probe : protège les apps lentes au démarrage
      # liveness et readiness sont DÉSACTIVÉES tant que startup n'est pas OK
      startupProbe:
        httpGet:
          path: /startup
          port: 8080
        failureThreshold: 30      # 30 × 10s = 5 minutes max pour démarrer
        periodSeconds: 10
        # Si échec après 30 tentatives → conteneur redémarré

      # Liveness Probe : détecte les deadlocks, états corrompus
      livenessProbe:
        httpGet:
          path: /healthz          # Réponse simple (200 OK)
          port: 8080
        initialDelaySeconds: 0    # Attendu que startupProbe réussisse
        periodSeconds: 30
        failureThreshold: 3       # 3 échecs consécutifs → restart
        timeoutSeconds: 5

      # Readiness Probe : détecte quand l'app n'est pas prête
      # (ex: base de données non connectée, cache non chargé)
      readinessProbe:
        httpGet:
          path: /ready             # Vérifications plus complètes
          port: 8080
        initialDelaySeconds: 0
        periodSeconds: 10          # Plus fréquent que liveness
        failureThreshold: 3
        successThreshold: 1        # 1 succès suffit pour revenir dans le LB
        timeoutSeconds: 3

Endpoints /healthz vs /ready — que vérifier ?

  • /healthz (liveness) : vérification légère — « le processus répond-il ? » Un 200 OK simple suffit. Ne pas vérifier les dépendances externes ici (sinon un problème de BDD redémarre tous vos Pods en boucle !).

  • /ready (readiness) : vérification des dépendances — « puis-je traiter des requêtes ? » Vérifier la connexion à la BDD, au cache, la disponibilité des fichiers de config…

Hide code cell source

fig, ax = plt.subplots(figsize=(14, 6))
ax.set_xlim(0, 14)
ax.set_ylim(0, 7)
ax.axis("off")
ax.set_title("Cycle de vie des probes au démarrage et en fonctionnement",
             fontsize=12, fontweight="bold", pad=12)

# Timeline
total_t = 180  # secondes
ax.axhline(y=1.2, xmin=0.05, xmax=0.95, color="#bdbdbd", linewidth=2)
ax.text(0.3, 1.2, "t=0", ha="center", va="center", fontsize=8, color="#9e9e9e")
ax.text(13.7, 1.2, f"t={total_t}s", ha="center", va="center", fontsize=8, color="#9e9e9e")

# Phases temporelles
phases = [
    (0, 50, "Démarrage\n(startup probe actif)", "#e8eaf6", "#3949ab"),
    (50, 80, "Prêt !\n(startup OK)", "#e8f5e9", "#2e7d32"),
    (80, 120, "Fonctionnement normal", "#e8f5e9", "#2e7d32"),
    (120, 145, "Problème readiness\n(BDD lente)", "#fff3e0", "#e65100"),
    (145, 165, "Retour à la\nnormale", "#e8f5e9", "#2e7d32"),
    (165, 180, "Fonctionnement\nnormal", "#e8f5e9", "#2e7d32"),
]

for t_start, t_end, label, fc, ec in phases:
    x1 = 0.5 + t_start / total_t * 13
    x2 = 0.5 + t_end / total_t * 13
    b = FancyBboxPatch((x1, 1.6), x2 - x1 - 0.1, 0.8,
                        boxstyle="round,pad=0.04", facecolor=fc, edgecolor=ec, linewidth=1.5)
    ax.add_patch(b)
    ax.text((x1 + x2) / 2, 2.0, label, ha="center", va="center",
            fontsize=7.5, color=ec, fontweight="bold", linespacing=1.3)

# Probes (3 lignes)
probes_config = [
    ("Startup Probe", 3.5, 0, 50, True, "#3949ab"),
    ("Liveness Probe", 4.6, 50, 180, True, "#c62828"),
    ("Readiness Probe", 5.7, 50, 180, True, "#2e7d32"),
]

for nom, y, t_start, t_end, actif, color in probes_config:
    x1 = 0.5 + t_start / total_t * 13
    x2 = 0.5 + t_end / total_t * 13
    ax.text(0.3, y, nom, ha="left", va="center", fontsize=8.5,
            fontweight="bold", color=color)

    if actif:
        ax.plot([x1, x2], [y, y], color=color, linewidth=3, alpha=0.4)

        # Points de vérification
        ticks_start = t_start
        period = 10 if "Startup" in nom else (30 if "Liveness" in nom else 10)
        t = ticks_start + period
        while t <= t_end:
            xt = 0.5 + t / total_t * 13
            if "Readiness" in nom and 120 <= t <= 145:
                # Échec readiness
                marker = "x"
                c = "#f44336"
                s = 80
            elif "Startup" in nom and t > 50:
                break
            else:
                marker = "o"
                c = color
                s = 40
            ax.scatter([xt], [y], marker=marker, color=c, s=s, zorder=5)
            t += period

# Événements clés
events = [
    (50, "App démarrée\n(startup OK)", "#2e7d32"),
    (120, "BDD lente\n(readiness fail)", "#e65100"),
    (145, "BDD récupérée\n(readiness OK)", "#2e7d32"),
]
for t, label, color in events:
    xt = 0.5 + t / total_t * 13
    ax.axvline(x=xt, ymin=0.28, ymax=0.9, color=color, linewidth=1.5, linestyle="--", alpha=0.7)
    ax.text(xt, 6.4, label, ha="center", va="center", fontsize=7.5, color=color,
            bbox=dict(boxstyle="round,pad=0.2", facecolor="white", edgecolor=color, alpha=0.9))

# Conséquences
ax.text(7.0, 0.5,
        "readiness=fail → Pod retiré du Service (trafic redirigé) mais PAS redémarré\n"
        "liveness=fail × 3 → conteneur redémarré (kubectl get events pour voir)",
        ha="center", va="center", fontsize=8.5, color="#37474f",
        bbox=dict(boxstyle="round,pad=0.3", facecolor="#fffde7", edgecolor="#f9a825"))

plt.tight_layout()
plt.savefig("_static/20_probes_lifecycle.png", dpi=130, bbox_inches="tight")
plt.show()
_images/1e576006f65fe21738df5e236f70b2df1acae35242328668082690cd0615511a.png

Resource sizing : méthode pour estimer requests et limits#

L’un des problèmes les plus courants en production est le mauvais sizing des ressources : trop peu (OOM, throttling), trop (gaspillage de coûts).

Méthode recommandée#

  1. Déployer en mode BestEffort (sans requests ni limits) pendant quelques heures en staging

  2. Observer via kubectl top pods ou Prometheus

  3. Appliquer VPA en mode Off : il recommande des valeurs basées sur l’utilisation réelle

  4. Configurer requests = P95 d’utilisation normale, limits = 2× ou 3× les requests

  5. Activer HPA pour absorber les pics

# Observer l'utilisation des ressources
kubectl top pods -n production
# NAME              CPU(cores)   MEMORY(bytes)
# api-7d4f9-x8k2    83m          145Mi
# api-7d4f9-p3l1    91m          158Mi
# api-7d4f9-nh2v    78m          142Mi

# VPA recommandation (après quelques heures d'observation)
kubectl describe vpa mon-api-vpa
# Target CPU : 250m   (recommandé comme requests)
# Target Mem : 200Mi

# Règle empirique :
# requests.cpu  = utilisation P95 en conditions normales
# limits.cpu    = 2× à 4× les requests (burst toléré)
# requests.mem  = utilisation P99 (la mémoire ne se throttle pas, elle OOM-kill)
# limits.mem    = 1.2× à 1.5× requests (marge, mais pas trop pour éviter OOM loop)

Checklist Pod/Deployment de production#

Hide code cell source

fig, ax = plt.subplots(figsize=(13, 7))
ax.set_xlim(0, 13)
ax.set_ylim(0, 8)
ax.axis("off")
ax.set_title("Checklist Deployment Kubernetes — Prêt pour la production",
             fontsize=13, fontweight="bold", pad=12)

categories = [
    {
        "titre": "Ressources & QoS",
        "x": 2.2, "y": 6.8, "color": "#e3f2fd", "ec": "#1565c0",
        "items": [
            ("resources.requests définis", True),
            ("resources.limits définis", True),
            ("Ratio limits/requests raisonnable", True),
            ("QoS Guaranteed pour services critiques", False),
            ("VPA en mode Off pour monitoring", False),
        ]
    },
    {
        "titre": "Disponibilité",
        "x": 6.5, "y": 6.8, "color": "#e8f5e9", "ec": "#2e7d32",
        "items": [
            ("livenessProbe configuré", True),
            ("readinessProbe configuré", True),
            ("startupProbe si démarrage long", False),
            ("PodDisruptionBudget créé", True),
            ("minReplicas ≥ 2", True),
        ]
    },
    {
        "titre": "Placement & Sécurité",
        "x": 10.8, "y": 6.8, "color": "#fff3e0", "ec": "#e65100",
        "items": [
            ("topologySpreadConstraints zones", True),
            ("podAntiAffinity entre réplicas", False),
            ("SecurityContext runAsNonRoot", True),
            ("ServiceAccount dédié (non-default)", True),
            ("NetworkPolicy configurée", True),
        ]
    },
]

for cat in categories:
    x, y = cat["x"], cat["y"]
    tb = FancyBboxPatch((x - 2.0, y - 0.3), 4.0, 0.7, boxstyle="round,pad=0.08",
                         facecolor=cat["ec"], edgecolor="white", linewidth=0)
    ax.add_patch(tb)
    ax.text(x, y + 0.05, cat["titre"], ha="center", va="center",
            fontsize=10, fontweight="bold", color="white")

    for i, (item, critique) in enumerate(cat["items"]):
        iy = y - 0.65 - i * 0.55
        symbol = "★" if critique else "☆"
        color_sym = cat["ec"] if critique else "#9e9e9e"
        item_bg = FancyBboxPatch((x - 2.0, iy - 0.2), 4.0, 0.45,
                                  boxstyle="round,pad=0.04",
                                  facecolor=cat["color"], edgecolor="#e0e0e0", linewidth=0.5)
        ax.add_patch(item_bg)
        ax.text(x - 1.8, iy + 0.02, symbol, ha="center", va="center",
                fontsize=10, color=color_sym)
        ax.text(x - 1.5, iy + 0.02, item, ha="left", va="center", fontsize=8, color="#37474f")

ax.text(6.5, 0.5,
        "★ = Absolument requis en production   ☆ = Fortement recommandé",
        ha="center", va="center", fontsize=9, color="#546e7a", fontstyle="italic")

plt.tight_layout()
plt.savefig("_static/20_checklist_deployment.png", dpi=130, bbox_inches="tight")
plt.show()
_images/305acf4849c2344c53a7728236e7448935108916756d29be267b395884bf6d29.png

Gestion des configurations : l’approche 12-Factor#

La règle d’or de l’application 12-factor pour la configuration : séparer strictement la configuration du code.

# Ce qui va dans le code : comportement de l'application
# Ce qui va dans la config : tout ce qui change entre les environnements

# ConfigMap — pour les configurations non-sensibles
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  APP_ENV: "production"
  LOG_LEVEL: "info"
  MAX_WORKERS: "8"
  CACHE_TTL: "300"

---
# Secret — pour les données sensibles
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
stringData:              # stringData : Kubernetes encode en base64 automatiquement
  DATABASE_URL: "postgresql://user:password@postgres:5432/appdb"
  JWT_SECRET: "..."
  API_KEY: "..."
# Utilisation dans le Deployment
spec:
  containers:
    - name: app
      # Injecter tout le ConfigMap comme variables d'environnement
      envFrom:
        - configMapRef:
            name: app-config
        - secretRef:
            name: app-secrets

      # Ou sélectivement
      env:
        - name: LOG_LEVEL
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: LOG_LEVEL

Vérificateur de conformité d’un Deployment#

from dataclasses import dataclass, field
from typing import List, Dict, Optional

@dataclass
class Critere:
    nom: str
    description: str
    critique: bool        # True = requis, False = recommandé
    points: int

@dataclass
class ResultatAudit:
    critere: Critere
    reussi: bool
    detail: str = ""

    @property
    def points_obtenus(self) -> int:
        return self.critere.points if self.reussi else 0


class VerificateurDeployment:
    """
    Vérificateur de conformité d'un manifeste Deployment Kubernetes.
    Calcule un score de maturité de 0 à 100.
    """

    CRITERES = [
        # Ressources
        Critere("resources-requests",  "resources.requests définis pour CPU et mémoire", True, 8),
        Critere("resources-limits",    "resources.limits définis pour CPU et mémoire",   True, 8),
        # Probes
        Critere("liveness-probe",      "livenessProbe configuré",                         True, 7),
        Critere("readiness-probe",     "readinessProbe configuré",                        True, 7),
        Critere("startup-probe",       "startupProbe configuré",                          False, 3),
        # Sécurité
        Critere("non-root",            "securityContext.runAsNonRoot = true",             True, 8),
        Critere("no-privilege-esc",    "allowPrivilegeEscalation = false",                True, 7),
        Critere("readonly-fs",         "readOnlyRootFilesystem = true",                   False, 5),
        Critere("drop-caps",           "capabilities.drop = ALL",                         False, 5),
        Critere("service-account",     "ServiceAccount dédié (pas default)",              False, 4),
        # Disponibilité
        Critere("min-replicas",        "replicas >= 2",                                   True, 7),
        Critere("topology-spread",     "topologySpreadConstraints défini",                False, 5),
        Critere("image-tag",           "Version d'image épinglée (pas :latest)",          True, 6),
        # Configuration
        Critere("no-inline-secrets",   "Pas de secrets en clair dans env",                True, 8),
        Critere("labels",              "Labels app, version, environment présents",       False, 3),
        Critere("rolling-update",      "Stratégie RollingUpdate configurée",              False, 4),
        Critere("pdb",                 "PodDisruptionBudget associé (à vérifier sep.)",   False, 5),
    ]

    def verifier(self, manifeste: dict) -> List[ResultatAudit]:
        resultats = []
        spec = manifeste.get("spec", {})
        template = spec.get("template", {})
        pod_spec = template.get("spec", {})
        containers = pod_spec.get("containers", [])
        metadata = manifeste.get("metadata", {})
        labels = metadata.get("labels", {})

        # resources
        has_requests = all(c.get("resources", {}).get("requests") for c in containers)
        has_limits   = all(c.get("resources", {}).get("limits")   for c in containers)
        resultats.append(ResultatAudit(self._get("resources-requests"), has_requests,
                          "Tous les conteneurs ont des requests" if has_requests else
                          "Au moins un conteneur sans resources.requests"))
        resultats.append(ResultatAudit(self._get("resources-limits"), has_limits))

        # probes
        has_liveness  = all(c.get("livenessProbe")  for c in containers)
        has_readiness = all(c.get("readinessProbe") for c in containers)
        has_startup   = any(c.get("startupProbe")   for c in containers)
        resultats.append(ResultatAudit(self._get("liveness-probe"),  has_liveness))
        resultats.append(ResultatAudit(self._get("readiness-probe"), has_readiness))
        resultats.append(ResultatAudit(self._get("startup-probe"),   has_startup))

        # sécurité
        pod_ctx = pod_spec.get("securityContext", {})
        non_root = pod_ctx.get("runAsNonRoot", False)
        resultats.append(ResultatAudit(self._get("non-root"), non_root))

        no_priv = all(c.get("securityContext", {}).get("allowPrivilegeEscalation") is False
                      for c in containers)
        resultats.append(ResultatAudit(self._get("no-privilege-esc"), no_priv))

        readonly = all(c.get("securityContext", {}).get("readOnlyRootFilesystem", False)
                       for c in containers)
        resultats.append(ResultatAudit(self._get("readonly-fs"), readonly))

        drop_all = all(c.get("securityContext", {}).get("capabilities", {}).get("drop") == ["ALL"]
                       for c in containers)
        resultats.append(ResultatAudit(self._get("drop-caps"), drop_all))

        sa = pod_spec.get("serviceAccountName", "default")
        resultats.append(ResultatAudit(self._get("service-account"),
                          sa != "default" and sa != "",
                          f"ServiceAccount : {sa}"))

        # disponibilité
        replicas = spec.get("replicas", 1)
        resultats.append(ResultatAudit(self._get("min-replicas"), replicas >= 2,
                          f"replicas = {replicas}"))

        has_topology = bool(pod_spec.get("topologySpreadConstraints"))
        resultats.append(ResultatAudit(self._get("topology-spread"), has_topology))

        # image tag
        not_latest = all(not c.get("image", ":latest").endswith(":latest") and
                         ":" in c.get("image", "")
                         for c in containers)
        resultats.append(ResultatAudit(self._get("image-tag"), not_latest))

        # secrets en clair
        no_inline_secrets = True
        for c in containers:
            for env in c.get("env", []):
                name_lower = env.get("name", "").lower()
                if any(kw in name_lower for kw in ["password", "secret", "token", "key", "api"]):
                    if "value" in env:
                        no_inline_secrets = False
                        break
        resultats.append(ResultatAudit(self._get("no-inline-secrets"), no_inline_secrets))

        # labels
        required_labels = {"app", "version", "environment"}
        has_labels = required_labels.issubset(set(labels.keys()))
        resultats.append(ResultatAudit(self._get("labels"), has_labels))

        # rolling update
        strategy = spec.get("strategy", {})
        has_rolling = strategy.get("type") == "RollingUpdate"
        resultats.append(ResultatAudit(self._get("rolling-update"), has_rolling))

        # PDB : ne peut pas être vérifié dans le manifeste Deployment seul
        resultats.append(ResultatAudit(self._get("pdb"), False,
                          "À vérifier séparément (objet PodDisruptionBudget)"))

        return resultats

    def _get(self, nom: str) -> Critere:
        return next(c for c in self.CRITERES if c.nom == nom)

    def score(self, resultats: List[ResultatAudit]) -> dict:
        max_points = sum(c.points for c in self.CRITERES)
        obtenus = sum(r.points_obtenus for r in resultats)
        critiques_rates = [r for r in resultats if r.critere.critique and not r.reussi]
        return {
            "score": obtenus,
            "max": max_points,
            "pct": obtenus / max_points * 100,
            "critiques_rates": critiques_rates,
            "niveau": "Prêt pour la production" if obtenus / max_points >= 0.85
                      else ("Presque prêt" if obtenus / max_points >= 0.65
                            else ("En développement" if obtenus / max_points >= 0.40
                                  else "Prototype")),
        }

    def rapport(self, nom: str, resultats: List[ResultatAudit]):
        sc = self.score(resultats)
        print(f"\n{'='*65}")
        print(f"Audit Deployment : {nom}")
        print(f"Score : {sc['score']}/{sc['max']} ({sc['pct']:.0f}%) — {sc['niveau']}")
        print(f"{'='*65}")

        if sc["critiques_rates"]:
            print(f"\n❌ CRITÈRES OBLIGATOIRES MANQUANTS ({len(sc['critiques_rates'])}) :")
            for r in sc["critiques_rates"]:
                detail = f" — {r.detail}" if r.detail else ""
                print(f"   ✗ {r.critere.description}{detail}")

        non_critiques_rates = [r for r in resultats if not r.critere.critique and not r.reussi]
        if non_critiques_rates:
            print(f"\n⚠  RECOMMANDATIONS NON APPLIQUÉES ({len(non_critiques_rates)}) :")
            for r in non_critiques_rates:
                print(f"   ! {r.critere.description}")

        ok_list = [r for r in resultats if r.reussi]
        print(f"\n✅ CONTRÔLES RÉUSSIS ({len(ok_list)}) :")
        for r in ok_list[:5]:   # Afficher les 5 premiers
            print(f"   ✓ {r.critere.description}")
        if len(ok_list) > 5:
            print(f"   ... et {len(ok_list) - 5} autres")


verificateur = VerificateurDeployment()

# Deployment en production bien configuré
deployment_prod = {
    "metadata": {"labels": {"app": "mon-api", "version": "2.1.0", "environment": "production"}},
    "spec": {
        "replicas": 3,
        "strategy": {"type": "RollingUpdate"},
        "template": {
            "spec": {
                "serviceAccountName": "api-service-account",
                "securityContext": {"runAsNonRoot": True, "runAsUser": 1000},
                "topologySpreadConstraints": [{"maxSkew": 1}],
                "containers": [{
                    "name": "api",
                    "image": "mon-api:2.1.0",
                    "resources": {
                        "requests": {"cpu": "200m", "memory": "256Mi"},
                        "limits":   {"cpu": "500m", "memory": "512Mi"},
                    },
                    "livenessProbe":  {"httpGet": {"path": "/healthz", "port": 8080}},
                    "readinessProbe": {"httpGet": {"path": "/ready", "port": 8080}},
                    "startupProbe":   {"httpGet": {"path": "/startup", "port": 8080}},
                    "env": [
                        {"name": "DB_URL", "valueFrom": {"secretKeyRef": {"name": "db", "key": "url"}}},
                    ],
                    "securityContext": {
                        "allowPrivilegeEscalation": False,
                        "readOnlyRootFilesystem": True,
                        "capabilities": {"drop": ["ALL"]},
                    },
                }]
            }
        }
    }
}

# Deployment de développement (peu configuré)
deployment_dev = {
    "metadata": {"labels": {"app": "mon-api"}},
    "spec": {
        "replicas": 1,
        "template": {
            "spec": {
                "containers": [{
                    "name": "api",
                    "image": "mon-api:latest",   # latest !
                    "env": [
                        {"name": "API_PASSWORD", "value": "secret123"},  # en clair !
                    ],
                }]
            }
        }
    }
}

res_prod = verificateur.verifier(deployment_prod)
verificateur.rapport("deployment-production.yaml", res_prod)

res_dev = verificateur.verifier(deployment_dev)
verificateur.rapport("deployment-dev.yaml", res_dev)
=================================================================
Audit Deployment : deployment-production.yaml
Score : 95/100 (95%) — Prêt pour la production
=================================================================

⚠  RECOMMANDATIONS NON APPLIQUÉES (1) :
   ! PodDisruptionBudget associé (à vérifier sep.)

✅ CONTRÔLES RÉUSSIS (16) :
   ✓ resources.requests définis pour CPU et mémoire
   ✓ resources.limits définis pour CPU et mémoire
   ✓ livenessProbe configuré
   ✓ readinessProbe configuré
   ✓ startupProbe configuré
   ... et 11 autres

=================================================================
Audit Deployment : deployment-dev.yaml
Score : 0/100 (0%) — Prototype
=================================================================

❌ CRITÈRES OBLIGATOIRES MANQUANTS (9) :
   ✗ resources.requests définis pour CPU et mémoire — Au moins un conteneur sans resources.requests
   ✗ resources.limits définis pour CPU et mémoire
   ✗ livenessProbe configuré
   ✗ readinessProbe configuré
   ✗ securityContext.runAsNonRoot = true
   ✗ allowPrivilegeEscalation = false
   ✗ replicas >= 2 — replicas = 1
   ✗ Version d'image épinglée (pas :latest)
   ✗ Pas de secrets en clair dans env

⚠  RECOMMANDATIONS NON APPLIQUÉES (8) :
   ! startupProbe configuré
   ! readOnlyRootFilesystem = true
   ! capabilities.drop = ALL
   ! ServiceAccount dédié (pas default)
   ! topologySpreadConstraints défini
   ! Labels app, version, environment présents
   ! Stratégie RollingUpdate configurée
   ! PodDisruptionBudget associé (à vérifier sep.)

✅ CONTRÔLES RÉUSSIS (0) :

Hide code cell source

# Visualisation comparative des scores
fig, axes = plt.subplots(1, 2, figsize=(13, 5))

scores = {
    "Deployment\nProduction": verificateur.score(res_prod),
    "Deployment\nDéveloppement": verificateur.score(res_dev),
}

ax1 = axes[0]
noms = list(scores.keys())
pcts = [s["pct"] for s in scores.values()]
niveaux = [s["niveau"] for s in scores.values()]
colors = ["#43a047" if p >= 85 else ("#ffa726" if p >= 65 else "#f44336") for p in pcts]

bars = ax1.barh(noms, pcts, color=colors, alpha=0.85, height=0.5)
ax1.set_xlim(0, 110)
ax1.set_xlabel("Score de conformité (%)")
ax1.set_title("Score de conformité des Deployments", fontweight="bold")
ax1.axvline(x=85, color="#2e7d32", linestyle="--", linewidth=1.5, label="Seuil production (85%)")
ax1.axvline(x=65, color="#ffa726", linestyle="--", linewidth=1.5, label="Seuil staging (65%)")
ax1.legend(fontsize=8)

for bar, pct, niveau in zip(bars, pcts, niveaux):
    ax1.text(pct + 1, bar.get_y() + bar.get_height()/2,
             f"{pct:.0f}%  ({niveau})", va="center", fontsize=9, fontweight="bold")

sns.despine(ax=ax1)

# Répartition des critères par statut
ax2 = axes[1]
categories_score = {
    "Prod ✓": sum(1 for r in res_prod if r.reussi),
    "Prod ✗ crit.": sum(1 for r in res_prod if not r.reussi and r.critere.critique),
    "Prod ✗ recom.": sum(1 for r in res_prod if not r.reussi and not r.critere.critique),
    "Dev ✓": sum(1 for r in res_dev if r.reussi),
    "Dev ✗ crit.": sum(1 for r in res_dev if not r.reussi and r.critere.critique),
    "Dev ✗ recom.": sum(1 for r in res_dev if not r.reussi and not r.critere.critique),
}

x = np.arange(2)
w = 0.25
ok_vals   = [categories_score["Prod ✓"],   categories_score["Dev ✓"]]
crit_vals = [categories_score["Prod ✗ crit."], categories_score["Dev ✗ crit."]]
rec_vals  = [categories_score["Prod ✗ recom."], categories_score["Dev ✗ recom."]]

ax2.bar(x - w,   ok_vals,   w, label="Réussis",             color="#66bb6a", alpha=0.85)
ax2.bar(x,       crit_vals, w, label="Manquants (critiques)", color="#f44336", alpha=0.85)
ax2.bar(x + w,   rec_vals,  w, label="Manquants (recom.)",   color="#ffa726", alpha=0.85)

ax2.set_xticks(x)
ax2.set_xticklabels(["Production", "Développement"])
ax2.set_ylabel("Nombre de critères")
ax2.set_title("Répartition des critères par statut", fontweight="bold")
ax2.legend(fontsize=9)
sns.despine(ax=ax2)

plt.tight_layout()
plt.savefig("_static/20_scores_conformite.png", dpi=130, bbox_inches="tight")
plt.show()
_images/f825a665b67ddacdfc496a413c90f2f2af91626fed7f3c97b321199f36452ae5.png

Récapitulatif visuel : de docker run à la production Kubernetes#

Hide code cell source

fig, ax = plt.subplots(figsize=(14, 9))
ax.set_xlim(0, 14)
ax.set_ylim(0, 10)
ax.axis("off")
ax.set_title("Parcours de maturité conteneurisation — De docker run à la production",
             fontsize=13, fontweight="bold", pad=15)

niveaux_maturite = [
    {
        "niveau": 0, "titre": "Niveau 0 — Découverte",
        "items": ["docker run hello-world", "docker pull / docker images",
                  "Premiers Dockerfiles"],
        "color": "#e3f2fd", "ec": "#1565c0",
        "emoji": "🐣",
    },
    {
        "niveau": 1, "titre": "Niveau 1 — Docker maîtrisé",
        "items": ["Dockerfiles optimisés (multi-stage)", "Docker Compose",
                  "Registre d'images privé", "Port publishing, volumes"],
        "color": "#e8f5e9", "ec": "#2e7d32",
        "emoji": "🐥",
    },
    {
        "niveau": 2, "titre": "Niveau 2 — Kubernetes débutant",
        "items": ["Pods, Deployments, Services", "ConfigMaps et Secrets",
                  "kubectl du quotidien", "Ingress basique"],
        "color": "#fff3e0", "ec": "#e65100",
        "emoji": "🌱",
    },
    {
        "niveau": 3, "titre": "Niveau 3 — Kubernetes intermédiaire",
        "items": ["RBAC et SecurityContext", "HPA + VPA", "Helm charts",
                  "Observabilité (Prometheus/Grafana)", "NetworkPolicy"],
        "color": "#fce4ec", "ec": "#c2185b",
        "emoji": "🌿",
    },
    {
        "niveau": 4, "titre": "Niveau 4 — Production-ready",
        "items": ["GitOps (ArgoCD/Flux)", "Chaos engineering", "Multi-cluster",
                  "cert-manager + Gateway API", "Checklist complète"],
        "color": "#ede7f6", "ec": "#6a1b9a",
        "emoji": "🌳",
    },
]

card_w = 12.0 / len(niveaux_maturite)
y_base = 1.0

for i, niv in enumerate(niveaux_maturite):
    x = 1.0 + i * card_w + card_w / 2
    card_h = 7.5

    # Carte principale
    card = FancyBboxPatch((x - card_w/2 + 0.1, y_base), card_w - 0.2, card_h,
                           boxstyle="round,pad=0.1", facecolor=niv["color"],
                           edgecolor=niv["ec"], linewidth=2)
    ax.add_patch(card)

    # Numéro de niveau (cercle)
    circle = plt.Circle((x, y_base + card_h + 0.15), 0.45, facecolor=niv["ec"],
                          edgecolor="white", linewidth=2)
    ax.add_patch(circle)
    ax.text(x, y_base + card_h + 0.15, str(i), ha="center", va="center",
            fontsize=12, color="white", fontweight="bold")

    # Titre
    ax.text(x, y_base + card_h - 0.5, niv["titre"].split("— ")[1],
            ha="center", va="center", fontsize=8.5, fontweight="bold",
            color=niv["ec"], wrap=True)

    # Items
    for j, item in enumerate(niv["items"]):
        iy = y_base + card_h - 1.2 - j * 1.1
        ax.text(x - card_w/2 + 0.3, iy, "•", ha="left", va="center",
                fontsize=10, color=niv["ec"])
        ax.text(x - card_w/2 + 0.55, iy, item, ha="left", va="center",
                fontsize=7.5, color="#37474f", wrap=True)

    # Flèche vers le niveau suivant
    if i < len(niveaux_maturite) - 1:
        ax.annotate("", xy=(x + card_w/2 + 0.1, y_base + card_h/2),
                    xytext=(x + card_w/2 - 0.1, y_base + card_h/2),
                    arrowprops=dict(arrowstyle="->", color="#546e7a", lw=2))

# Titre de bas de page
ax.text(7, 0.4,
        "Ce livre couvre les niveaux 0 → 4 — chaque chapitre vous fait progresser",
        ha="center", va="center", fontsize=10, color="#546e7a", fontstyle="italic",
        bbox=dict(boxstyle="round,pad=0.3", facecolor="#fafafa", edgecolor="#e0e0e0"))

plt.tight_layout()
plt.savefig("_static/20_maturite_roadmap.png", dpi=130, bbox_inches="tight")
plt.show()
_images/b41dda09225b2e2bdde0feb176ef8e3f694e428406fa1a9fe2774e34b9060f14.png

Coûts Kubernetes : optimisation#

Kubernetes peut rapidement devenir coûteux si les ressources ne sont pas bien dimensionnées. Quelques stratégies d’optimisation :

Réduire les coûts :

  • Spot / Preemptible instances : 60-80% moins cher pour les workloads tolerant aux interruptions (batch, CI)

  • Quotas de namespace : ResourceQuota pour limiter la consommation par équipe

  • VPA : éviter le sur-dimensionnement des containers

  • KEDA scale-to-zero : certains workloads peuvent descendre à 0 Pods hors heures de travail

  • Node consolidation : kubectl-node-cleanup ou Karpenter pour consolider les Pods sur moins de nœuds

# ResourceQuota — limiter les ressources d'un namespace
apiVersion: v1
kind: ResourceQuota
metadata:
  name: quota-equipe-alpha
  namespace: equipe-alpha
spec:
  hard:
    requests.cpu: "10"         # 10 CPU au total pour le namespace
    requests.memory: 20Gi
    limits.cpu: "20"
    limits.memory: 40Gi
    pods: "50"                  # Max 50 Pods
    services: "10"
    persistentvolumeclaims: "20"

Mise à jour du cluster : stratégies#

Mettre à jour Kubernetes lui-même est une opération délicate qui nécessite de la préparation.

# Stratégie 1 : Rolling upgrade (in-place)
# 1. Mettre à jour le control plane (un nœud à la fois si HA)
# 2. Mettre à jour les nœuds workers un par un (drain → upgrade → uncordon)

# Drain d'un nœud (évacuer les Pods)
kubectl drain node-1 --ignore-daemonsets --delete-emptydir-data
# Mettre à jour le nœud (OS + kubelet + kubectl)
# ...
kubectl uncordon node-1   # Remettre le nœud en service

# Stratégie 2 : Blue/Green cluster (recommandée pour les mises à jour majeures)
# 1. Créer un nouveau cluster (version N+1)
# 2. Migrer les applications progressivement (canary via DNS/load balancer)
# 3. Basculer le trafic à 100% sur le nouveau cluster
# 4. Supprimer l'ancien cluster

Points clés à retenir — Récapitulatif du livre#

Hide code cell source

fig, ax = plt.subplots(figsize=(14, 8))
ax.set_xlim(0, 14)
ax.set_ylim(0, 9)
ax.axis("off")
ax.set_title("Récapitulatif — Concepts essentiels de Docker et Kubernetes",
             fontsize=13, fontweight="bold", pad=12)

concepts = [
    # (x, y, titre, sous-titre, color, ec)
    (2.0, 7.5, "Images Docker",    "Layers, registry,\nmulti-stage, cache", "#e3f2fd", "#1565c0"),
    (5.5, 7.5, "Réseau Docker",    "bridge/host/overlay,\ndns, iptables NAT",   "#e8f5e9", "#2e7d32"),
    (9.0, 7.5, "Docker Compose",   "Services, volumes,\nréseaux, dépendances", "#fff3e0", "#e65100"),
    (12.5,7.5, "Registres",        "Docker Hub, GHCR,\nHarbor, signing",        "#fce4ec", "#c2185b"),

    (2.0, 5.0, "Architecture K8s", "Control plane,\nnœuds, etcd, CNI",       "#e8eaf6", "#3949ab"),
    (5.5, 5.0, "Pods & Workloads", "Deployment, DS,\nSS, Job, labels",        "#e3f2fd", "#1565c0"),
    (9.0, 5.0, "Services & Réseau","ClusterIP, LB,\nIngress, Gateway",         "#e8f5e9", "#2e7d32"),
    (12.5,5.0, "Config & Secrets", "ConfigMap, Secret,\n12-factor, Vault",     "#fff3e0", "#e65100"),

    (2.0, 2.5, "Stockage",         "PV, PVC,\nStorageClass, CSI",             "#f3e5f5", "#7b1fa2"),
    (5.5, 2.5, "Sécurité K8s",     "RBAC, NetworkPolicy\nPSA, SecretMgmt",    "#fce4ec", "#c2185b"),
    (9.0, 2.5, "Observabilité",    "Prometheus, Grafana,\nLoki, tracing",      "#e8eaf6", "#3949ab"),
    (12.5,2.5, "CI/CD & GitOps",   "Helm, ArgoCD,\nFlux, pipelines",           "#e3f2fd", "#1565c0"),
]

for x, y, titre, sous_titre, fc, ec in concepts:
    b = FancyBboxPatch((x - 1.8, y - 0.85), 3.6, 1.7, boxstyle="round,pad=0.1",
                        facecolor=fc, edgecolor=ec, linewidth=2)
    ax.add_patch(b)
    ax.text(x, y + 0.35, titre, ha="center", va="center",
            fontsize=9.5, fontweight="bold", color=ec)
    ax.text(x, y - 0.2, sous_titre, ha="center", va="center",
            fontsize=8, color="#37474f", linespacing=1.4)

# Séparateur Docker / K8s
ax.axhline(y=6.3, xmin=0.03, xmax=0.97, color="#546e7a", linewidth=1.5, linestyle="--", alpha=0.5)
ax.text(7, 6.55, "DOCKER", ha="center", va="center", fontsize=9, color="#546e7a",
        fontweight="bold",
        bbox=dict(boxstyle="round,pad=0.2", facecolor="white", edgecolor="#90a4ae"))
ax.text(7, 4.0, "KUBERNETES", ha="center", va="center", fontsize=9, color="#3949ab",
        fontweight="bold",
        bbox=dict(boxstyle="round,pad=0.2", facecolor="white", edgecolor="#7986cb"))
ax.text(7, 1.0, "HPA/VPA/KEDA — Scalabilité & Résilience — Bonnes pratiques",
        ha="center", va="center", fontsize=9, color="#37474f",
        bbox=dict(boxstyle="round,pad=0.2", facecolor="white", edgecolor="#bdbdbd"))

ax.text(7, 0.2, "Du conteneur unique à l'orchestration à grande échelle",
        ha="center", va="center", fontsize=10, color="#1b5e20",
        fontweight="bold", fontstyle="italic")

plt.tight_layout()
plt.savefig("_static/20_recap_global.png", dpi=130, bbox_inches="tight")
plt.show()
_images/f9cfef40596060662684f50921b3bb2f4f43b828f759aece783fbfd5a92a7eb0.png

Points clés à retenir#

  • La checklist Dockerfile : image minimale, multi-stage, utilisateur non-root, HEALTHCHECK, .dockerignore, pas de secrets dans ENV

  • La checklist Deployment : resources requests+limits, liveness+readiness probes, replicas≥2, SecurityContext, PDB, topologySpreadConstraints

  • Le sizing des ressources se fait empiriquement : observer d’abord (VPA mode Off), puis configurer — requests.cpu = P95, requests.mem = P99

  • Les trois probes ont des rôles distincts : liveness redémarre, readiness retire du LB, startup protège le démarrage lent

  • La séparation config/code (12-factor) est fondamentale : ConfigMaps pour la config, Secrets pour les données sensibles

  • Optimiser les coûts : spot instances pour les workloads non-critiques, ResourceQuota par namespace, VPA pour éviter le sur-dimensionnement

  • La mise à jour du cluster : rolling upgrade pour les mises à jour mineures, blue/green cluster pour les mises à jour majeures

  • La maturité conteneurisation est un chemin progressif : commencer par maîtriser Docker, puis progresser vers K8s, puis vers GitOps et la production complète