Architecture Kubernetes — Le chef d’orchestre des conteneurs#

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 json
import yaml
import time
from collections import defaultdict
import random

sns.set_theme(style="whitegrid", palette="muted")
plt.rcParams.update({
    "figure.dpi": 120,
    "font.family": "sans-serif",
    "axes.spines.top": False,
    "axes.spines.right": False,
})
random.seed(42)
np.random.seed(42)

Pourquoi Kubernetes ? Les limites de Docker seul#

Docker est excellent pour faire tourner des conteneurs sur une seule machine. Mais en production à grande échelle, vous faites face à des problèmes que Docker seul ne résout pas :

Problème

Docker seul

Kubernetes

Un serveur tombe

Tous les conteneurs sont perdus

Replanifie automatiquement sur d’autres nœuds

Pic de trafic

Vous devez scaler manuellement

Auto-scaling horizontal en quelques secondes

Déploiement sans interruption

Complexe à mettre en place

Intégré natif (rolling update)

50 services à gérer

docker run × 50, difficile

Manifestes déclaratifs, état désiré maintenu

Gestion des secrets

Manuelle, risquée

Objets Secret avec chiffrement, RBAC

Réseau inter-services

Configuration manuelle

DNS interne automatique, load balancing

Utilisation des ressources

Aucune optimisation

Scheduler intelligent (bin packing)

Analogie — Le chef d’orchestre

Imaginez un grand orchestre symphonique avec 100 musiciens (vos conteneurs). Sans chef d’orchestre, chaque musicien joue à son rythme — c’est le chaos. Le chef d’orchestre (Kubernetes) :

  • Sait quelle partition chaque musicien doit jouer (état désiré)

  • Remarque quand un musicien s’arrête et en fait venir un autre (self-healing)

  • Ajuste le tempo selon l’acoustique de la salle (auto-scaling)

  • Coordonne les entrées et sorties des différents pupitres (rolling updates)

Vous, le compositeur, définissez la partition (les manifestes YAML). Kubernetes s’occupe du reste.

Architecture du cluster Kubernetes#

Un cluster Kubernetes est composé de deux types de machines :

  1. Le control plane (cerveau du cluster) — gère l’état désiré

  2. Les worker nodes (muscles du cluster) — font tourner les conteneurs

fig, ax = plt.subplots(figsize=(15, 10))
ax.set_xlim(0, 15)
ax.set_ylim(0, 10)
ax.axis("off")
ax.set_facecolor("#f0f4f8")
fig.patch.set_facecolor("#f0f4f8")
ax.set_title("Architecture complète d'un cluster Kubernetes", fontsize=14, fontweight="bold", pad=12)

def draw_box(ax, x, y, w, h, label, sublabel="", color="#fff", border="#555",
             fontsize=10, border_lw=1.8, label_color="#2c3e50"):
    box = FancyBboxPatch((x, y), w, h, boxstyle="round,pad=0.1",
                          facecolor=color, edgecolor=border, linewidth=border_lw)
    ax.add_patch(box)
    ax.text(x + w/2, y + h * (0.65 if sublabel else 0.5), label,
            ha="center", va="center", fontsize=fontsize,
            fontweight="bold", color=label_color)
    if sublabel:
        ax.text(x + w/2, y + h * 0.28, sublabel,
                ha="center", va="center", fontsize=7.5,
                color="#555", style="italic")

# ============================================================
# CONTROL PLANE
# ============================================================
cp_box = FancyBboxPatch((0.3, 5.8), 6.8, 3.9, boxstyle="round,pad=0.2",
                         facecolor="#dbeafe", edgecolor="#2563eb", linewidth=2.5, linestyle="--")
ax.add_patch(cp_box)
ax.text(3.7, 9.55, "Control Plane", ha="center", va="center",
        fontsize=12, fontweight="bold", color="#1e40af",
        bbox=dict(boxstyle="round,pad=0.3", facecolor="#dbeafe", edgecolor="#2563eb"))

# kube-apiserver
draw_box(ax, 0.5, 7.6, 2.8, 1.8, "kube-apiserver",
         "Point d'entrée unique\nREST API / auth / admission",
         color="#eff6ff", border="#3b82f6", fontsize=9.5)

# etcd
draw_box(ax, 4.0, 7.6, 2.8, 1.8, "etcd",
         "Base de données distribuée\nÉtat du cluster (clé/valeur)",
         color="#f0fdf4", border="#22c55e", fontsize=9.5)

# kube-scheduler
draw_box(ax, 0.5, 5.9, 2.8, 1.5, "kube-scheduler",
         "Place les Pods\nsur les nodes disponibles",
         color="#fdf4ff", border="#a855f7", fontsize=9.5)

# kube-controller-manager
draw_box(ax, 4.0, 5.9, 2.8, 1.5, "controller-manager",
         "Boucles de réconciliation\n(node, replica, endpoint…)",
         color="#fff7ed", border="#f97316", fontsize=9.5)

# Flèche apiserver ↔ etcd
ax.annotate("", xy=(4.0, 8.5), xytext=(3.3, 8.5),
            arrowprops=dict(arrowstyle="<->", color="#555", lw=2))

# Flèches controller/scheduler → apiserver
for y_src in [6.65, 6.65]:
    pass
ax.annotate("", xy=(1.9, 7.6), xytext=(1.9, 7.4),
            arrowprops=dict(arrowstyle="-|>", color="#555", lw=1.5))
ax.annotate("", xy=(5.4, 7.6), xytext=(5.4, 7.4),
            arrowprops=dict(arrowstyle="-|>", color="#555", lw=1.5))

# ============================================================
# WORKER NODES
# ============================================================
node_colors = ["#fef9c3", "#dcfce7", "#fce7f3"]
node_borders = ["#ca8a04", "#16a34a", "#db2777"]
node_names = ["worker-node-1", "worker-node-2", "worker-node-3"]

for ni, (nc, nb, nn) in enumerate(zip(node_colors, node_borders, node_names)):
    x_n = 0.3 + ni * 4.9
    node_box = FancyBboxPatch((x_n, 0.3), 4.5, 5.2, boxstyle="round,pad=0.2",
                               facecolor=nc, edgecolor=nb, linewidth=2.0, linestyle="--")
    ax.add_patch(node_box)
    ax.text(x_n + 2.25, 5.38, nn, ha="center", va="center",
            fontsize=10, fontweight="bold", color=nb,
            bbox=dict(boxstyle="round,pad=0.2", facecolor=nc, edgecolor=nb))

    # kubelet
    draw_box(ax, x_n + 0.15, 3.85, 2.0, 1.2, "kubelet",
             "Agent\nsur chaque node", color="white", border="#555", fontsize=8.5)

    # kube-proxy
    draw_box(ax, x_n + 2.35, 3.85, 2.0, 1.2, "kube-proxy",
             "Règles réseau\niptables/ipvs", color="white", border="#555", fontsize=8.5)

    # container runtime
    draw_box(ax, x_n + 0.15, 2.5, 4.2, 1.15, "containerd / CRI-O",
             "Container Runtime Interface", color="#f1f5f9", border="#64748b", fontsize=8.5)

    # Pods (rectangles représentant des pods)
    pod_colors_inner = ["#86efac", "#93c5fd", "#fca5a5"]
    for pi in range(min(3 - ni % 2, 3)):
        px = x_n + 0.2 + pi * 1.35
        draw_box(ax, px, 0.55, 1.15, 1.7, f"Pod {pi+1}",
                 "🐳 app\n🔵 sidecar" if pi == 0 and ni == 1 else "🐳 app",
                 color=pod_colors_inner[pi % 3], border="#555", fontsize=7.5)

# ============================================================
# kubectl / Utilisateur
# ============================================================
draw_box(ax, 8.2, 7.8, 2.5, 1.5, "kubectl",
         "Client CLI\nde l'utilisateur", color="#fef3c7", border="#d97706",
         fontsize=10, label_color="#92400e")

# Flèche kubectl → apiserver
ax.annotate("", xy=(7.1, 8.5), xytext=(8.2, 8.5),
            arrowprops=dict(arrowstyle="-|>", color="#d97706", lw=2.5))
ax.text(7.65, 8.75, "HTTPS\nREST", ha="center", va="center",
        fontsize=8, color="#d97706", fontweight="bold")

# Flèche apiserver → kubelet (control plane → workers)
ax.annotate("", xy=(2.55, 5.3), xytext=(2.55, 5.8),
            arrowprops=dict(arrowstyle="-|>", color="#2563eb", lw=2))
ax.text(2.55, 5.55, "watch/update", ha="center", va="center",
        fontsize=7.5, color="#2563eb",
        bbox=dict(boxstyle="round,pad=0.1", facecolor="white", edgecolor="#2563eb", alpha=0.7))

# Légende composants
legend_items = [
    mpatches.Patch(facecolor="#dbeafe", edgecolor="#2563eb", label="Control Plane"),
    mpatches.Patch(facecolor="#fef9c3", edgecolor="#ca8a04", label="Worker Node"),
    mpatches.Patch(facecolor="#86efac", edgecolor="#555", label="Pod en cours"),
    mpatches.Patch(facecolor="#fef3c7", edgecolor="#d97706", label="Client kubectl"),
]
ax.legend(handles=legend_items, loc="lower right", fontsize=9.5, framealpha=0.95)

plt.tight_layout()
plt.savefig("k8s_architecture.png", dpi=120, bbox_inches="tight")
plt.show()
/tmp/ipykernel_22866/2223878659.py:129: UserWarning: Glyph 128051 (\N{SPOUTING WHALE}) missing from font(s) DejaVu Sans.
  plt.tight_layout()
/tmp/ipykernel_22866/2223878659.py:129: UserWarning: Glyph 128309 (\N{LARGE BLUE CIRCLE}) missing from font(s) DejaVu Sans.
  plt.tight_layout()
/tmp/ipykernel_22866/2223878659.py:130: UserWarning: Glyph 128051 (\N{SPOUTING WHALE}) missing from font(s) DejaVu Sans.
  plt.savefig("k8s_architecture.png", dpi=120, bbox_inches="tight")
/tmp/ipykernel_22866/2223878659.py:130: UserWarning: Glyph 128309 (\N{LARGE BLUE CIRCLE}) missing from font(s) DejaVu Sans.
  plt.savefig("k8s_architecture.png", dpi=120, bbox_inches="tight")
/home/loc/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 128051 (\N{SPOUTING WHALE}) missing from font(s) DejaVu Sans.
  fig.canvas.print_figure(bytes_io, **kw)
/home/loc/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 128309 (\N{LARGE BLUE CIRCLE}) missing from font(s) DejaVu Sans.
  fig.canvas.print_figure(bytes_io, **kw)
_images/75dcfcd629fc97c883738ead216c9411aa8c7767d4fbf9cfa0e75b4fa88539b5.png

Le control plane en détail#

kube-apiserver : la porte d’entrée#

Tout ce qui se passe dans Kubernetes passe par l”apiserver. C’est le seul composant avec lequel les autres communiquent directement :

  • kubectl apply → apiserver

  • kubelet (sur chaque node) → apiserver

  • kube-scheduler → apiserver

  • kube-controller-manager → apiserver

L’apiserver est stateless : il ne stocke rien lui-même — tout va dans etcd.

etcd : la mémoire du cluster#

etcd est une base de données clé-valeur distribuée et très cohérente (consensus Raft). Elle stocke tout l’état du cluster : quels Pods existent, quels Deployments, quels Secrets, quels Nodes…

Sauvegardez etcd !

Si vous perdez etcd sans backup, vous perdez l’état entier de votre cluster. La sauvegarde d’etcd (etcdctl snapshot save) est une opération de maintenance critique en production.

kube-scheduler : où placer les Pods ?#

Quand un Pod est créé, il est d’abord Pending (en attente de placement). Le scheduler examine tous les nodes disponibles et choisit le meilleur en tenant compte :

  • des ressources disponibles (CPU, mémoire)

  • des contraintes de l’utilisateur (nodeSelector, affinity, taints/tolerations)

  • de l’équilibre de charge entre les nodes

kube-controller-manager : les boucles de réconciliation#

Le controller-manager est un processus qui fait tourner plusieurs controllers en parallèle. Chaque controller surveille un type d’objet et s’assure que l’état réel correspond à l’état désiré.

La boucle de réconciliation#

C’est le concept fondamental de Kubernetes. Contrairement à une approche impérative (« fais X maintenant »), Kubernetes est déclaratif : vous décrivez l’état que vous voulez, et Kubernetes fait tout pour atteindre et maintenir cet état.

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

# --- Boucle de réconciliation ---
ax1 = axes[0]
ax1.set_xlim(0, 10)
ax1.set_ylim(0, 10)
ax1.axis("off")
ax1.set_facecolor("#f8f9fa")
ax1.set_title("Boucle de réconciliation (Control Loop)", fontsize=12, fontweight="bold", pad=10)

# Cercle principal
theta = np.linspace(0, 2 * np.pi, 300)
r = 3.2
cx, cy = 5, 5
ax1.plot(cx + r * np.cos(theta), cy + r * np.sin(theta),
         color="#3b82f6", lw=3, alpha=0.4)

# Étapes sur le cercle
steps = [
    (90, "Observer\nl'état actuel", "#3b82f6"),
    (0, "Comparer\nétat actuel\nvs désiré", "#f97316"),
    (270, "Agir pour\nréduire l'écart", "#22c55e"),
    (180, "Attendre\nla prochaine\nitération", "#a855f7"),
]

for angle_deg, label, color in steps:
    angle_rad = np.radians(angle_deg)
    x = cx + r * np.cos(angle_rad)
    y = cy + r * np.sin(angle_rad)
    circle = plt.Circle((x, y), 0.75, color=color, ec="white", lw=2.5, zorder=4)
    ax1.add_patch(circle)
    ax1.text(x, y, label, ha="center", va="center", fontsize=8.5,
             fontweight="bold", color="white", zorder=5)

# Flèches sur la boucle
for angle_deg in [70, 340, 250, 160]:
    angle_rad = np.radians(angle_deg)
    dx = -np.sin(angle_rad) * 0.4
    dy = np.cos(angle_rad) * 0.4
    x = cx + r * np.cos(angle_rad)
    y = cy + r * np.sin(angle_rad)
    ax1.annotate("", xy=(x + dx, y + dy), xytext=(x - dx, y - dy),
                 arrowprops=dict(arrowstyle="-|>", color="#555", lw=2))

# Centre
ax1.text(cx, cy + 0.3, "Controller", ha="center", va="center",
         fontsize=11, fontweight="bold", color="#1e40af")
ax1.text(cx, cy - 0.3, "∞ en boucle", ha="center", va="center",
         fontsize=9, color="#555", style="italic")

# Exemple concret
ax1.text(5, 1.0, "Exemple : Deployment demande 3 Pods\nController observe 2 Pods → crée 1 Pod",
         ha="center", va="center", fontsize=9, color="#555",
         bbox=dict(boxstyle="round,pad=0.4", facecolor="#fffbeb", edgecolor="#f59e0b"))

# --- Flux kubectl → apiserver → etcd → kubelet ---
ax2 = axes[1]
ax2.set_xlim(0, 10)
ax2.set_ylim(0, 10)
ax2.axis("off")
ax2.set_facecolor("#f8f9fa")
ax2.set_title("Flux : kubectl apply → Pod qui tourne", fontsize=12, fontweight="bold", pad=10)

flow_steps = [
    (5, 9.2, "1. kubectl apply -f deployment.yaml", "#fef3c7", "#d97706"),
    (5, 7.8, "2. kube-apiserver\nvalidation + authentification + admission", "#dbeafe", "#2563eb"),
    (5, 6.4, "3. etcd\nsauvegarde le nouvel objet Deployment", "#f0fdf4", "#16a34a"),
    (5, 5.0, "4. kube-controller-manager\ndétecte un nouveau Deployment, crée un ReplicaSet", "#fff7ed", "#f97316"),
    (5, 3.6, "5. kube-scheduler\nchoisit le node pour chaque Pod Pending", "#fdf4ff", "#a855f7"),
    (5, 2.2, "6. kubelet (sur le node)\ntélécharge l'image et démarre le conteneur", "#f0fdf4", "#16a34a"),
    (5, 0.8, "7. Pod Running ✓\ncontainer runtime → conteneur opérationnel", "#dcfce7", "#15803d"),
]

for x, y, text, bg, border in flow_steps:
    first_line = text.split("\n")[0]
    rest = "\n".join(text.split("\n")[1:])
    box = FancyBboxPatch((x - 4.2, y - 0.45), 8.4, 0.9, boxstyle="round,pad=0.08",
                          facecolor=bg, edgecolor=border, linewidth=1.5)
    ax2.add_patch(box)
    ax2.text(x, y + 0.12, first_line, ha="center", va="center",
             fontsize=9, fontweight="bold", color=border)
    if rest:
        ax2.text(x, y - 0.18, rest, ha="center", va="center",
                 fontsize=8, color="#555")

for i in range(len(flow_steps) - 1):
    y_from = flow_steps[i][1] - 0.45
    y_to = flow_steps[i+1][1] + 0.45
    ax2.annotate("", xy=(5, y_to), xytext=(5, y_from),
                 arrowprops=dict(arrowstyle="-|>", color="#888", lw=1.8))

plt.tight_layout()
plt.show()
_images/2175f9bf01bad44045030765be5e28a900439d5ce02123a17c603f523965fa29.png

Worker nodes en détail#

kubelet : l’agent sur chaque machine#

Le kubelet est le processus qui tourne sur chaque node et fait le lien entre le control plane et le container runtime :

  1. Surveille l’apiserver pour les Pods assignés à son node

  2. Demande au container runtime (containerd) de démarrer/arrêter les conteneurs

  3. Rapporte l’état des Pods à l’apiserver

  4. Exécute les healthchecks (liveness, readiness, startup probes)

kube-proxy : le réseau des Services#

kube-proxy gère les règles réseau qui permettent aux Services Kubernetes de fonctionner. Il configure iptables (ou ipvs) pour router le trafic vers les bons Pods.

Container runtime : containerd et CRI-O#

Docker était le runtime historique, mais Kubernetes a défini une interface standard : la Container Runtime Interface (CRI). Les runtimes modernes sont :

  • containerd : extrait de Docker, très utilisé (EKS, GKE, AKS l’utilisent)

  • CRI-O : conçu spécialement pour Kubernetes (Red Hat OpenShift)

kubectl : le couteau suisse de Kubernetes#

kubectl est l’outil CLI pour interagir avec n’importe quel cluster Kubernetes.

# Voir les clusters configurés
kubectl config get-contexts

# Changer de cluster
kubectl config use-context mon-cluster-prod

# Voir les nodes du cluster
kubectl get nodes
kubectl get nodes -o wide    # Plus de détails (IP, OS, version)

# Informations sur un node
kubectl describe node worker-1

# Lister les namespaces
kubectl get namespaces

# Travailler dans un namespace spécifique
kubectl get pods -n kube-system
kubectl get pods -n mon-app

# Définir le namespace par défaut pour la session
kubectl config set-context --current --namespace=mon-app

# Raccourcis utiles
kubectl get po      # pods
kubectl get svc     # services
kubectl get deploy  # deployments
kubectl get all     # tout dans le namespace courant

Objets Kubernetes : déclaratif vs impératif#

L’approche impérative (à éviter en prod)#

# Impératif : on dit CE QUE FAIRE
kubectl run mon-pod --image=nginx
kubectl create deployment mon-app --image=myapp:v1
kubectl scale deployment mon-app --replicas=5

L’approche déclarative (recommandée)#

# Déclaratif : on décrit L'ÉTAT DÉSIRÉ
kubectl apply -f deployment.yaml
kubectl apply -f ./manifests/   # Tous les YAML d'un dossier
kubectl apply -f https://raw.githubusercontent.com/…/deployment.yaml

# Supprimer ce qui est décrit dans le fichier
kubectl delete -f deployment.yaml

# Voir les différences avant d'appliquer
kubectl diff -f deployment.yaml

Structure d’un objet Kubernetes#

Tout objet Kubernetes a la même structure de base :

apiVersion: apps/v1        # Groupe d'API + version
kind: Deployment           # Type d'objet
metadata:
  name: mon-app            # Nom unique dans le namespace
  namespace: production    # Namespace (isolation logique)
  labels:                  # Tags libres (clé/valeur)
    app: mon-app
    version: v2.1.0
  annotations:             # Métadonnées non-structurées
    kubernetes.io/change-cause: "Mise à jour vers v2.1.0"
spec:                      # ÉTAT DÉSIRÉ — vous définissez ça
  replicas: 3
  selector:
    matchLabels:
      app: mon-app
  template:
    # ... (spec du Pod)
status:                    # ÉTAT ACTUEL — Kubernetes remplit ça
  readyReplicas: 3
  updatedReplicas: 3
  # (champ lu par kubectl, jamais écrit par l'utilisateur)

spec vs status

La distinction spec / status est fondamentale : vous écrivez le spec (ce que vous voulez), Kubernetes écrit le status (ce qui existe réellement). La boucle de réconciliation fait constamment converger status vers spec.

L’API Kubernetes : groupes et versioning#

# Structure de l'API Kubernetes
api_groups = {
    "core (v1)": {
        "color": "#dbeafe",
        "resources": ["Pod", "Service", "ConfigMap", "Secret",
                       "PersistentVolume", "PersistentVolumeClaim",
                       "Namespace", "Node", "ServiceAccount"],
    },
    "apps/v1": {
        "color": "#d1fae5",
        "resources": ["Deployment", "ReplicaSet", "StatefulSet", "DaemonSet"],
    },
    "batch/v1": {
        "color": "#fef3c7",
        "resources": ["Job", "CronJob"],
    },
    "networking.k8s.io/v1": {
        "color": "#fce7f3",
        "resources": ["Ingress", "IngressClass", "NetworkPolicy"],
    },
    "rbac.authorization.k8s.io/v1": {
        "color": "#f3e8ff",
        "resources": ["Role", "ClusterRole", "RoleBinding", "ClusterRoleBinding"],
    },
    "storage.k8s.io/v1": {
        "color": "#fff7ed",
        "resources": ["StorageClass", "VolumeAttachment", "CSIDriver"],
    },
}

fig, ax = plt.subplots(figsize=(14, 6))
ax.set_facecolor("#f8f9fa")
fig.patch.set_facecolor("#f8f9fa")
ax.axis("off")
ax.set_title("Groupes d'API Kubernetes", fontsize=13, fontweight="bold", pad=10)

cols = 3
rows = 2
group_items = list(api_groups.items())
w_box = 14 / cols - 0.2
h_box = 5.5 / rows - 0.2

for idx, (group_name, group_data) in enumerate(group_items):
    col = idx % cols
    row = idx // cols
    x = col * (w_box + 0.2) + 0.1
    y = (rows - 1 - row) * (h_box + 0.2) + 0.2

    box = FancyBboxPatch((x, y), w_box, h_box, boxstyle="round,pad=0.1",
                          facecolor=group_data["color"], edgecolor="#555", linewidth=1.5,
                          transform=ax.transData)
    ax.add_patch(box)
    ax.text(x + w_box/2, y + h_box - 0.25, group_name,
            ha="center", va="center", fontsize=10, fontweight="bold", color="#1e293b")

    for i, resource in enumerate(group_data["resources"]):
        r_col = i % 2
        r_row = i // 2
        rx = x + 0.1 + r_col * (w_box/2 - 0.05)
        ry = y + h_box - 0.65 - r_row * 0.38
        if ry > y + 0.1:
            ax.text(rx, ry, f"• {resource}", ha="left", va="center",
                    fontsize=8.5, color="#374151")

ax.set_xlim(0, 14)
ax.set_ylim(0, 6)
plt.tight_layout()
plt.show()
_images/28a781e0eeae947ce1d00d28f3d1c528ffc9347e27bb3cdf947ec1f8df0fd14c.png

Distributions Kubernetes : où faire tourner K8s ?#

distributions = {
    "Développement local": [
        ("minikube", "VM ou conteneur local\nSimple, officiel\nBonne compatibilité"),
        ("kind", "K8s dans Docker\nTrès rapide (CI)\nMulti-node simulé"),
        ("k3s", "Distribution légère\nRaspberry Pi / Edge\nProduction possible"),
    ],
    "Auto-géré": [
        ("kubeadm", "Outil officiel\nComplexe à maintenir\nFull contrôle"),
        ("k3s", "Facile à installer\nFaible overhead\nSingle-binary"),
        ("RKE2", "Sécurité renforcée\nCIS Benchmark\nRancher"),
    ],
    "Cloud géré (managed)": [
        ("EKS\n(AWS)", "Intégration IAM\nFargate (serverless)\nPrix moyen"),
        ("GKE\n(GCP)", "Le plus mature\nAutopilot mode\nExcellent tooling"),
        ("AKS\n(Azure)", "Intégration AAD\nGratuit control plane\nBon pour .NET"),
    ],
}

fig, axes = plt.subplots(1, 3, figsize=(14, 5.5))
fig.suptitle("Distributions Kubernetes selon le contexte", fontsize=13, fontweight="bold")

cat_colors_dist = {
    "Développement local": "#fef3c7",
    "Auto-géré": "#dcfce7",
    "Cloud géré (managed)": "#dbeafe",
}
cat_borders_dist = {
    "Développement local": "#d97706",
    "Auto-géré": "#16a34a",
    "Cloud géré (managed)": "#2563eb",
}

for ax, (cat_name, distros) in zip(axes, distributions.items()):
    ax.set_facecolor("#f8f9fa")
    ax.axis("off")
    ax.set_title(cat_name, fontsize=11, fontweight="bold",
                 color=cat_borders_dist[cat_name], pad=8)

    for i, (name, desc) in enumerate(distros):
        y_start = 0.88 - i * 0.32
        box = FancyBboxPatch((0.05, y_start - 0.22), 0.9, 0.28,
                              boxstyle="round,pad=0.02",
                              facecolor=cat_colors_dist[cat_name],
                              edgecolor=cat_borders_dist[cat_name], linewidth=1.5,
                              transform=ax.transAxes, clip_on=False)
        ax.add_patch(box)
        ax.text(0.5, y_start + 0.03, name, ha="center", va="center",
                fontsize=10.5, fontweight="bold", color=cat_borders_dist[cat_name],
                transform=ax.transAxes)
        ax.text(0.5, y_start - 0.13, desc, ha="center", va="center",
                fontsize=7.5, color="#374151", transform=ax.transAxes)

plt.tight_layout()
plt.show()
_images/aecabc731820f80735fb795fc9b0680fe03b8e501edc615c46064f4d52d031a9.png

Code Python : simulation de la boucle de réconciliation#

import yaml
from dataclasses import dataclass, field
from typing import Optional
import time as time_module

# ================================================================
# Simulation de la boucle de réconciliation d'un Deployment
# ================================================================

@dataclass
class PodSpec:
    name: str
    image: str
    labels: dict = field(default_factory=dict)
    status: str = "Pending"   # Pending → Running → Terminating → Deleted

@dataclass
class DeploymentSpec:
    name: str
    namespace: str
    replicas: int
    selector: dict
    image: str
    labels: dict = field(default_factory=dict)

class FakeAPIServer:
    """Simule l'API Kubernetes (état stocké dans etcd)."""

    def __init__(self):
        self._pods: dict[str, PodSpec] = {}
        self._deployments: dict[str, DeploymentSpec] = {}
        self._event_log: list[str] = []
        self._pod_counter = 0

    def apply_deployment(self, spec: DeploymentSpec):
        key = f"{spec.namespace}/{spec.name}"
        self._deployments[key] = spec
        self._log(f"APPLY Deployment {key} (replicas={spec.replicas})")

    def get_pods_for_deployment(self, deploy: DeploymentSpec) -> list[PodSpec]:
        """Retourne les Pods correspondant au sélecteur du Deployment."""
        result = []
        for pod in self._pods.values():
            if (pod.labels.get("app") == deploy.selector.get("app") and
                    pod.status not in ("Terminating", "Deleted")):
                result.append(pod)
        return result

    def create_pod(self, deploy: DeploymentSpec) -> PodSpec:
        self._pod_counter += 1
        name = f"{deploy.name}-{self._pod_counter:05d}"
        pod = PodSpec(
            name=name,
            image=deploy.image,
            labels={"app": deploy.selector["app"], "deploy": deploy.name},
            status="Pending",
        )
        self._pods[name] = pod
        self._log(f"  CREATE Pod {name} (image={deploy.image})")
        return pod

    def delete_pod(self, pod: PodSpec):
        pod.status = "Terminating"
        self._log(f"  DELETE Pod {pod.name}")

    def simulate_pod_lifecycle(self):
        """Simule la progression des Pods (Pending → Running / Terminating → Deleted)."""
        for pod in list(self._pods.values()):
            if pod.status == "Pending":
                pod.status = "Running"
            elif pod.status == "Terminating":
                pod.status = "Deleted"

    def _log(self, msg: str):
        self._event_log.append(msg)
        print(msg)

    def status_summary(self):
        counts = defaultdict(int)
        for pod in self._pods.values():
            counts[pod.status] += 1
        return dict(counts)


class DeploymentController:
    """Simule le controller de Deployment (boucle de réconciliation)."""

    def __init__(self, api: FakeAPIServer):
        self.api = api

    def reconcile(self, deploy: DeploymentSpec):
        """Une itération de la boucle de réconciliation."""
        current_pods = self.api.get_pods_for_deployment(deploy)
        current_count = len(current_pods)
        desired_count = deploy.replicas

        print(f"\n[Reconcile] {deploy.namespace}/{deploy.name}")
        print(f"  État désiré  : {desired_count} réplicas")
        print(f"  État actuel  : {current_count} pods (Running/Pending)")

        if current_count < desired_count:
            to_create = desired_count - current_count
            print(f"  → Création de {to_create} pod(s)...")
            for _ in range(to_create):
                self.api.create_pod(deploy)

        elif current_count > desired_count:
            to_delete = current_count - desired_count
            print(f"  → Suppression de {to_delete} pod(s) en excès...")
            for pod in current_pods[:to_delete]:
                self.api.delete_pod(pod)
        else:
            print("  ✓ Rien à faire — état convergé")

        print(f"  Pods totaux : {self.api.status_summary()}")


# ================================================================
# Scénario de démonstration
# ================================================================
print("=" * 60)
print("Simulation de la boucle de réconciliation Kubernetes")
print("=" * 60)

api = FakeAPIServer()
controller = DeploymentController(api)

deploy = DeploymentSpec(
    name="webapp",
    namespace="production",
    replicas=3,
    selector={"app": "webapp"},
    image="myregistry/webapp:v2.1.0",
    labels={"app": "webapp", "version": "v2.1.0"},
)

print("\n--- Étape 1 : Déploiement initial (0 → 3 pods) ---")
api.apply_deployment(deploy)
controller.reconcile(deploy)

print("\n--- Simulation : les pods passent Pending → Running ---")
api.simulate_pod_lifecycle()
print(f"  État pods : {api.status_summary()}")

print("\n--- Étape 2 : Réconciliation — état déjà convergé ---")
controller.reconcile(deploy)

print("\n--- Étape 3 : Scale up (3 → 5 pods) ---")
deploy.replicas = 5
api.apply_deployment(deploy)
controller.reconcile(deploy)
api.simulate_pod_lifecycle()
print(f"  État pods : {api.status_summary()}")

print("\n--- Étape 4 : Scale down (5 → 2 pods) ---")
deploy.replicas = 2
api.apply_deployment(deploy)
controller.reconcile(deploy)
api.simulate_pod_lifecycle()
api.simulate_pod_lifecycle()  # Passe Terminating → Deleted
controller.reconcile(deploy)
print(f"  État pods final : {api.status_summary()}")
============================================================
Simulation de la boucle de réconciliation Kubernetes
============================================================

--- Étape 1 : Déploiement initial (0 → 3 pods) ---
APPLY Deployment production/webapp (replicas=3)

[Reconcile] production/webapp
  État désiré  : 3 réplicas
  État actuel  : 0 pods (Running/Pending)
  → Création de 3 pod(s)...
  CREATE Pod webapp-00001 (image=myregistry/webapp:v2.1.0)
  CREATE Pod webapp-00002 (image=myregistry/webapp:v2.1.0)
  CREATE Pod webapp-00003 (image=myregistry/webapp:v2.1.0)
  Pods totaux : {'Pending': 3}

--- Simulation : les pods passent Pending → Running ---
  État pods : {'Running': 3}

--- Étape 2 : Réconciliation — état déjà convergé ---

[Reconcile] production/webapp
  État désiré  : 3 réplicas
  État actuel  : 3 pods (Running/Pending)
  ✓ Rien à faire — état convergé
  Pods totaux : {'Running': 3}

--- Étape 3 : Scale up (3 → 5 pods) ---
APPLY Deployment production/webapp (replicas=5)

[Reconcile] production/webapp
  État désiré  : 5 réplicas
  État actuel  : 3 pods (Running/Pending)
  → Création de 2 pod(s)...
  CREATE Pod webapp-00004 (image=myregistry/webapp:v2.1.0)
  CREATE Pod webapp-00005 (image=myregistry/webapp:v2.1.0)
  Pods totaux : {'Running': 3, 'Pending': 2}
  État pods : {'Running': 5}

--- Étape 4 : Scale down (5 → 2 pods) ---
APPLY Deployment production/webapp (replicas=2)

[Reconcile] production/webapp
  État désiré  : 2 réplicas
  État actuel  : 5 pods (Running/Pending)
  → Suppression de 3 pod(s) en excès...
  DELETE Pod webapp-00001
  DELETE Pod webapp-00002
  DELETE Pod webapp-00003
  Pods totaux : {'Terminating': 3, 'Running': 2}

[Reconcile] production/webapp
  État désiré  : 2 réplicas
  État actuel  : 2 pods (Running/Pending)
  ✓ Rien à faire — état convergé
  Pods totaux : {'Deleted': 3, 'Running': 2}
  État pods final : {'Deleted': 3, 'Running': 2}
# Parsing d'un manifeste YAML Kubernetes
DEPLOYMENT_YAML = """
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp
  namespace: production
  labels:
    app: webapp
    version: v2.1.0
  annotations:
    kubernetes.io/change-cause: "Déploiement initial de webapp"
spec:
  replicas: 3
  selector:
    matchLabels:
      app: webapp
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  template:
    metadata:
      labels:
        app: webapp
        version: v2.1.0
    spec:
      containers:
        - name: webapp
          image: myregistry/webapp:v2.1.0
          ports:
            - containerPort: 8000
          resources:
            requests:
              cpu: 250m
              memory: 256Mi
            limits:
              cpu: 1000m
              memory: 512Mi
          livenessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 15
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /ready
              port: 8000
            initialDelaySeconds: 5
            periodSeconds: 5
"""

def parse_k8s_manifest(yaml_str: str) -> dict:
    """Parse un manifeste Kubernetes et extrait les informations clés."""
    obj = yaml.safe_load(yaml_str)
    return obj

def summarize_manifest(obj: dict):
    """Affiche un résumé structuré d'un objet Kubernetes."""
    api_ver = obj.get("apiVersion", "?")
    kind = obj.get("kind", "?")
    meta = obj.get("metadata", {})
    spec = obj.get("spec", {})

    print(f"{'=' * 50}")
    print(f"Objet : {kind}")
    print(f"API   : {api_ver}")
    print(f"Nom   : {meta.get('namespace', 'default')}/{meta.get('name', '?')}")
    print(f"Labels: {meta.get('labels', {})}")
    print()

    if kind == "Deployment":
        replicas = spec.get("replicas", 1)
        strategy = spec.get("strategy", {})
        containers = spec.get("template", {}).get("spec", {}).get("containers", [])

        print(f"Réplicas  : {replicas}")
        print(f"Stratégie : {strategy.get('type', 'RollingUpdate')}")
        if "rollingUpdate" in strategy:
            ru = strategy["rollingUpdate"]
            print(f"  maxUnavailable: {ru.get('maxUnavailable')}")
            print(f"  maxSurge      : {ru.get('maxSurge')}")
        print()
        print(f"Conteneurs ({len(containers)}) :")
        for c in containers:
            res = c.get("resources", {})
            req = res.get("requests", {})
            lim = res.get("limits", {})
            print(f"  [{c['name']}]")
            print(f"    Image   : {c['image']}")
            print(f"    Ports   : {[p['containerPort'] for p in c.get('ports', [])]}")
            print(f"    Requests: CPU={req.get('cpu','?')}, Mem={req.get('memory','?')}")
            print(f"    Limits  : CPU={lim.get('cpu','?')}, Mem={lim.get('memory','?')}")
            probes = [p for p in ("livenessProbe", "readinessProbe", "startupProbe") if p in c]
            print(f"    Probes  : {', '.join(probes) if probes else 'aucune'}")

manifest = parse_k8s_manifest(DEPLOYMENT_YAML)
summarize_manifest(manifest)
==================================================
Objet : Deployment
API   : apps/v1
Nom   : production/webapp
Labels: {'app': 'webapp', 'version': 'v2.1.0'}

Réplicas  : 3
Stratégie : RollingUpdate
  maxUnavailable: 1
  maxSurge      : 1

Conteneurs (1) :
  [webapp]
    Image   : myregistry/webapp:v2.1.0
    Ports   : [8000]
    Requests: CPU=250m, Mem=256Mi
    Limits  : CPU=1000m, Mem=512Mi
    Probes  : livenessProbe, readinessProbe

Points clés à retenir#

Résumé du chapitre

L’architecture Kubernetes en 7 points :

  1. Control plane : apiserver (API centrale), etcd (état), scheduler (placement), controller-manager (réconciliation)

  2. Worker nodes : kubelet (agent), kube-proxy (réseau), container runtime (containerd)

  3. Déclaratif : vous décrivez l’état désiré (spec) ; Kubernetes travaille pour l’atteindre (status)

  4. Boucle de réconciliation : chaque controller surveille des objets et agit pour converger vers l’état désiré — en continu

  5. kubectl : CLI universel ; apply -f pour les changements, get/describe pour observer

  6. Namespaces : isolation logique au sein d’un cluster (pas une isolation de sécurité forte)

  7. Tout est une API : Pods, Deployments, Services… sont des objets REST stockés dans etcd

La phrase clé : « Kubernetes doesn’t run containers, it manages desired state. »