Pods et workloads Kubernetes#

Kubernetes ne gère pas directement des conteneurs — il gère des Pods. Et au-dessus des Pods, il existe toute une hiérarchie d’abstractions (Deployment, DaemonSet, StatefulSet…) pour répondre à différents besoins opérationnels. Ce chapitre démêle cette hiérarchie et explique quand utiliser quelle abstraction.

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 random
import json

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)

Le Pod : l’unité de base de Kubernetes#

Un Pod est le plus petit objet déployable dans Kubernetes. Contrairement à Docker où l’unité est le conteneur, Kubernetes encapsule un ou plusieurs conteneurs dans un Pod.

Pourquoi plusieurs conteneurs dans un Pod ?#

Les conteneurs d’un même Pod partagent :

  • Le namespace réseau : ils communiquent via localhost, ont la même adresse IP

  • Les volumes : les mêmes volumes montés sont accessibles à tous

  • Le cycle de vie : ils démarrent et s’arrêtent ensemble

Pattern sidecar

Le pattern le plus courant avec plusieurs conteneurs dans un Pod est le sidecar : un conteneur principal (votre application) accompagné d’un conteneur auxiliaire (un agent de log, un proxy mTLS comme Envoy, un agent de monitoring…).

Exemples :

  • Application + agent Fluentd (collecte des logs)

  • Application + Envoy proxy (maillage de service / service mesh)

  • Serveur web + init container (qui prépare des fichiers de configuration)

Manifeste Pod YAML#

# pod-exemple.yaml
apiVersion: v1
kind: Pod
metadata:
  name: mon-application
  namespace: default
  labels:
    app: mon-app
    version: "1.0"
    environment: production
spec:
  # Conteneur principal
  containers:
    - name: app
      image: mon-app:1.0
      ports:
        - containerPort: 8080
      # Ressources : requests = garanti, limits = maximum
      resources:
        requests:
          memory: "128Mi"
          cpu: "250m"        # 250 millicores = 0.25 CPU
        limits:
          memory: "256Mi"
          cpu: "500m"
      # Variables d'environnement
      env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: url
      # Probes de santé
      livenessProbe:
        httpGet:
          path: /healthz
          port: 8080
        initialDelaySeconds: 10
        periodSeconds: 30
      readinessProbe:
        httpGet:
          path: /ready
          port: 8080
        initialDelaySeconds: 5
        periodSeconds: 10

  # Conteneur sidecar (agent de logs)
  - name: log-agent
    image: fluentd:v1.16
    volumeMounts:
      - name: logs
        mountPath: /var/log/app

  volumes:
    - name: logs
      emptyDir: {}

  # Redémarrage : Always (défaut), OnFailure, Never
  restartPolicy: Always

Pourquoi ne pas déployer des Pods directement ?#

Déployer un Pod « nu » est rarement la bonne pratique en production. Si ce Pod crash ou si le nœud qui le porte tombe, il n’est pas recréé automatiquement. Les Pods nus n’ont pas de self-healing.

Hide code cell source

fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# --- Sans Deployment (Pod nu) ---
ax = axes[0]
ax.set_xlim(0, 10)
ax.set_ylim(0, 8)
ax.axis("off")
ax.set_title("Pod nu — SANS Deployment\n(pas de self-healing)", fontsize=11,
             fontweight="bold", color="#c62828")

# Nœud
node_box = FancyBboxPatch((1, 1.5), 8, 5.5, boxstyle="round,pad=0.2",
                           facecolor="#fafafa", edgecolor="#90a4ae", linewidth=2)
ax.add_patch(node_box)
ax.text(5, 6.7, "Nœud Kubernetes", ha="center", va="center", fontsize=9, color="#546e7a")

# Pod
pod = FancyBboxPatch((3, 3.0), 4, 1.8, boxstyle="round,pad=0.1",
                      facecolor="#e3f2fd", edgecolor="#1565c0", linewidth=2)
ax.add_patch(pod)
ax.text(5, 4.0, "Pod : mon-app", ha="center", va="center",
        fontsize=10, fontweight="bold", color="#0d47a1")
ax.text(5, 3.5, "app:1.0 | IP: 10.0.0.5", ha="center", va="center",
        fontsize=8.5, color="#0d47a1")

# Crash du nœud
ax.annotate("", xy=(5, 2.5), xytext=(5, 3.0),
            arrowprops=dict(arrowstyle="-", color="#f44336", lw=3))

crash_box = FancyBboxPatch((2.5, 1.6), 5, 0.9, boxstyle="round,pad=0.1",
                             facecolor="#ffcdd2", edgecolor="#c62828", linewidth=2)
ax.add_patch(crash_box)
ax.text(5, 2.05, "Nœud tombe ou Pod crash", ha="center", va="center",
        fontsize=9, fontweight="bold", color="#c62828")

ax.text(5, 0.8, "→ Pod PERDU définitivement\nAucune récupération automatique",
        ha="center", va="center", fontsize=9, color="#c62828",
        bbox=dict(boxstyle="round,pad=0.3", facecolor="#ffebee", edgecolor="#f44336"))

# --- Avec Deployment ---
ax2 = axes[1]
ax2.set_xlim(0, 10)
ax2.set_ylim(0, 8)
ax2.axis("off")
ax2.set_title("Avec Deployment\n(self-healing automatique)", fontsize=11,
              fontweight="bold", color="#2e7d32")

# Control plane
cp = FancyBboxPatch((3, 6.5), 4, 0.9, boxstyle="round,pad=0.1",
                     facecolor="#e8eaf6", edgecolor="#3949ab", linewidth=2)
ax2.add_patch(cp)
ax2.text(5, 6.95, "Controller Manager\n(surveille l'état désiré)", ha="center", va="center",
         fontsize=8.5, fontweight="bold", color="#283593")

# Nœud A (tombe)
nodeA = FancyBboxPatch((0.5, 2.5), 4, 3.5, boxstyle="round,pad=0.15",
                        facecolor="#ffebee", edgecolor="#f44336", linewidth=2, linestyle="--")
ax2.add_patch(nodeA)
ax2.text(2.5, 5.7, "Nœud A (tombe)", ha="center", va="center",
         fontsize=9, color="#c62828", fontweight="bold")
pod_mort = FancyBboxPatch((0.9, 3.5), 3.2, 1.2, boxstyle="round,pad=0.08",
                           facecolor="#ffcdd2", edgecolor="#f44336", linewidth=1.5)
ax2.add_patch(pod_mort)
ax2.text(2.5, 4.1, "Pod (MORT)", ha="center", va="center",
         fontsize=9, fontweight="bold", color="#c62828")

# Nœud B (reçoit le nouveau pod)
nodeB = FancyBboxPatch((5.5, 2.5), 4, 3.5, boxstyle="round,pad=0.15",
                        facecolor="#e8f5e9", edgecolor="#2e7d32", linewidth=2)
ax2.add_patch(nodeB)
ax2.text(7.5, 5.7, "Nœud B (sain)", ha="center", va="center",
         fontsize=9, color="#2e7d32", fontweight="bold")
pod_new = FancyBboxPatch((5.9, 3.5), 3.2, 1.2, boxstyle="round,pad=0.08",
                          facecolor="#c8e6c9", edgecolor="#2e7d32", linewidth=2)
ax2.add_patch(pod_new)
ax2.text(7.5, 4.1, "Pod (RECRÉÉ ✓)", ha="center", va="center",
         fontsize=9, fontweight="bold", color="#2e7d32")

# Flèches du controller
ax2.annotate("", xy=(2.5, 5.7), xytext=(4.0, 6.5),
             arrowprops=dict(arrowstyle="->", color="#f44336", lw=1.5))
ax2.annotate("", xy=(7.5, 5.7), xytext=(6.0, 6.5),
             arrowprops=dict(arrowstyle="->", color="#2e7d32", lw=2))

ax2.text(5, 1.8, "→ Controller détecte le manque\n→ Replanifie sur Nœud B automatiquement",
         ha="center", va="center", fontsize=9, color="#2e7d32",
         bbox=dict(boxstyle="round,pad=0.3", facecolor="#e8f5e9", edgecolor="#2e7d32"))

# Légende Deployment
dep_label = FancyBboxPatch((0.3, 1.0), 9.4, 0.7, boxstyle="round,pad=0.05",
                             facecolor="#e8eaf6", edgecolor="#3949ab", linewidth=1)
ax2.add_patch(dep_label)
ax2.text(5, 1.35, "Deployment (état désiré : 1 réplica) — contrôle en boucle continue",
         ha="center", va="center", fontsize=8.5, color="#283593")

plt.tight_layout()
plt.savefig("_static/10_pod_vs_deployment.png", dpi=130, bbox_inches="tight")
plt.show()
_images/d772357ba174a862c777977e60e0097f1107c1384efe38b6981c8534c51d367c.png

ReplicaSet : maintenir N réplicas#

Un ReplicaSet garantit qu’un nombre défini de réplicas d’un Pod tourne en permanence. Si un Pod crash, le ReplicaSet en crée un nouveau. Si trop de Pods tournent (ex : un Pod de trop démarré manuellement), il en supprime un.

# replicaset.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: mon-app-rs
spec:
  replicas: 3          # État désiré : 3 Pods doivent tourner
  selector:
    matchLabels:
      app: mon-app     # Gère tous les Pods avec ce label
  template:            # Template pour créer de nouveaux Pods
    metadata:
      labels:
        app: mon-app
    spec:
      containers:
        - name: app
          image: mon-app:1.0
          ports:
            - containerPort: 8080

ReplicaSet vs Deployment

En pratique, vous ne créez jamais de ReplicaSet directement. Vous créez un Deployment, qui crée et gère les ReplicaSets pour vous. La raison : les Deployments ajoutent la gestion des mises à jour (rolling update) et du rollback par-dessus les ReplicaSets.

Deployment : le workload le plus courant#

Le Deployment est l’abstraction la plus utilisée dans Kubernetes. Il gère :

  1. La création du ReplicaSet (qui gère les Pods)

  2. Les mises à jour sans interruption (rolling update)

  3. Le rollback en cas de problème

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mon-app
  labels:
    app: mon-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: mon-app
  # Stratégie de mise à jour
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1    # Au plus 1 Pod indisponible pendant la mise à jour
      maxSurge: 1          # Au plus 1 Pod en plus pendant la mise à jour
  template:
    metadata:
      labels:
        app: mon-app
        version: "2.0"
    spec:
      containers:
        - name: app
          image: mon-app:2.0
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "256Mi"
# Commandes Deployment essentielles
kubectl apply -f deployment.yaml       # Créer ou mettre à jour
kubectl get deployments                # Lister
kubectl describe deployment mon-app    # Détails
kubectl rollout status deployment/mon-app   # Suivre une mise à jour
kubectl rollout history deployment/mon-app  # Historique des versions
kubectl rollout undo deployment/mon-app     # Rollback vers la version précédente
kubectl rollout undo deployment/mon-app --to-revision=2  # Rollback vers v2
kubectl scale deployment mon-app --replicas=5  # Scaler manuellement

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("Stratégie RollingUpdate — Deployment Kubernetes\n"
             "(maxUnavailable=1, maxSurge=1, 3 réplicas)",
             fontsize=12, fontweight="bold", pad=12)

etapes = [
    {
        "titre": "État initial\n(v1.0)",
        "pods": [("Pod-1", "v1.0", "#e3f2fd"), ("Pod-2", "v1.0", "#e3f2fd"),
                 ("Pod-3", "v1.0", "#e3f2fd")],
        "note": "3 Pods v1.0\ntournent",
    },
    {
        "titre": "Étape 1\n(démarrage)",
        "pods": [("Pod-1", "v1.0", "#e3f2fd"), ("Pod-2", "v1.0", "#e3f2fd"),
                 ("Pod-3", "Arrêt", "#ffcdd2"), ("Pod-4", "v2.0⟳", "#fff9c4")],
        "note": "Pod-3 arrêté\nPod-4 démarre",
    },
    {
        "titre": "Étape 2",
        "pods": [("Pod-1", "v1.0", "#e3f2fd"), ("Pod-2", "v1.0", "#e3f2fd"),
                 ("Pod-4", "v2.0 ✓", "#c8e6c9")],
        "note": "Pod-4 prêt\n2 v1.0 + 1 v2.0",
    },
    {
        "titre": "Étape 3",
        "pods": [("Pod-1", "v1.0", "#e3f2fd"), ("Pod-2", "Arrêt", "#ffcdd2"),
                 ("Pod-4", "v2.0 ✓", "#c8e6c9"), ("Pod-5", "v2.0⟳", "#fff9c4")],
        "note": "Pod-2 arrêté\nPod-5 démarre",
    },
    {
        "titre": "Étape 4",
        "pods": [("Pod-1", "Arrêt", "#ffcdd2"), ("Pod-4", "v2.0 ✓", "#c8e6c9"),
                 ("Pod-5", "v2.0 ✓", "#c8e6c9"), ("Pod-6", "v2.0⟳", "#fff9c4")],
        "note": "Pod-1 arrêté\nPod-6 démarre",
    },
    {
        "titre": "État final\n(v2.0)",
        "pods": [("Pod-4", "v2.0 ✓", "#c8e6c9"), ("Pod-5", "v2.0 ✓", "#c8e6c9"),
                 ("Pod-6", "v2.0 ✓", "#c8e6c9")],
        "note": "3 Pods v2.0\n0 interruption !",
    },
]

n = len(etapes)
step_w = 14.0 / n

for i, etape in enumerate(etapes):
    cx = i * step_w + step_w / 2
    # Titre
    ax.text(cx, 6.5, etape["titre"], ha="center", va="center",
            fontsize=8.5, fontweight="bold", color="#37474f")

    # Pods
    pod_h = 0.7
    pod_w = step_w * 0.78
    n_pods = len(etape["pods"])
    y_start = 4.8 - (n_pods - 3) * 0.5

    for j, (nom, version, color) in enumerate(etape["pods"]):
        y = y_start - j * (pod_h + 0.1)
        b = FancyBboxPatch((cx - pod_w/2, y - pod_h/2), pod_w, pod_h,
                            boxstyle="round,pad=0.05", facecolor=color,
                            edgecolor="#546e7a", linewidth=1)
        ax.add_patch(b)
        ax.text(cx, y + 0.1, nom, ha="center", va="center", fontsize=7.5, fontweight="bold")
        ax.text(cx, y - 0.2, version, ha="center", va="center", fontsize=7, color="#37474f")

    # Note
    ax.text(cx, 0.5, etape["note"], ha="center", va="center", fontsize=7.5,
            color="#37474f", linespacing=1.4,
            bbox=dict(boxstyle="round,pad=0.2", facecolor="#f5f5f5", edgecolor="#bdbdbd"))

    # Flèche vers étape suivante
    if i < n - 1:
        ax.annotate("", xy=((i + 1) * step_w + 0.1, 3.5), xytext=(cx + pod_w/2 + 0.05, 3.5),
                    arrowprops=dict(arrowstyle="->", color="#546e7a", lw=1.5))

# Légende
patches = [
    mpatches.Patch(facecolor="#e3f2fd", edgecolor="#546e7a", label="v1.0 — actif"),
    mpatches.Patch(facecolor="#fff9c4", edgecolor="#546e7a", label="v2.0 — démarrage"),
    mpatches.Patch(facecolor="#c8e6c9", edgecolor="#546e7a", label="v2.0 — prêt"),
    mpatches.Patch(facecolor="#ffcdd2", edgecolor="#546e7a", label="Arrêt en cours"),
]
ax.legend(handles=patches, loc="upper right", fontsize=8, ncol=2)

plt.tight_layout()
plt.savefig("_static/10_rolling_update.png", dpi=130, bbox_inches="tight")
plt.show()
_images/abf206fed11fff945967c26381f69b6d45a8eaa94afb46364d5d607567b72863.png

DaemonSet : un Pod par nœud#

Un DaemonSet garantit qu’une copie d’un Pod tourne sur chaque nœud du cluster (ou sur un sous-ensemble de nœuds). Quand un nouveau nœud rejoint le cluster, le DaemonSet y déploie automatiquement le Pod. Quand un nœud est retiré, le Pod est supprimé.

Cas d’usage typiques :

  • Agents de monitoring (Prometheus Node Exporter, Datadog Agent)

  • Agents de collecte de logs (Fluentd, Filebeat)

  • Plugins réseau (CNI comme Calico, Weave)

  • Agents de sécurité (Falco)

# daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-exporter
spec:
  selector:
    matchLabels:
      app: node-exporter
  template:
    metadata:
      labels:
        app: node-exporter
    spec:
      hostNetwork: true     # Accès au réseau de l'hôte (monitoring réseau)
      hostPID: true         # Accès aux processus de l'hôte
      containers:
        - name: node-exporter
          image: prom/node-exporter:latest
          ports:
            - containerPort: 9100
          volumeMounts:
            - name: proc
              mountPath: /host/proc
              readOnly: true
      volumes:
        - name: proc
          hostPath:
            path: /proc

StatefulSet : identité et état persistant#

Un StatefulSet est utilisé pour les applications stateful (avec état) : bases de données, clusters Kafka, Elasticsearch…

Différences clés avec un Deployment :

  • Les Pods ont des identités stables et ordonnées : mon-app-0, mon-app-1, mon-app-2

  • Les Pods démarrent et s’arrêtent dans l”ordre (0 → 1 → 2)

  • Chaque Pod a son propre Volume Persistant (PVC) qui lui reste attaché même après redémarrage

# statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres-headless   # Service headless requis
  replicas: 3
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:16
          env:
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: password
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
  # Chaque Pod reçoit son propre PVC
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 10Gi

Job et CronJob : tâches ponctuelles et planifiées#

Job#

Un Job crée un ou plusieurs Pods et attend qu’ils se terminent avec succès. Contrairement à un Deployment, les Pods d’un Job ne sont pas redémarrés après leur succès.

# job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: migration-bdd
spec:
  completions: 1        # Nombre de Pods à terminer avec succès
  parallelism: 1        # Nombre de Pods en parallèle
  backoffLimit: 3       # Nombre de tentatives en cas d'échec
  template:
    spec:
      containers:
        - name: migration
          image: mon-app:1.0
          command: ["python", "manage.py", "migrate"]
      restartPolicy: OnFailure   # Important pour les Jobs !

CronJob#

Un CronJob crée des Jobs selon un planning cron.

# cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: sauvegarde-nocturne
spec:
  schedule: "0 2 * * *"   # Tous les jours à 2h du matin
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: backup
              image: backup-tool:latest
              command: ["./backup.sh"]
          restartPolicy: OnFailure
  successfulJobsHistoryLimit: 3  # Garder les 3 derniers Jobs réussis
  failedJobsHistoryLimit: 1

Labels et sélecteurs#

Les labels sont des paires clé-valeur attachées aux objets Kubernetes. Les sélecteurs permettent de filtrer et de cibler des objets par leurs labels. C’est le système de tags qui relie toute l’architecture Kubernetes.

# Labels sur un Pod
metadata:
  labels:
    app: mon-service          # Nom de l'application
    version: "2.1.3"          # Version
    environment: production   # Environnement
    tier: backend             # Couche applicative
    team: plateforme          # Équipe responsable

# Sélecteur dans un Service
selector:
  app: mon-service
  environment: production
  # → Cible tous les Pods avec CES DEUX labels
# Utiliser les labels en ligne de commande
kubectl get pods -l app=mon-service
kubectl get pods -l environment=production,tier=backend
kubectl get pods -l "version in (2.0, 2.1)"   # Sélecteur ensembliste
kubectl label pod mon-pod release=stable       # Ajouter un label
kubectl label pod mon-pod release-             # Supprimer un label

Hide code cell source

fig, ax = plt.subplots(figsize=(14, 7))
ax.set_xlim(0, 14)
ax.set_ylim(0, 8)
ax.axis("off")
ax.set_title("Hiérarchie des workloads Kubernetes", fontsize=13, fontweight="bold", pad=12)

# Deployment
dep_box = FancyBboxPatch((1, 5.8), 12, 1.0, boxstyle="round,pad=0.1",
                          facecolor="#e8eaf6", edgecolor="#3949ab", linewidth=2.5)
ax.add_patch(dep_box)
ax.text(7, 6.3, "Deployment  (gère les mises à jour, rollback, scaling)",
        ha="center", va="center", fontsize=11, fontweight="bold", color="#1a237e")

# ReplicaSet
rs_box = FancyBboxPatch((2.5, 4.2), 9, 0.9, boxstyle="round,pad=0.1",
                         facecolor="#e3f2fd", edgecolor="#1565c0", linewidth=2)
ax.add_patch(rs_box)
ax.text(7, 4.65, "ReplicaSet  (maintient N réplicas actifs en permanence)",
        ha="center", va="center", fontsize=10, fontweight="bold", color="#0d47a1")

# Flèche Deployment → ReplicaSet
ax.annotate("", xy=(7, 5.1), xytext=(7, 5.8),
            arrowprops=dict(arrowstyle="->", color="#3949ab", lw=2))
ax.text(7.3, 5.45, "crée / gère", fontsize=8, color="#3949ab")

# Pods
pod_positions = [3, 5.5, 8, 10.5]
for i, px in enumerate(pod_positions):
    pod_box = FancyBboxPatch((px - 0.9, 1.8), 1.8, 1.8, boxstyle="round,pad=0.08",
                              facecolor="#e8f5e9", edgecolor="#2e7d32", linewidth=1.5)
    ax.add_patch(pod_box)
    ax.text(px, 3.1, f"Pod-{i+1}", ha="center", va="center",
            fontsize=9, fontweight="bold", color="#1b5e20")
    # Conteneur dans le pod
    inner = FancyBboxPatch((px - 0.7, 2.0), 1.4, 0.8, boxstyle="round,pad=0.04",
                            facecolor="#c8e6c9", edgecolor="#388e3c", linewidth=1)
    ax.add_patch(inner)
    ax.text(px, 2.4, "app:2.0", ha="center", va="center", fontsize=7.5, color="#1b5e20")
    # Flèche ReplicaSet → Pod
    ax.annotate("", xy=(px, 3.6), xytext=(px, 4.2),
                arrowprops=dict(arrowstyle="->", color="#1565c0", lw=1.5))

# Label
ax.text(7, 0.8, "Labels : app=mon-service, env=prod — le sélecteur du ReplicaSet cible ces Pods",
        ha="center", va="center", fontsize=9, color="#546e7a",
        bbox=dict(boxstyle="round,pad=0.3", facecolor="#fafafa", edgecolor="#90a4ae"))

plt.tight_layout()
plt.savefig("_static/10_hierarchie_deployment.png", dpi=130, bbox_inches="tight")
plt.show()
_images/730e57d195e2207e60cb37ded6f9909050e9475d040f642eb52f942c57afba9b.png

Comparaison des workloads#

Hide code cell source

fig, ax = plt.subplots(figsize=(14, 6))
ax.axis("off")
ax.set_title("Comparaison des workloads Kubernetes", fontsize=13, fontweight="bold", pad=10)

headers = ["Workload", "Self-healing", "Scaling", "Identités stables", "Ordre", "Cas d'usage"]
rows = [
    ["Deployment",  "✓ Oui", "✓ Oui", "✗ Non (noms aléatoires)", "✗ Non", "Serveurs web, APIs, microservices"],
    ["ReplicaSet",  "✓ Oui", "✓ Oui", "✗ Non",                   "✗ Non", "Géré par Deployment (rarement direct)"],
    ["DaemonSet",   "✓ Oui", "Auto",  "✓ Par nœud",               "✗ Non", "Monitoring, logging, CNI plugins"],
    ["StatefulSet", "✓ Oui", "✓ Oui", "✓ Oui (pod-0, pod-1...)", "✓ Oui", "Bases de données, Kafka, ZooKeeper"],
    ["Job",         "✗ Non", "✓ Oui", "✗ Non",                   "✗ Non", "Migrations, batch processing, ETL"],
    ["CronJob",     "✗ Non", "✗ Non", "✗ Non",                   "✗ Non", "Sauvegardes, rapports, purges"],
]

color_map = {
    "✓ Oui": "#c8e6c9", "✗ Non": "#ffcdd2",
    "✓ Par nœud": "#c8e6c9", "✓ Oui (pod-0, pod-1...)": "#c8e6c9",
    "Auto": "#b3e5fc",
}

col_widths = [0.17, 0.12, 0.10, 0.22, 0.08, 0.31]
x_starts = [0.0]
for w in col_widths[:-1]:
    x_starts.append(x_starts[-1] + w)

row_h = 0.13
y_header = 0.93

for i, (h, x, w) in enumerate(zip(headers, x_starts, col_widths)):
    rect = FancyBboxPatch((x + 0.002, y_header), w - 0.004, 0.065,
                           transform=ax.transAxes,
                           boxstyle="round,pad=0.005", facecolor="#37474f",
                           edgecolor="white", linewidth=0.5, clip_on=False)
    ax.add_patch(rect)
    ax.text(x + w/2, y_header + 0.033, h, transform=ax.transAxes,
            ha="center", va="center", fontsize=9, fontweight="bold", color="white")

for r_idx, row in enumerate(rows):
    y = y_header - (r_idx + 1) * row_h
    bg_base = "#fafafa" if r_idx % 2 == 0 else "#f0f4f8"
    for c_idx, (cell, x, w) in enumerate(zip(row, x_starts, col_widths)):
        fc = color_map.get(cell, bg_base)
        rect = FancyBboxPatch((x + 0.002, y), w - 0.004, row_h - 0.005,
                               transform=ax.transAxes,
                               boxstyle="round,pad=0.003", facecolor=fc,
                               edgecolor="#e0e0e0", linewidth=0.5, clip_on=False)
        ax.add_patch(rect)
        ax.text(x + w/2, y + row_h/2, cell, transform=ax.transAxes,
                ha="center", va="center", fontsize=8,
                color="#212121" if c_idx > 0 else "#1a237e",
                fontweight="bold" if c_idx == 0 else "normal")

plt.tight_layout()
plt.savefig("_static/10_workloads_comparaison.png", dpi=130, bbox_inches="tight")
plt.show()
_images/577abc5fc75150fa3d4408fee07769e10ef14873cdee7495aec067a298c98044.png

Simulation Python : scheduler simplifié et parsing de manifestes#

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

@dataclass
class Noeud:
    """Représente un nœud Kubernetes dans la simulation."""
    nom: str
    cpu_total_m: int      # millicores total
    mem_total_mi: int     # MiB total
    cpu_dispo_m: int = 0
    mem_dispo_mi: int = 0
    pods: List[str] = field(default_factory=list)

    def __post_init__(self):
        self.cpu_dispo_m = self.cpu_total_m
        self.mem_dispo_mi = self.mem_total_mi

    def peut_accueillir(self, cpu_m: int, mem_mi: int) -> bool:
        return self.cpu_dispo_m >= cpu_m and self.mem_dispo_mi >= mem_mi

    def reserver(self, pod_nom: str, cpu_m: int, mem_mi: int):
        self.cpu_dispo_m -= cpu_m
        self.mem_dispo_mi -= mem_mi
        self.pods.append(pod_nom)

    def utilisation_cpu_pct(self) -> float:
        return (self.cpu_total_m - self.cpu_dispo_m) / self.cpu_total_m * 100

    def utilisation_mem_pct(self) -> float:
        return (self.mem_total_mi - self.mem_dispo_mi) / self.mem_total_mi * 100


@dataclass
class PodRequest:
    """Représente une demande de planification d'un Pod."""
    nom: str
    cpu_request_m: int    # millicores
    mem_request_mi: int   # MiB


class SchedulerSimple:
    """
    Simulation d'un scheduler Kubernetes simplifié.
    Stratégie : LeastRequested (choisir le nœud le moins chargé).
    """
    def __init__(self, noeuds: List[Noeud]):
        self.noeuds = noeuds
        self.historique = []

    def planifier(self, pod: PodRequest) -> Optional[str]:
        """Planifie un Pod sur le nœud le plus disponible."""
        candidats = [n for n in self.noeuds if n.peut_accueillir(pod.cpu_request_m, pod.mem_request_mi)]

        if not candidats:
            self.historique.append((pod.nom, None, "PENDING — aucun nœud disponible"))
            return None

        # Stratégie LeastRequested : nœud avec le plus de ressources disponibles (normalisé)
        def score(noeud: Noeud) -> float:
            cpu_score = noeud.cpu_dispo_m / noeud.cpu_total_m
            mem_score = noeud.mem_dispo_mi / noeud.mem_total_mi
            return (cpu_score + mem_score) / 2

        noeud_choisi = max(candidats, key=score)
        noeud_choisi.reserver(pod.nom, pod.cpu_request_m, pod.mem_request_mi)
        self.historique.append((pod.nom, noeud_choisi.nom,
                                 f"OK — CPU: {pod.cpu_request_m}m, Mém: {pod.mem_request_mi}Mi"))
        return noeud_choisi.nom

    def rapport(self):
        print("\n" + "="*65)
        print("Rapport de planification (Scheduler simplifié)")
        print("="*65)
        for pod_nom, noeud, statut in self.historique:
            icone = "✓" if noeud else "⚠"
            noeud_str = noeud if noeud else "AUCUN"
            print(f"  {icone} {pod_nom:<20}{noeud_str:<15} | {statut}")

        print("\nÉtat des nœuds :")
        print(f"  {'Nœud':<15} {'CPU util.':<12} {'Mém. util.':<12} {'Pods'}")
        print("-" * 60)
        for n in self.noeuds:
            pods_str = ", ".join(n.pods) if n.pods else "(vide)"
            print(f"  {n.nom:<15} {n.utilisation_cpu_pct():<12.0f}% "
                  f"{n.utilisation_mem_pct():<12.0f}% {pods_str}")


# Simulation
noeuds = [
    Noeud("node-1", cpu_total_m=4000, mem_total_mi=8192),
    Noeud("node-2", cpu_total_m=4000, mem_total_mi=8192),
    Noeud("node-3", cpu_total_m=2000, mem_total_mi=4096),
]

pods_a_planifier = [
    PodRequest("api-0",       cpu_request_m=250,  mem_request_mi=256),
    PodRequest("api-1",       cpu_request_m=250,  mem_request_mi=256),
    PodRequest("api-2",       cpu_request_m=250,  mem_request_mi=256),
    PodRequest("db-0",        cpu_request_m=1000, mem_request_mi=2048),
    PodRequest("db-1",        cpu_request_m=1000, mem_request_mi=2048),
    PodRequest("worker-0",    cpu_request_m=500,  mem_request_mi=512),
    PodRequest("worker-1",    cpu_request_m=500,  mem_request_mi=512),
    PodRequest("log-agent-0", cpu_request_m=100,  mem_request_mi=128),
    PodRequest("log-agent-1", cpu_request_m=100,  mem_request_mi=128),
    PodRequest("log-agent-2", cpu_request_m=100,  mem_request_mi=128),
    PodRequest("gros-batch",  cpu_request_m=3000, mem_request_mi=6000),  # trop gros
]

scheduler = SchedulerSimple(noeuds)
for pod in pods_a_planifier:
    scheduler.planifier(pod)

scheduler.rapport()
=================================================================
Rapport de planification (Scheduler simplifié)
=================================================================
  ✓ api-0                → node-1          | OK — CPU: 250m, Mém: 256Mi
  ✓ api-1                → node-2          | OK — CPU: 250m, Mém: 256Mi
  ✓ api-2                → node-3          | OK — CPU: 250m, Mém: 256Mi
  ✓ db-0                 → node-1          | OK — CPU: 1000m, Mém: 2048Mi
  ✓ db-1                 → node-2          | OK — CPU: 1000m, Mém: 2048Mi
  ✓ worker-0             → node-3          | OK — CPU: 500m, Mém: 512Mi
  ✓ worker-1             → node-3          | OK — CPU: 500m, Mém: 512Mi
  ✓ log-agent-0          → node-1          | OK — CPU: 100m, Mém: 128Mi
  ✓ log-agent-1          → node-2          | OK — CPU: 100m, Mém: 128Mi
  ✓ log-agent-2          → node-1          | OK — CPU: 100m, Mém: 128Mi
  ⚠ gros-batch           → AUCUN           | PENDING — aucun nœud disponible

État des nœuds :
  Nœud            CPU util.    Mém. util.   Pods
------------------------------------------------------------
  node-1          36          % 31          % api-0, db-0, log-agent-0, log-agent-2
  node-2          34          % 30          % api-1, db-1, log-agent-1
  node-3          62          % 31          % api-2, worker-0, worker-1

Hide code cell source

# Visualisation de l'utilisation des nœuds après planification
fig, axes = plt.subplots(1, 2, figsize=(12, 4))

noms_noeuds = [n.nom for n in noeuds]
cpu_util = [n.utilisation_cpu_pct() for n in noeuds]
mem_util = [n.utilisation_mem_pct() for n in noeuds]

x = np.arange(len(noms_noeuds))
w = 0.35

bars_cpu = axes[0].bar(x - w/2, cpu_util, w, label="CPU", color="#42a5f5", alpha=0.85)
bars_mem = axes[0].bar(x + w/2, mem_util, w, label="Mémoire", color="#66bb6a", alpha=0.85)
axes[0].set_xticks(x)
axes[0].set_xticklabels(noms_noeuds)
axes[0].set_ylabel("Utilisation (%)")
axes[0].set_title("Utilisation des ressources par nœud\naprès planification", fontweight="bold")
axes[0].axhline(y=80, color="#f44336", linestyle="--", linewidth=1.5, alpha=0.7,
                 label="Seuil 80%")
axes[0].set_ylim(0, 110)
axes[0].legend(fontsize=9)

for bar in list(bars_cpu) + list(bars_mem):
    h = bar.get_height()
    axes[0].text(bar.get_x() + bar.get_width()/2, h + 1, f"{h:.0f}%",
                 ha="center", va="bottom", fontsize=8)

sns.despine(ax=axes[0])

# Nombre de Pods par nœud
n_pods = [len(n.pods) for n in noeuds]
colors_pods = ["#42a5f5", "#66bb6a", "#ffa726"]
bars2 = axes[1].bar(noms_noeuds, n_pods, color=colors_pods, alpha=0.85, width=0.5)
axes[1].set_ylabel("Nombre de Pods")
axes[1].set_title("Distribution des Pods par nœud", fontweight="bold")

for bar, n_p in zip(bars2, n_pods):
    axes[1].text(bar.get_x() + bar.get_width()/2, n_p + 0.05,
                 str(n_p), ha="center", va="bottom", fontsize=11, fontweight="bold")

for i, n in enumerate(noeuds):
    pod_labels = "\n".join(n.pods) if n.pods else "(vide)"
    axes[1].text(i, -0.6, pod_labels, ha="center", va="top",
                 fontsize=7, color="#37474f", transform=axes[1].get_xaxis_transform())

axes[1].set_ylim(0, max(n_pods) + 1.5)
sns.despine(ax=axes[1])

plt.tight_layout()
plt.savefig("_static/10_scheduler_simulation.png", dpi=130, bbox_inches="tight")
plt.show()
_images/3f9c71baaad3c5ec57dd52dc3bd408a65d61451db4cc72a0b92e802dcc0a5e96.png

Points clés à retenir#

  • Un Pod est l’unité de base de Kubernetes : un ou plusieurs conteneurs partageant le réseau et les volumes

  • Ne jamais déployer des Pods nus en production — utiliser un Deployment pour bénéficier du self-healing

  • Deployment → ReplicaSet → Pods : c’est la hiérarchie standard pour les applications stateless

  • DaemonSet pour les agents qui doivent tourner sur chaque nœud (monitoring, logging, CNI)

  • StatefulSet pour les applications stateful avec identité stable et stockage persistant par Pod

  • Job pour les tâches à exécution unique, CronJob pour les tâches planifiées

  • Les labels et sélecteurs sont le ciment de Kubernetes : ils relient Services, Deployments, ReplicaSets et Pods

  • Le RollingUpdate permet de mettre à jour sans interruption de service (maxUnavailable + maxSurge)