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

# 11 — Services et réseau Kubernetes

```{code-cell} python
:tags: [hide-input]
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.patheffects as pe
from matplotlib.patches import FancyArrowPatch, FancyBboxPatch, Circle
import numpy as np
import pandas as pd
import seaborn as sns
sns.set_theme(style="whitegrid", palette="muted")
```

## Le problème des IPs éphémères

Imaginons un Pod comme un locataire d'appartement. À chaque fois qu'il déménage (redémarrage, mise à jour, crash), il reçoit une nouvelle adresse IP. Si d'autres Pods communiquaient directement avec cette IP, ils perdraient le contact à chaque redémarrage.

C'est exactement le problème que résout le **Service** Kubernetes.

```{admonition} Analogie : la boîte postale
:class: tip
Un Service, c'est comme une boîte postale à adresse fixe. Peu importe combien de fois le destinataire déménage (les Pods changent d'IP), le courrier arrive toujours à la bonne boîte. C'est le Service qui fait le tri et achemine vers les Pods actifs.
```

## Le Service Kubernetes

Un Service est une abstraction stable qui définit :
- **Un sélecteur de labels** : quels Pods sont ciblés
- **Une ClusterIP virtuelle** : adresse stable, inchangée tant que le Service existe
- **Des ports** : mapping port du Service → port du Pod

```yaml
apiVersion: v1
kind: Service
metadata:
  name: mon-app
  namespace: production
spec:
  selector:
    app: mon-app          # Sélectionne les Pods avec ce label
    version: stable
  ports:
    - name: http
      port: 80            # Port du Service (stable)
      targetPort: 8080    # Port du Pod (conteneur)
      protocol: TCP
  type: ClusterIP
```

### kube-proxy : le routeur de Kubernetes

Sur chaque nœud tourne un composant essentiel : **kube-proxy**. Son rôle est de programmer les règles réseau pour que le trafic vers une ClusterIP soit redirigé vers l'un des Pods réels.

```{admonition} Comment fonctionne kube-proxy ?
:class: note
kube-proxy surveille l'API Server. Dès qu'un Service est créé ou modifié, il met à jour les règles du noyau Linux (iptables ou IPVS) sur le nœud. Le trafic ne passe jamais "par" kube-proxy en production — ce composant ne fait que configurer les règles, le noyau fait le routage directement.
```

```{code-cell} python
:tags: [hide-input]
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# --- Schéma gauche : flux requête ClusterIP ---
ax = axes[0]
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis('off')
ax.set_title("Flux d'une requête via ClusterIP", fontsize=13, fontweight='bold', pad=15)

def boite(ax, x, y, w, h, label, sublabel="", color="#4A90D9", textcolor="white", fontsize=10):
    box = FancyBboxPatch((x - w/2, y - h/2), w, h,
                          boxstyle="round,pad=0.1", linewidth=1.5,
                          edgecolor="white", facecolor=color, alpha=0.9)
    ax.add_patch(box)
    ax.text(x, y + (0.15 if sublabel else 0), label, ha='center', va='center',
            color=textcolor, fontsize=fontsize, fontweight='bold')
    if sublabel:
        ax.text(x, y - 0.35, sublabel, ha='center', va='center',
                color=textcolor, fontsize=8, alpha=0.85)

def fleche(ax, x1, y1, x2, y2, label="", color="#333"):
    ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
                arrowprops=dict(arrowstyle="-|>", color=color, lw=2))
    if label:
        mx, my = (x1+x2)/2, (y1+y2)/2
        ax.text(mx + 0.15, my, label, fontsize=8, color=color, ha='left')

boite(ax, 2, 8.5, 3.2, 0.9, "Client Pod", "10.244.1.5", "#6C63FF")
fleche(ax, 2, 8.05, 2, 7.15, "dst: 10.96.0.1:80")
boite(ax, 2, 6.7, 3.2, 0.9, "iptables / IPVS", "(noyau Linux)", "#E67E22")
ax.text(5.5, 6.7, "ClusterIP\n10.96.0.1:80", ha='center', va='center',
        fontsize=9, color="#E67E22",
        bbox=dict(boxstyle="round,pad=0.3", facecolor="#FEF9E7", edgecolor="#E67E22", lw=1.5))
ax.annotate("", xy=(4.0, 6.7), xytext=(3.4, 6.7),
            arrowprops=dict(arrowstyle="-|>", color="#E67E22", lw=1.5, linestyle='dashed'))

fleche(ax, 2, 6.25, 2, 5.35, "DNAT →")
boite(ax, 2, 4.9, 3.2, 0.9, "Pod A", "10.244.2.3:8080", "#27AE60")
boite(ax, 5, 4.9, 3.2, 0.9, "Pod B", "10.244.3.7:8080", "#27AE60")
boite(ax, 3.5, 3.3, 3.2, 0.9, "Pod C", "10.244.1.9:8080", "#27AE60")

fleche(ax, 2, 6.25, 5, 5.35, "ou")
fleche(ax, 2, 6.25, 3.5, 3.75, "ou")

ax.text(2, 2.5, "Load balancing aléatoire\n(ou IPVS : round-robin, least-conn...)",
        ha='center', va='center', fontsize=8.5, color="#555",
        bbox=dict(boxstyle="round,pad=0.3", facecolor="#ECF0F1", edgecolor="#BDC3C7"))

# --- Schéma droit : Endpoints ---
ax2 = axes[1]
ax2.set_xlim(0, 10)
ax2.set_ylim(0, 10)
ax2.axis('off')
ax2.set_title("Service, Endpoints et Pods", fontsize=13, fontweight='bold', pad=15)

boite(ax2, 5, 9, 5, 0.9, "Service: mon-app", "ClusterIP: 10.96.0.1:80", "#4A90D9")
boite(ax2, 5, 7.2, 5, 0.9, "Endpoints", "10.244.2.3:8080, 10.244.3.7:8080, 10.244.1.9:8080", "#8E44AD", fontsize=8)
fleche(ax2, 5, 8.55, 5, 7.65, "surveille les Pods via sélecteur")

for i, (px, py, name, ip) in enumerate([
    (2, 5.2, "Pod A", "10.244.2.3"), (5, 5.2, "Pod B", "10.244.3.7"), (8, 5.2, "Pod C", "10.244.1.9")
]):
    boite(ax2, px, py, 2.5, 1.1, name, ip, "#27AE60")
    fleche(ax2, 5, 6.75, px, 5.75)

ax2.text(5, 3.5,
    "kube-proxy met à jour les règles\nà chaque changement de Pod\n(crash, scale, rolling update)",
    ha='center', va='center', fontsize=9, color="#555",
    bbox=dict(boxstyle="round,pad=0.4", facecolor="#EBF5FB", edgecolor="#4A90D9"))

plt.tight_layout()
plt.savefig("11_flux_clusterip.png", dpi=120, bbox_inches='tight')
plt.show()
```

## Les quatre types de Services

Kubernetes propose quatre saveurs de Service, adaptées à des cas d'usage différents.

```{code-cell} python
:tags: [hide-input]
fig, axes = plt.subplots(2, 2, figsize=(15, 11))
fig.suptitle("Les quatre types de Services Kubernetes", fontsize=15, fontweight='bold', y=1.01)

couleurs = {
    "ClusterIP": "#4A90D9",
    "NodePort": "#E67E22",
    "LoadBalancer": "#27AE60",
    "ExternalName": "#8E44AD",
}

def draw_cluster(ax, title, color, description, extra_fn=None):
    ax.set_xlim(0, 12)
    ax.set_ylim(0, 9)
    ax.axis('off')
    ax.set_title(title, fontsize=12, fontweight='bold', color=color, pad=10)
    # Fond cluster
    cluster_bg = FancyBboxPatch((1, 1), 10, 7, boxstyle="round,pad=0.2",
                                 linewidth=2, edgecolor=color, facecolor=color, alpha=0.07)
    ax.add_patch(cluster_bg)
    ax.text(6, 7.7, "Cluster Kubernetes", ha='center', fontsize=9, color=color, style='italic')
    ax.text(6, 0.4, description, ha='center', fontsize=8.5, color="#444",
            bbox=dict(boxstyle="round,pad=0.3", facecolor="white", edgecolor=color, alpha=0.8))
    if extra_fn:
        extra_fn(ax, color)

def clusterip_fn(ax, color):
    boite(ax, 6, 5.5, 3, 0.9, "Service ClusterIP", "10.96.42.1:80", color)
    boite(ax, 4, 3.2, 2.2, 0.85, "Pod A", "10.244.1.2", "#27AE60")
    boite(ax, 8, 3.2, 2.2, 0.85, "Pod B", "10.244.1.3", "#27AE60")
    fleche(ax, 6, 5.05, 4, 3.63)
    fleche(ax, 6, 5.05, 8, 3.63)
    boite(ax, 6, 7.0, 2.5, 0.75, "Client interne", "", "#6C63FF", fontsize=9)
    fleche(ax, 6, 6.63, 6, 5.95)
    ax.text(2.5, 6.5, "Inaccessible\ndepuis l'extérieur", ha='center', fontsize=8,
            color="#E74C3C", bbox=dict(boxstyle="round,pad=0.2", facecolor="#FDEDEC", edgecolor="#E74C3C"))

def nodeport_fn(ax, color):
    # Extérieur
    ax.add_patch(FancyBboxPatch((0.1, 6.8), 2, 1.3, boxstyle="round,pad=0.1",
                                 facecolor="#F8F9FA", edgecolor="#95A5A6", lw=1.5))
    ax.text(1.1, 7.45, "Internet /\nClient ext.", ha='center', fontsize=8, color="#555")
    fleche(ax, 2.1, 7.45, 3.5, 7.45, f":30080")
    # Node
    ax.add_patch(FancyBboxPatch((3.5, 5.8), 7, 2.5, boxstyle="round,pad=0.1",
                                 facecolor=color, alpha=0.1, edgecolor=color, lw=1.5))
    ax.text(7, 8.1, "Node (IP: 192.168.1.10)", ha='center', fontsize=9, color=color)
    boite(ax, 7, 7.05, 3, 0.85, "NodePort :30080", "→ Service :80", color)
    boite(ax, 5, 3.5, 2.2, 0.85, "Pod A", "", "#27AE60")
    boite(ax, 9, 3.5, 2.2, 0.85, "Pod B", "", "#27AE60")
    fleche(ax, 7, 6.63, 5, 3.93)
    fleche(ax, 7, 6.63, 9, 3.93)

def lb_fn(ax, color):
    ax.add_patch(FancyBboxPatch((0.1, 7.0), 2.2, 1.2, boxstyle="round,pad=0.1",
                                 facecolor="#F8F9FA", edgecolor="#95A5A6", lw=1.5))
    ax.text(1.2, 7.6, "Internet", ha='center', fontsize=9, color="#555")
    fleche(ax, 2.3, 7.6, 3.5, 7.6)
    boite(ax, 5.5, 7.6, 3.2, 0.85, "Cloud LB", "34.105.12.77", "#E74C3C")
    fleche(ax, 7.1, 7.6, 8.5, 7.6)
    ax.text(9.8, 7.6, "Cloud\nprovider", ha='center', fontsize=8, color="#E74C3C")
    boite(ax, 5.5, 5.8, 3, 0.85, "Service LB", "10.96.5.1:80", color)
    fleche(ax, 5.5, 7.18, 5.5, 6.23)
    boite(ax, 3.5, 3.5, 2, 0.8, "Pod A", "", "#27AE60")
    boite(ax, 7.5, 3.5, 2, 0.8, "Pod B", "", "#27AE60")
    fleche(ax, 5.5, 5.38, 3.5, 3.9)
    fleche(ax, 5.5, 5.38, 7.5, 3.9)

def extname_fn(ax, color):
    boite(ax, 5.5, 7.0, 4, 0.85, "Service ExternalName", "type: ExternalName", color)
    ax.text(5.5, 6.2, 'externalName:\n  "db.externe.example.com"',
            ha='center', fontsize=9, color="#555",
            bbox=dict(boxstyle="round,pad=0.3", facecolor="white", edgecolor=color))
    boite(ax, 3, 4.5, 2.5, 0.85, "Pod client", "", "#6C63FF")
    fleche(ax, 3, 4.07, 5.5, 6.58, "DNS CNAME")
    ax.add_patch(FancyBboxPatch((7.5, 4.0), 3, 1.2, boxstyle="round,pad=0.1",
                                 facecolor="#F8F9FA", edgecolor="#E74C3C", lw=1.5))
    ax.text(9, 4.6, "Service\nextérieur", ha='center', fontsize=9, color="#E74C3C")
    fleche(ax, 7.5, 4.6, 7.5, 6.58)

draw_cluster(axes[0, 0], "ClusterIP (défaut) — accès interne uniquement",
             couleurs["ClusterIP"],
             "Usage : communication entre microservices dans le cluster",
             clusterip_fn)

draw_cluster(axes[0, 1], "NodePort — exposition sur un port du nœud",
             couleurs["NodePort"],
             "Usage : accès direct depuis l'extérieur (dev/test) — port 30000-32767",
             nodeport_fn)

draw_cluster(axes[1, 0], "LoadBalancer — via le cloud provider",
             couleurs["LoadBalancer"],
             "Usage : exposition en production sur un cloud (GKE, EKS, AKS…)",
             lb_fn)

draw_cluster(axes[1, 1], "ExternalName — alias DNS vers l'extérieur",
             couleurs["ExternalName"],
             "Usage : pointer vers une base de données ou API externe par nom DNS",
             extname_fn)

plt.tight_layout()
plt.savefig("11_types_services.png", dpi=120, bbox_inches='tight')
plt.show()
```

### Comparatif des types de Services

| Type | Accès | ClusterIP ? | Cas d'usage |
|------|-------|-------------|-------------|
| ClusterIP | Interne uniquement | Oui (virtuelle) | Communication inter-services |
| NodePort | NodeIP:30000-32767 | Oui + NodePort | Dev/test, accès direct |
| LoadBalancer | IP publique via cloud | Oui + NodePort + LB | Production sur cloud |
| ExternalName | DNS CNAME | Non | Alias vers services externes |

## DNS Kubernetes : CoreDNS

Kubernetes embarque un serveur DNS interne : **CoreDNS**. Il permet de joindre un Service par nom plutôt que par IP.

Le format complet d'un nom DNS de Service est :

```
<nom-service>.<namespace>.svc.cluster.local
```

Par exemple, `mon-app.production.svc.cluster.local` résout vers la ClusterIP du Service `mon-app` dans le namespace `production`.

```{admonition} Raccourcis DNS
:class: tip
Depuis le même namespace, on peut utiliser juste `mon-app` (sans suffixe). Depuis un autre namespace, il faut au minimum `mon-app.production`. Le suffixe complet `.svc.cluster.local` est toujours valide quel que soit le contexte.
```

```bash
# Tester la résolution DNS depuis un Pod
kubectl run -it --rm dns-test --image=busybox --restart=Never -- nslookup mon-app.production

# Résultat attendu :
# Server: 10.96.0.10 (CoreDNS)
# Address: 10.96.0.10:53
# Name: mon-app.production.svc.cluster.local
# Address: 10.96.42.1
```

## Endpoints et EndpointSlices

Quand un Service est créé avec un sélecteur, Kubernetes crée automatiquement un objet **Endpoints** qui liste les IPs et ports des Pods correspondants.

```bash
# Voir les Endpoints d'un Service
kubectl get endpoints mon-app -n production

# NAME      ENDPOINTS                                      AGE
# mon-app   10.244.1.2:8080,10.244.2.5:8080,10.244.3.1:8080   5d
```

Pour les clusters de grande taille, les **EndpointSlices** (depuis K8s 1.21) découpent les Endpoints en tranches de 100 entrées maximum pour améliorer les performances.

```yaml
# Exemple d'EndpointSlice (géré automatiquement par K8s)
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: mon-app-abc12
  labels:
    kubernetes.io/service-name: mon-app
addressType: IPv4
ports:
  - name: http
    protocol: TCP
    port: 8080
endpoints:
  - addresses: ["10.244.1.2"]
    conditions:
      ready: true
    nodeName: node-1
  - addresses: ["10.244.2.5"]
    conditions:
      ready: true
    nodeName: node-2
```

## kube-proxy : trois modes de fonctionnement

```{code-cell} python
:tags: [hide-input]
fig, axes = plt.subplots(1, 3, figsize=(15, 6))
fig.suptitle("Modes de kube-proxy", fontsize=14, fontweight='bold')

modes = [
    {
        "name": "iptables",
        "color": "#E67E22",
        "desc": "Mode par défaut",
        "pros": ["Stable, largement utilisé", "Intégré au noyau Linux", "Pas de dépendance externe"],
        "cons": ["Règles linéaires O(n)", "Lent avec >10k Services", "Difficile à déboguer"],
        "detail": "Chaînes iptables KUBE-SVC-*\nKUBE-SEP-* par endpoint\nDNAT vers Pod réel"
    },
    {
        "name": "IPVS",
        "color": "#4A90D9",
        "desc": "Mode haute performance",
        "pros": ["Table de hash O(1)", "Algorithmes LB riches", "Meilleures perfs >1000 svc"],
        "cons": ["Nécessite kernel modules", "ipvsadm requis", "Moins répandu"],
        "detail": "Algorithmes : rr, lc,\ndh, sh, sed, nq\nHash table noyau"
    },
    {
        "name": "nftables",
        "color": "#8E44AD",
        "desc": "Futur (K8s 1.31+)",
        "pros": ["Successeur d'iptables", "API atomique", "Meilleures perfs"],
        "cons": ["Encore expérimental", "Kernel récent requis", "Pas encore par défaut"],
        "detail": "Remplace iptables\nAPI noyau moderne\nGestion atomique des règles"
    }
]

for ax, mode in zip(axes, modes):
    ax.set_xlim(0, 10)
    ax.set_ylim(0, 12)
    ax.axis('off')

    # Titre
    ax.add_patch(FancyBboxPatch((0.5, 10.5), 9, 1.3, boxstyle="round,pad=0.1",
                                 facecolor=mode["color"], alpha=0.9, edgecolor='none'))
    ax.text(5, 11.2, mode["name"], ha='center', va='center', fontsize=14,
            fontweight='bold', color='white')
    ax.text(5, 10.75, mode["desc"], ha='center', va='center', fontsize=9, color='white', alpha=0.9)

    # Avantages
    ax.text(5, 9.9, "Avantages", ha='center', fontsize=10, fontweight='bold',
            color="#27AE60")
    for i, pro in enumerate(mode["pros"]):
        ax.text(1, 9.3 - i*0.65, f"+ {pro}", fontsize=9, color="#27AE60")

    # Inconvénients
    ax.text(5, 7.2, "Inconvénients", ha='center', fontsize=10, fontweight='bold',
            color="#E74C3C")
    for i, con in enumerate(mode["cons"]):
        ax.text(1, 6.6 - i*0.65, f"- {con}", fontsize=9, color="#E74C3C")

    # Détail technique
    ax.add_patch(FancyBboxPatch((0.5, 1.5), 9, 3.2, boxstyle="round,pad=0.2",
                                 facecolor=mode["color"], alpha=0.1, edgecolor=mode["color"], lw=1))
    ax.text(5, 4.5, "Mécanisme interne", ha='center', fontsize=9, fontweight='bold',
            color=mode["color"])
    ax.text(5, 3.1, mode["detail"], ha='center', va='center', fontsize=9,
            color="#333", family='monospace')

plt.tight_layout()
plt.savefig("11_kube_proxy_modes.png", dpi=120, bbox_inches='tight')
plt.show()
```

## Simulation d'un load balancer kube-proxy (iptables)

Pour comprendre comment kube-proxy programme les règles iptables, simulons en Python le processus de sélection d'un endpoint.

```{code-cell} python
import random
import hashlib
import json
from collections import defaultdict

# Simulation des règles iptables générées par kube-proxy
# pour un Service avec 3 Pods

class KubeProxySimulator:
    """Simule les règles iptables de kube-proxy pour un Service."""

    def __init__(self, service_name, cluster_ip, port):
        self.service_name = service_name
        self.cluster_ip = cluster_ip
        self.port = port
        self.endpoints = []
        self.rules = {}  # Simule les chaînes iptables KUBE-SVC-*
        self._stats = defaultdict(int)

    def add_endpoint(self, pod_name, pod_ip, pod_port):
        """Ajoute un endpoint (Pod prêt à recevoir du trafic)."""
        self.endpoints.append({
            "pod": pod_name,
            "ip": pod_ip,
            "port": pod_port,
        })
        self._build_iptables_rules()

    def remove_endpoint(self, pod_name):
        """Retire un endpoint (Pod crashé ou en cours d'arrêt)."""
        self.endpoints = [e for e in self.endpoints if e["pod"] != pod_name]
        self._build_iptables_rules()

    def _build_iptables_rules(self):
        """
        Reconstruit les règles iptables.
        iptables utilise une probabilité 1/n pour chaque endpoint :
        - 1er endpoint : probabilité 1/3
        - 2ème endpoint : probabilité 1/2 (des 2/3 restants)
        - 3ème endpoint : probabilité 1/1 (le reste)
        """
        n = len(self.endpoints)
        if n == 0:
            self.rules = {}
            return

        rules = []
        for i, ep in enumerate(self.endpoints):
            # Règle KUBE-SEP-* (Service EndPoint)
            sep_name = f"KUBE-SEP-{hashlib.md5(ep['ip'].encode()).hexdigest()[:8].upper()}"
            remaining = n - i
            probability = round(1.0 / remaining, 4)

            rules.append({
                "chain": f"KUBE-SVC-{self.service_name[:8].upper()}",
                "sep_chain": sep_name,
                "probability": probability,
                "dnat_target": f"{ep['ip']}:{ep['port']}",
                "pod": ep["pod"],
            })

        self.rules = {
            "service_chain": f"KUBE-SVC-{self.service_name[:8].upper()}",
            "cluster_ip": f"{self.cluster_ip}:{self.port}",
            "endpoints": rules,
        }

    def route_packet(self, src_ip="10.244.0.1"):
        """Simule le routage d'un paquet via les règles iptables."""
        if not self.endpoints:
            return None, "REJECT — aucun endpoint disponible"

        # Sélection probabiliste (comme iptables statistic --mode random)
        n = len(self.endpoints)
        selected = None
        for i, ep in enumerate(self.endpoints):
            remaining = n - i
            prob = 1.0 / remaining
            if random.random() < prob:
                selected = ep
                break

        if not selected:
            selected = self.endpoints[-1]

        self._stats[selected["pod"]] += 1
        return selected, f"DNAT {self.cluster_ip}:{self.port} → {selected['ip']}:{selected['port']}"

    def print_rules(self):
        """Affiche les règles iptables simulées."""
        if not self.rules:
            print("Aucune règle (pas d'endpoint)")
            return
        print(f"\n{'='*60}")
        print(f"Chaîne : {self.rules['service_chain']}")
        print(f"ClusterIP : {self.rules['cluster_ip']}")
        print(f"{'='*60}")
        for rule in self.rules["endpoints"]:
            print(f"  -A {rule['chain']} -m statistic --mode random "
                  f"--probability {rule['probability']:.4f} "
                  f"-j {rule['sep_chain']}")
            print(f"     → DNAT vers {rule['dnat_target']} ({rule['pod']})")
        print(f"{'='*60}\n")


# Création du simulateur
sim = KubeProxySimulator("mon-app", "10.96.0.1", 80)

# Ajout des Pods initiaux
sim.add_endpoint("pod-a", "10.244.1.2", 8080)
sim.add_endpoint("pod-b", "10.244.2.5", 8080)
sim.add_endpoint("pod-c", "10.244.3.1", 8080)

print("=== Règles iptables générées par kube-proxy ===")
sim.print_rules()

# Simulation de 300 requêtes
print("Simulation de 300 requêtes...")
for _ in range(300):
    sim.route_packet()

print("Distribution du trafic :")
total = sum(sim._stats.values())
for pod, count in sorted(sim._stats.items()):
    pct = count / total * 100
    bar = "█" * int(pct / 2)
    print(f"  {pod:10s}: {count:4d} ({pct:.1f}%) {bar}")

# Simulation d'un crash de pod-b
print("\n--- Pod-b crash ! Mise à jour des règles... ---")
sim.remove_endpoint("pod-b")
sim.print_rules()

print("Simulation de 100 requêtes supplémentaires (sans pod-b)...")
sim._stats.clear()
for _ in range(100):
    sim.route_packet()

print("Distribution après crash de pod-b :")
total = sum(sim._stats.values())
for pod, count in sorted(sim._stats.items()):
    pct = count / total * 100
    bar = "█" * int(pct / 2)
    print(f"  {pod:10s}: {count:4d} ({pct:.1f}%) {bar}")
```

## Calcul du hash IPVS

En mode IPVS, kube-proxy utilise des tables de hash pour une sélection O(1) des endpoints.

```{code-cell} python
import hashlib
import struct

def ipvs_hash_key(src_ip: str, dst_ip: str, dst_port: int, protocol: int = 6) -> int:
    """
    Simule le calcul de clé de hash IPVS pour le load balancing.
    En mode 'sh' (Source Hash), le même client va toujours
    vers le même serveur (session persistence).
    """
    # Conversion IP → entier 32 bits
    def ip_to_int(ip):
        parts = [int(p) for p in ip.split('.')]
        return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]

    src_int = ip_to_int(src_ip)
    dst_int = ip_to_int(dst_ip)

    # Clé combinée (simplifié par rapport à l'implémentation réelle)
    key_bytes = struct.pack(">IIIH", src_int, dst_int, 0, dst_port)
    hash_val = int(hashlib.md5(key_bytes).hexdigest(), 16)
    return hash_val

# Test : 5 clients, 3 serveurs
clients = [f"10.244.{i}.{j}" for i in range(1, 3) for j in range(1, 4)]
servers = [
    {"name": "pod-a", "ip": "10.244.10.1"},
    {"name": "pod-b", "ip": "10.244.10.2"},
    {"name": "pod-c", "ip": "10.244.10.3"},
]

print("Mode IPVS 'sh' (Source Hash) — persistance de session :")
print(f"{'Client IP':<18} {'Hash (mod 3)':<15} {'Serveur sélectionné'}")
print("-" * 55)

assignments = {}
for client in clients:
    h = ipvs_hash_key(client, "10.96.0.1", 80)
    server_idx = h % len(servers)
    server = servers[server_idx]
    assignments[client] = server["name"]
    print(f"  {client:<16}  {h % 1000:>8} mod 3 = {server_idx}   → {server['name']} ({server['ip']})")

print("\nLe même client va TOUJOURS vers le même Pod (session persistence).")
print("Utile pour : paniers e-commerce, sessions authentifiées, WebSockets.")
```

## NetworkPolicy : isolation réseau entre Pods

Par défaut, **tous les Pods dans un cluster Kubernetes peuvent communiquer entre eux**. C'est pratique au démarrage, mais dangereux en production. Les **NetworkPolicy** permettent de définir des règles de pare-feu au niveau Pod.

```{admonition} NetworkPolicy nécessite un CNI compatible
:class: warning
Les NetworkPolicy ne fonctionnent que si le plugin CNI installé les supporte. Flannel (simple overlay) ne supporte PAS les NetworkPolicy. Calico, Cilium, Weave Net, et Antrea les supportent.
```

```yaml
# Politique : le Pod "backend" n'accepte du trafic entrant
# que depuis les Pods "frontend" du même namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-ingress
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend          # Politique appliquée à ces Pods
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend  # Autorise depuis les Pods frontend
      ports:
        - protocol: TCP
          port: 8080
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              name: monitoring  # Autorise vers le namespace monitoring
      ports:
        - protocol: TCP
          port: 9090
    - to: []                    # DNS interne (CoreDNS)
      ports:
        - protocol: UDP
          port: 53
```

```{code-cell} python
:tags: [hide-input]
fig, axes = plt.subplots(1, 2, figsize=(15, 7))
fig.suptitle("NetworkPolicy : isolation réseau des Pods", fontsize=14, fontweight='bold')

# --- Sans NetworkPolicy ---
ax = axes[0]
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis('off')
ax.set_title("Sans NetworkPolicy\n(par défaut : tout est autorisé)", fontsize=11,
             fontweight='bold', color="#E74C3C")

pods_left = [
    (2, 8, "frontend", "#6C63FF"),
    (5, 8, "backend", "#E67E22"),
    (8, 8, "database", "#27AE60"),
    (2, 5, "monitoring", "#4A90D9"),
    (8, 5, "attaquant ?", "#E74C3C"),
]
for (x, y, name, color) in pods_left:
    boite(ax, x, y, 2.2, 0.9, name, "", color)

# Flèches dans tous les sens
connections = [
    (2,8,5,8), (5,8,8,8), (2,8,8,5), (8,5,5,8), (8,5,8,8), (8,5,2,8)
]
for (x1,y1,x2,y2) in connections:
    ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
                arrowprops=dict(arrowstyle="<->", color="#E74C3C", lw=1.5, alpha=0.6))

ax.text(5, 3, "Tout le monde parle\nà tout le monde !", ha='center', fontsize=11,
        color="#E74C3C", fontweight='bold',
        bbox=dict(boxstyle="round,pad=0.4", facecolor="#FDEDEC", edgecolor="#E74C3C"))

# --- Avec NetworkPolicy ---
ax2 = axes[1]
ax2.set_xlim(0, 10)
ax2.set_ylim(0, 10)
ax2.axis('off')
ax2.set_title("Avec NetworkPolicy\n(moindre privilège réseau)", fontsize=11,
              fontweight='bold', color="#27AE60")

pods_right = [
    (2, 8, "frontend", "#6C63FF"),
    (5, 8, "backend", "#E67E22"),
    (8, 8, "database", "#27AE60"),
    (2, 5, "monitoring", "#4A90D9"),
    (8, 5, "attaquant ?", "#E74C3C"),
]
for (x, y, name, color) in pods_right:
    boite(ax2, x, y, 2.2, 0.9, name, "", color)

# Flèches autorisées uniquement
ok_connections = [(2,8,5,8,"→ :8080"), (5,8,8,8,"→ :5432"), (2,5,5,8,"→ /metrics")]
for (x1,y1,x2,y2,label) in ok_connections:
    ax2.annotate("", xy=(x2, y2), xytext=(x1, y1),
                arrowprops=dict(arrowstyle="-|>", color="#27AE60", lw=2))
    mx, my = (x1+x2)/2, (y1+y2)/2
    ax2.text(mx, my+0.25, label, fontsize=8, color="#27AE60", ha='center')

# Flèches bloquées
blocked = [(8,5,8,8), (8,5,5,8), (8,5,2,8)]
for (x1,y1,x2,y2) in blocked:
    ax2.annotate("", xy=(x2, y2), xytext=(x1, y1),
                arrowprops=dict(arrowstyle="-|>", color="#E74C3C", lw=1.5,
                                linestyle='dashed', alpha=0.5))
    mx, my = (x1+x2)/2, (y1+y2)/2
    ax2.text(mx+0.3, my, "✗", fontsize=14, color="#E74C3C", ha='center')

ax2.text(5, 3, "Seul le trafic explicitement\nautorisé est accepté", ha='center',
         fontsize=11, color="#27AE60", fontweight='bold',
         bbox=dict(boxstyle="round,pad=0.4", facecolor="#EAFAF1", edgecolor="#27AE60"))

plt.tight_layout()
plt.savefig("11_networkpolicy.png", dpi=120, bbox_inches='tight')
plt.show()
```

## CNI Plugins : le réseau sous-jacent

Le réseau dans Kubernetes repose sur un standard : **CNI** (Container Network Interface). Chaque cluster doit avoir un plugin CNI installé pour que les Pods obtiennent des adresses IP et puissent communiquer.

```{code-cell} python
:tags: [hide-input]
fig, ax = plt.subplots(figsize=(14, 7))
ax.set_xlim(0, 14)
ax.set_ylim(0, 8)
ax.axis('off')
ax.set_title("Comparaison des principaux plugins CNI", fontsize=14, fontweight='bold')

plugins = [
    {
        "name": "Flannel",
        "color": "#3498DB",
        "complexite": 1,
        "perf": 3,
        "networkpolicy": False,
        "mecanisme": "VXLAN overlay\n(UDP encapsulation)",
        "usage": "Environnements\nsimples, dev/test",
        "x": 2
    },
    {
        "name": "Calico",
        "color": "#E67E22",
        "complexite": 3,
        "perf": 4,
        "networkpolicy": True,
        "mecanisme": "BGP (routage natif)\nou VXLAN",
        "usage": "Production, besoin\nde NetworkPolicy",
        "x": 7
    },
    {
        "name": "Cilium",
        "color": "#8E44AD",
        "complexite": 4,
        "perf": 5,
        "networkpolicy": True,
        "mecanisme": "eBPF (bypass netfilter)\nLayer 7 aware",
        "usage": "Haute perf, service mesh,\nobservabilité avancée",
        "x": 12
    },
]

for plugin in plugins:
    x = plugin["x"]
    color = plugin["color"]

    # En-tête
    ax.add_patch(FancyBboxPatch((x-2.2, 6.5), 4.4, 1.2, boxstyle="round,pad=0.1",
                                 facecolor=color, alpha=0.9, edgecolor='none'))
    ax.text(x, 7.1, plugin["name"], ha='center', va='center', fontsize=14,
            fontweight='bold', color='white')

    # Mécanisme
    ax.add_patch(FancyBboxPatch((x-2.2, 4.9), 4.4, 1.3, boxstyle="round,pad=0.1",
                                 facecolor=color, alpha=0.12, edgecolor=color, lw=1))
    ax.text(x, 6.1, "Mécanisme :", ha='center', fontsize=8.5, color=color, fontweight='bold')
    ax.text(x, 5.55, plugin["mecanisme"], ha='center', fontsize=9, color="#333")

    # NetworkPolicy
    np_color = "#27AE60" if plugin["networkpolicy"] else "#E74C3C"
    np_text = "NetworkPolicy : OUI" if plugin["networkpolicy"] else "NetworkPolicy : NON"
    ax.text(x, 4.6, np_text, ha='center', fontsize=9.5, color=np_color, fontweight='bold')

    # Performance (barres)
    ax.text(x, 4.1, "Performance :", ha='center', fontsize=8.5, color="#555")
    for j in range(5):
        rect_color = color if j < plugin["perf"] else "#ECF0F1"
        ax.add_patch(FancyBboxPatch((x - 2.0 + j * 0.85, 3.4), 0.75, 0.45,
                                     boxstyle="round,pad=0.05",
                                     facecolor=rect_color, edgecolor="#BDC3C7", lw=0.5))

    # Complexité
    ax.text(x, 3.1, "Complexité :", ha='center', fontsize=8.5, color="#555")
    for j in range(5):
        rect_color = "#E74C3C" if j < plugin["complexite"] else "#ECF0F1"
        ax.add_patch(FancyBboxPatch((x - 2.0 + j * 0.85, 2.4), 0.75, 0.45,
                                     boxstyle="round,pad=0.05",
                                     facecolor=rect_color, edgecolor="#BDC3C7", lw=0.5))

    # Usage recommandé
    ax.add_patch(FancyBboxPatch((x-2.2, 0.8), 4.4, 1.3, boxstyle="round,pad=0.1",
                                 facecolor="#F8F9FA", edgecolor="#BDC3C7", lw=1))
    ax.text(x, 1.85, "Usage recommandé :", ha='center', fontsize=8, color="#555", style='italic')
    ax.text(x, 1.25, plugin["usage"], ha='center', fontsize=9, color="#333")

ax.text(7, 0.2, "eBPF = Berkeley Packet Filter étendu : programmes sécurisés exécutés dans le noyau, sans patch",
        ha='center', fontsize=8, color="#888", style='italic')

plt.tight_layout()
plt.savefig("11_cni_plugins.png", dpi=120, bbox_inches='tight')
plt.show()
```

## Service Mesh : quand les Services ne suffisent plus

Un **service mesh** est une couche d'infrastructure qui gère la communication entre microservices de façon transparente, sans modifier le code applicatif.

```{admonition} Analogie : le service mesh comme un réseau téléphonique d'entreprise
:class: tip
Sans service mesh : chaque développeur doit coder lui-même la gestion des timeouts, les retries, le chiffrement TLS, les métriques... C'est comme si chaque employé devait construire son propre téléphone.

Avec un service mesh (Istio, Linkerd) : un proxy sidecar (Envoy) est injecté dans chaque Pod. Il intercepte tout le trafic et gère automatiquement le mTLS, le circuit breaking, le tracing distribué — comme un standard téléphonique d'entreprise.
```

Les fonctionnalités apportées par un service mesh :

| Fonctionnalité | Sans service mesh | Avec service mesh |
|---------------|-------------------|-------------------|
| Chiffrement TLS | Codé dans l'app | mTLS automatique |
| Retries / timeouts | Codé dans l'app | Politique déclarative |
| Circuit breaker | Bibliothèque (Hystrix…) | Proxy transparent |
| Tracing distribué | Instrumentation manuelle | Automatique (Zipkin, Jaeger) |
| Canary deployment | Logique complexe | Règle de routage simple |

```bash
# Installation d'Istio (exemple)
istioctl install --set profile=demo

# Activation de l'injection automatique du sidecar sur un namespace
kubectl label namespace production istio-injection=enabled

# Après cette étape, chaque Pod créé dans 'production'
# aura automatiquement un conteneur 'istio-proxy' (Envoy)
kubectl get pods -n production
# NAME                    READY   STATUS    RESTARTS
# mon-app-5d4f7b-x9j2k   2/2     Running   0
#                          ^-- 2 conteneurs : l'app + le proxy Envoy
```

## Récapitulatif

```{code-cell} python
:tags: [hide-input]
fig, ax = plt.subplots(figsize=(13, 7))
ax.set_xlim(0, 13)
ax.set_ylim(0, 8)
ax.axis('off')
ax.set_title("Vue d'ensemble : réseau Kubernetes", fontsize=14, fontweight='bold')

layers = [
    {"y": 7.0, "label": "Couche applicative", "items": ["Service mesh (Istio/Linkerd)", "mTLS, tracing, circuit breaker"], "color": "#8E44AD"},
    {"y": 5.8, "label": "Services K8s", "items": ["ClusterIP · NodePort · LoadBalancer · ExternalName", "DNS CoreDNS : <svc>.<ns>.svc.cluster.local"], "color": "#4A90D9"},
    {"y": 4.6, "label": "Routage (kube-proxy)", "items": ["iptables / IPVS / nftables", "Endpoints & EndpointSlices"], "color": "#E67E22"},
    {"y": 3.4, "label": "Politique réseau", "items": ["NetworkPolicy (ingress/egress)", "Sélecteurs de Pods et Namespaces"], "color": "#E74C3C"},
    {"y": 2.2, "label": "Réseau Pod-à-Pod (CNI)", "items": ["Flannel (simple) · Calico (BGP+NP) · Cilium (eBPF)", "Chaque Pod a une IP routable dans le cluster"], "color": "#27AE60"},
    {"y": 1.0, "label": "Réseau physique / overlay", "items": ["VXLAN · BGP · eBPF · Wireguard", "Infrastructure cloud ou bare-metal"], "color": "#7F8C8D"},
]

for layer in layers:
    ax.add_patch(FancyBboxPatch((0.3, layer["y"] - 0.45), 12.4, 0.9,
                                 boxstyle="round,pad=0.1",
                                 facecolor=layer["color"], alpha=0.15,
                                 edgecolor=layer["color"], lw=1.5))
    ax.text(1.0, layer["y"] + 0.1, layer["label"], fontsize=10,
            fontweight='bold', color=layer["color"], va='center')
    ax.text(1.0, layer["y"] - 0.2, " · ".join(layer["items"]), fontsize=8.5,
            color="#333", va='center')

plt.tight_layout()
plt.savefig("11_recapitulatif_reseau.png", dpi=120, bbox_inches='tight')
plt.show()
```

Dans ce chapitre, nous avons vu comment Kubernetes résout le problème des IPs éphémères avec l'abstraction **Service**, comment **kube-proxy** programme les règles réseau sur chaque nœud, et comment les **NetworkPolicy** permettent d'isoler les workloads. Le chapitre suivant aborde la gestion de la configuration et des secrets.
