---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# Architecture Kubernetes — Le chef d'orchestre des conteneurs

```{code-cell} python
:tags: [hide-input]
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) |

```{admonition} Analogie — Le chef d'orchestre
:class: tip
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

```{code-cell} python
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()
```

## 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…

```{admonition} Sauvegardez etcd !
:class: warning
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.

```{code-cell} python
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()
```

## 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.

```bash
# 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)

```bash
# 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)

```bash
# 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 :

```yaml
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)
```

```{admonition} spec vs status
:class: note
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

```{code-cell} python
# 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()
```

## Distributions Kubernetes : où faire tourner K8s ?

```{code-cell} python
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()
```

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

```{code-cell} python
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()}")
```

```{code-cell} python
# 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)
```

## Points clés à retenir

```{admonition} Résumé du chapitre
:class: important
**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."*
```
