Architecture Kubernetes — Le chef d’orchestre des conteneurs#
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 |
|
Manifestes déclaratifs, état désiré maintenu |
Gestion des secrets |
Manuelle, risquée |
Objets |
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 :
Le control plane (cerveau du cluster) — gère l’état désiré
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)
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→ apiserverkubelet (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()
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 :
Surveille l’apiserver pour les Pods assignés à son node
Demande au container runtime (containerd) de démarrer/arrêter les conteneurs
Rapporte l’état des Pods à l’apiserver
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()
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()
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 :
Control plane : apiserver (API centrale), etcd (état), scheduler (placement), controller-manager (réconciliation)
Worker nodes : kubelet (agent), kube-proxy (réseau), container runtime (containerd)
Déclaratif : vous décrivez l’état désiré (
spec) ; Kubernetes travaille pour l’atteindre (status)Boucle de réconciliation : chaque controller surveille des objets et agit pour converger vers l’état désiré — en continu
kubectl : CLI universel ;
apply -fpour les changements,get/describepour observerNamespaces : isolation logique au sein d’un cluster (pas une isolation de sécurité forte)
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. »