Ingress et Gateway API#

Vos applications tournent dans des Pods, exposées par des Services. Mais comment le trafic HTTP/HTTPS provenant d’Internet arrive-t-il jusqu’à elles ? C’est le rôle de l”Ingress et de la Gateway API, les deux mécanismes de Kubernetes pour exposer des services vers l’extérieur du cluster.

Hide code cell source

import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
import numpy as np
import pandas as pd
import seaborn as sns
from urllib.parse import urlparse

sns.set_theme(style="whitegrid", palette="muted")
plt.rcParams.update({
    "figure.dpi": 120,
    "font.family": "DejaVu Sans",
    "axes.titlesize": 13,
    "axes.labelsize": 11,
})

Le problème : exposer des services HTTP#

Sans Ingress, pour exposer une application web en HTTPS, vous devriez :

  • Créer un Service de type LoadBalancer pour chaque application (un load balancer cloud par service = coûteux)

  • Gérer les certificats TLS dans chaque application ou Service

  • Gérer le routage par host/path manuellement

L”Ingress centralise tout ça : un seul point d’entrée, des règles de routage déclaratives, et la terminaison TLS.

Hide code cell source

fig, ax = plt.subplots(figsize=(14, 8))
ax.set_xlim(0, 14)
ax.set_ylim(0, 9)
ax.axis("off")
ax.set_title("Flux d'une requête externe via Ingress Controller",
             fontsize=13, fontweight="bold", pad=12)

# Internet
inet = FancyBboxPatch((5.5, 7.8), 3, 0.9, boxstyle="round,pad=0.1",
                       facecolor="#b3e5fc", edgecolor="#0288d1", linewidth=2)
ax.add_patch(inet)
ax.text(7, 8.25, "Internet / Client HTTPS", ha="center", va="center",
        fontsize=10, fontweight="bold", color="#01579b")

# Load Balancer cloud
lb = FancyBboxPatch((4.5, 6.3), 5, 0.9, boxstyle="round,pad=0.1",
                     facecolor="#fff9c4", edgecolor="#f9a825", linewidth=2)
ax.add_patch(lb)
ax.text(7, 6.75, "Load Balancer cloud — IP publique 203.0.113.1",
        ha="center", va="center", fontsize=9.5, fontweight="bold", color="#e65100")

# Cluster Kubernetes
cluster = FancyBboxPatch((0.3, 0.5), 13.4, 5.5, boxstyle="round,pad=0.2",
                          facecolor="#f3f4f6", edgecolor="#5c6bc0", linewidth=2,
                          linestyle="--")
ax.add_patch(cluster)
ax.text(7, 5.75, "Cluster Kubernetes", ha="center", va="center",
        fontsize=9, color="#5c6bc0", fontstyle="italic")

# Ingress Controller
ic = FancyBboxPatch((3.5, 4.2), 7, 1.2, boxstyle="round,pad=0.1",
                     facecolor="#e8eaf6", edgecolor="#3949ab", linewidth=2.5)
ax.add_patch(ic)
ax.text(7, 4.95, "Ingress Controller (Nginx / Traefik)",
        ha="center", va="center", fontsize=10.5, fontweight="bold", color="#1a237e")
ax.text(7, 4.55, "Lit les ressources Ingress → configure les règles HTTP",
        ha="center", va="center", fontsize=8.5, color="#283593")

# Ressource Ingress
ingress_res = FancyBboxPatch((5.5, 3.0), 3, 0.9, boxstyle="round,pad=0.08",
                              facecolor="#ede7f6", edgecolor="#7e57c2", linewidth=1.5)
ax.add_patch(ingress_res)
ax.text(7, 3.45, "Ressource Ingress\n(règles de routage YAML)", ha="center", va="center",
        fontsize=8.5, color="#4a148c")

# Services
services_data = [
    (2.5, "Service\napi-svc\n:8080",   "#e3f2fd", "#1565c0"),
    (7.0, "Service\nweb-svc\n:3000",   "#e8f5e9", "#2e7d32"),
    (11.5,"Service\nadmin-svc\n:9000", "#fff3e0", "#e65100"),
]

for sx, label, fc, ec in services_data:
    s = FancyBboxPatch((sx - 1.3, 1.5), 2.6, 1.1, boxstyle="round,pad=0.08",
                        facecolor=fc, edgecolor=ec, linewidth=1.5)
    ax.add_patch(s)
    ax.text(sx, 2.05, label, ha="center", va="center", fontsize=8.5,
            fontweight="bold", color=ec)

# Pods derrière services
pod_groups = [(2.5, ["api-pod-1", "api-pod-2"]), (7.0, ["web-pod-1"]),
              (11.5, ["admin-pod-1"])]
for sx, pods in pod_groups:
    for j, p in enumerate(pods):
        px = sx + (j - (len(pods)-1)/2) * 1.1
        pod = FancyBboxPatch((px - 0.5, 0.5), 1.0, 0.7, boxstyle="round,pad=0.05",
                              facecolor="#e0f7fa", edgecolor="#00838f", linewidth=1)
        ax.add_patch(pod)
        ax.text(px, 0.85, p, ha="center", va="center", fontsize=7, color="#00695c")
    # Flèche service → pods
    ax.annotate("", xy=(sx, 1.5), xytext=(sx, 1.2),
                arrowprops=dict(arrowstyle="->", color="#546e7a", lw=1.5))

# Flèches de routage
for sx, rule in [(2.5, "api.exemple.com"),
                 (7.0, "exemple.com"),
                 (11.5, "admin.exemple.com")]:
    ax.annotate("", xy=(sx, 2.6), xytext=(sx, 4.2),
                arrowprops=dict(arrowstyle="->", color="#3949ab", lw=1.5))
    ax.text(sx, 3.4, rule, ha="center", va="center", fontsize=7,
            color="#1a237e", rotation=90 if sx != 7.0 else 0)

# Flèches principales
ax.annotate("", xy=(7, 7.8), xytext=(7, 7.2),
            arrowprops=dict(arrowstyle="->", color="#01579b", lw=2.5))
ax.text(7.3, 7.5, "HTTPS :443", fontsize=8.5, color="#01579b")

ax.annotate("", xy=(7, 6.3), xytext=(7, 7.2),
            arrowprops=dict(arrowstyle="->", color="#f9a825", lw=2.5))

ax.annotate("", xy=(7, 5.4), xytext=(7, 6.3),
            arrowprops=dict(arrowstyle="->", color="#3949ab", lw=2.5))
ax.text(7.2, 5.85, "TLS terminé ici\n→ HTTP interne", fontsize=7.5, color="#3949ab")

# Ressource Ingress → Ingress Controller
ax.annotate("", xy=(6.5, 4.5), xytext=(6.5, 3.9),
            arrowprops=dict(arrowstyle="->", color="#7e57c2", lw=1.5, linestyle="dashed"))
ax.text(4.8, 4.1, "watch/configure", fontsize=7.5, color="#7e57c2", fontstyle="italic")

plt.tight_layout()
plt.savefig("_static/14_ingress_flow.png", dpi=130, bbox_inches="tight")
plt.show()
_images/197a20f68a8ee71032e2451f22a7867b11346bfcb0c4080e55674a89a6e75d45.png

La ressource Ingress#

Un objet Ingress déclare les règles de routage HTTP : quel host et quel path doivent être redirigés vers quel Service.

# ingress-exemple.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: mon-ingress
  namespace: production
  annotations:
    # Annotations spécifiques au contrôleur Nginx
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
    nginx.ingress.kubernetes.io/rate-limit: "100"    # Requêtes/seconde
spec:
  # Classe d'Ingress Controller à utiliser
  ingressClassName: nginx

  # Configuration TLS
  tls:
    - hosts:
        - api.exemple.com
        - exemple.com
      secretName: tls-certificate   # Secret contenant le certificat

  # Règles de routage
  rules:
    # Routage par host
    - host: api.exemple.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: api-svc
                port:
                  number: 8080

    - host: exemple.com
      http:
        paths:
          # Routage par path sur le même host
          - path: /app
            pathType: Prefix
            backend:
              service:
                name: web-svc
                port:
                  number: 3000
          - path: /api/v2
            pathType: Prefix
            backend:
              service:
                name: api-v2-svc
                port:
                  number: 8081
          # Catch-all : si aucun path ne correspond
          - path: /
            pathType: Prefix
            backend:
              service:
                name: default-svc
                port:
                  number: 80

Types de pathType#

  • Exact : correspondance exacte (/api ne correspond pas à /api/v2)

  • Prefix : correspondance par préfixe (/api correspond à /api/v2, /api/users…)

  • ImplementationSpecific : comportement défini par le contrôleur

L’Ingress Controller : le composant qui fait le travail#

La ressource Ingress est juste un objet de configuration. Elle ne fait rien toute seule. Il faut déployer un Ingress Controller dans le cluster — c’est lui qui lit les ressources Ingress et configure réellement le proxy HTTP.

# Installer le contrôleur Nginx Ingress via Helm
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  --set controller.replicaCount=2

# Vérifier l'installation
kubectl get pods -n ingress-nginx
kubectl get svc -n ingress-nginx
# service/ingress-nginx-controller   LoadBalancer  10.0.0.100  203.0.113.1  80:31080/TCP,443:31443/TCP

Ingress Controllers populaires :

Contrôleur

Points forts

Nginx Ingress

Stable, très répandu, documentation riche

Traefik

Découverte automatique, dashboard intégré, Let’s Encrypt natif

HAProxy

Hautes performances, configurations avancées

Contour

Basé sur Envoy, gateway API ready

Kong

API Gateway features (auth, rate limiting, plugins)

Annotations Nginx Ingress courantes#

annotations:
  # Réécriture de l'URL (ex: /api/v1/users → /users)
  nginx.ingress.kubernetes.io/rewrite-target: /$2

  # Authentification basique
  nginx.ingress.kubernetes.io/auth-type: basic
  nginx.ingress.kubernetes.io/auth-secret: basic-auth-secret

  # Rate limiting
  nginx.ingress.kubernetes.io/limit-rps: "10"

  # CORS
  nginx.ingress.kubernetes.io/enable-cors: "true"
  nginx.ingress.kubernetes.io/cors-allow-origin: "https://mon-frontend.com"

  # Timeout
  nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
  nginx.ingress.kubernetes.io/proxy-send-timeout: "60"

  # Redirect HTTP → HTTPS
  nginx.ingress.kubernetes.io/ssl-redirect: "true"

  # Taille max du body (upload)
  nginx.ingress.kubernetes.io/proxy-body-size: "50m"

TLS avec Ingress et cert-manager#

Secret TLS manuel#

# Créer un Secret TLS à partir de fichiers certificat
kubectl create secret tls tls-certificate \
  --cert=certificat.crt \
  --key=cle-privee.key \
  --namespace production

cert-manager : automatisation des certificats#

cert-manager est un contrôleur Kubernetes qui automatise la gestion des certificats TLS, notamment avec Let’s Encrypt (certificats gratuits et renouvelés automatiquement).

# Installer cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml

# Vérifier
kubectl get pods -n cert-manager
# cluster-issuer-letsencrypt.yaml
# Émetteur de certificats Let's Encrypt (production)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: admin@exemple.com    # Email pour les notifications d'expiration
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
      - http01:
          ingress:
            class: nginx        # Utilise le challenge HTTP-01 via Ingress
# Ingress avec cert-manager (renouvellement automatique)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: mon-ingress-tls
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"  # Clé magique !
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - exemple.com
      secretName: exemple-com-tls   # cert-manager crée ce Secret automatiquement
  rules:
    - host: exemple.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web-svc
                port:
                  number: 80

Cycle de vie d’un certificat avec cert-manager

  1. cert-manager détecte l’annotation cert-manager.io/cluster-issuer

  2. Il crée un objet CertificateRequest et contacte Let’s Encrypt

  3. Let’s Encrypt vérifie que vous contrôlez le domaine (challenge HTTP-01 ou DNS-01)

  4. cert-manager reçoit le certificat et le stocke dans le Secret indiqué

  5. cert-manager surveille l’expiration (30 jours avant) et renouvelle automatiquement

Gateway API : la nouvelle génération#

La Gateway API est la successeure de l’Ingress. Elle résout plusieurs limitations de l’Ingress :

  • L’Ingress est trop couplé à Nginx (annotations spécifiques au contrôleur)

  • Pas de support natif pour le routage TCP/UDP

  • Séparation des responsabilités floue (infrastructure vs application)

La Gateway API introduit trois ressources distinctes avec des responsabilités claires :

Ressource

Qui la gère

Rôle

GatewayClass

Admin infrastructure

Définit quel contrôleur utiliser (Nginx, Envoy…)

Gateway

Admin cluster

Configure le point d’entrée (ports, TLS, protocoles)

HTTPRoute

Développeur

Définit les règles de routage de l’application

# gatewayclass.yaml (géré par l'admin infra)
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: nginx-gateway
spec:
  controllerName: k8s.nginx.org/nginx-gateway-controller
# gateway.yaml (géré par l'admin cluster)
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: passerelle-prod
  namespace: infra
spec:
  gatewayClassName: nginx-gateway
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      tls:
        mode: Terminate
        certificateRefs:
          - name: tls-wildcard-cert
      allowedRoutes:
        namespaces:
          from: Selector
          selector:
            matchLabels:
              gateway: autorisee   # Seuls les namespaces avec ce label peuvent y accéder
# httproute.yaml (géré par le développeur)
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: route-api
  namespace: production
spec:
  parentRefs:
    - name: passerelle-prod
      namespace: infra
  hostnames:
    - "api.exemple.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /v2
      backendRefs:
        - name: api-v2-svc
          port: 8080
          weight: 90       # 90% du trafic (canary deployment !)
        - name: api-v3-canary-svc
          port: 8080
          weight: 10       # 10% vers la nouvelle version

    - matches:
        - headers:
            - name: "x-beta-user"
              value: "true"  # Routage par header (feature flag)
      backendRefs:
        - name: api-beta-svc
          port: 8080

Hide code cell source

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

# --- Ingress : architecture couplée ---
ax = axes[0]
ax.set_xlim(0, 10)
ax.set_ylim(0, 8)
ax.axis("off")
ax.set_title("Ingress\n(architecture couplée)", fontsize=11, fontweight="bold", color="#e65100")

layers_ingress = [
    (5, 7.1, "Développeur", "#fffde7", "#f9a825", "kubectl apply ingress.yaml"),
    (5, 5.5, "Ressource Ingress\n(+ annotations Nginx)", "#fce4ec", "#e91e63", "Kind: Ingress"),
    (5, 3.8, "Nginx Ingress Controller\n(lit les ressources Ingress)", "#e8eaf6", "#3949ab",
     "Annotations Nginx-spécifiques"),
    (5, 2.2, "Service Kubernetes", "#e3f2fd", "#1565c0", "ClusterIP"),
    (5, 0.8, "Pods applicatifs", "#e8f5e9", "#2e7d32", "app:1.0"),
]

for x, y, label, fc, ec, note in layers_ingress:
    b = FancyBboxPatch((x - 3.5, y - 0.45), 7, 0.9, boxstyle="round,pad=0.08",
                        facecolor=fc, edgecolor=ec, linewidth=1.5)
    ax.add_patch(b)
    ax.text(x, y + 0.1, label, ha="center", va="center", fontsize=8.5,
            fontweight="bold", color=ec)
    ax.text(x, y - 0.2, note, ha="center", va="center", fontsize=7.5, color="#666666")
    if y > 0.9:
        ax.annotate("", xy=(x, y - 0.45), xytext=(x, y - 0.45 - 0.65),
                    arrowprops=dict(arrowstyle="->", color="#9e9e9e", lw=1.5))

ax.text(5, 0.1, "⚠ Annotations couplées à Nginx\n→ migration difficile",
        ha="center", va="center", fontsize=8, color="#c62828",
        bbox=dict(boxstyle="round,pad=0.2", facecolor="#ffebee", edgecolor="#f44336"))

# --- Gateway API : séparation des responsabilités ---
ax2 = axes[1]
ax2.set_xlim(0, 10)
ax2.set_ylim(0, 8)
ax2.axis("off")
ax2.set_title("Gateway API\n(séparation des responsabilités)", fontsize=11,
              fontweight="bold", color="#2e7d32")

layers_gw = [
    (5, 7.2, "Admin infra", "#e8eaf6", "#3949ab",
     "GatewayClass\n(quel contrôleur)"),
    (5, 5.7, "Admin cluster", "#fff3e0", "#e65100",
     "Gateway\n(ports, TLS, namespaces autorisés)"),
    (5, 4.1, "Développeur", "#e8f5e9", "#2e7d32",
     "HTTPRoute\n(règles de routage applicatives)"),
    (5, 2.5, "Service Kubernetes", "#e3f2fd", "#1565c0", "ClusterIP"),
    (5, 1.0, "Pods applicatifs", "#e8f5e9", "#2e7d32", "app:1.0"),
]

for x, y, label, fc, ec, note in layers_gw:
    b = FancyBboxPatch((x - 3.5, y - 0.55), 7, 1.1, boxstyle="round,pad=0.08",
                        facecolor=fc, edgecolor=ec, linewidth=1.5)
    ax2.add_patch(b)
    ax2.text(x - 1.8, y + 0.05, label, ha="center", va="center", fontsize=8.5,
             fontweight="bold", color=ec)
    ax2.text(x + 1.5, y, note, ha="center", va="center", fontsize=7.5, color="#37474f",
             linespacing=1.4)
    if y > 1.1:
        ax2.annotate("", xy=(x, y - 0.55), xytext=(x, y - 0.55 - 0.55),
                     arrowprops=dict(arrowstyle="->", color="#9e9e9e", lw=1.5))

ax2.text(5, 0.15, "✓ Rôles clairement séparés\n→ portable entre contrôleurs",
         ha="center", va="center", fontsize=8, color="#2e7d32",
         bbox=dict(boxstyle="round,pad=0.2", facecolor="#e8f5e9", edgecolor="#2e7d32"))

plt.tight_layout()
plt.savefig("_static/14_ingress_vs_gateway.png", dpi=130, bbox_inches="tight")
plt.show()
_images/adcfbbffc13c1eff2dad81b77784ca6229e879367306f146bf8cae37386beddc.png

Simulation Python : routeur Ingress#

from dataclasses import dataclass, field
from typing import List, Optional, Tuple
import re

@dataclass
class RegleIngress:
    """Représente une règle de routage Ingress."""
    host: str
    path: str
    path_type: str          # Exact, Prefix
    service_nom: str
    service_port: int
    priorite: int = 0       # Plus bas = plus prioritaire

    def correspond(self, host: str, path: str) -> bool:
        """Vérifie si une requête correspond à cette règle."""
        # Vérification du host (wildcards supportés : *.exemple.com)
        if self.host.startswith("*."):
            domaine = self.host[2:]
            if not (host.endswith("." + domaine) or host == domaine):
                return False
        elif self.host != "*" and self.host != host:
            return False

        # Vérification du path
        if self.path_type == "Exact":
            return path == self.path
        elif self.path_type == "Prefix":
            return path == self.path or path.startswith(self.path.rstrip("/") + "/")
        return False


@dataclass
class RouterIngress:
    """Simulation d'un routeur Ingress (comme nginx-ingress-controller)."""
    nom: str
    regles: List[RegleIngress] = field(default_factory=list)

    def ajouter_regle(self, regle: RegleIngress):
        self.regles.append(regle)
        # Trier par priorité puis par longueur de path (plus long = plus spécifique)
        self.regles.sort(key=lambda r: (r.priorite, -len(r.path)))

    def router(self, host: str, path: str) -> Optional[Tuple[str, int, str]]:
        """
        Route une requête vers le service approprié.
        Retourne (service_nom, service_port, regle_matchée) ou None.
        """
        for regle in self.regles:
            if regle.correspond(host, path):
                return regle.service_nom, regle.service_port, f"{regle.host}{regle.path}"
        return None

    def tester(self, requetes: List[Tuple[str, str]]):
        """Teste une liste de requêtes et affiche le résultat."""
        print(f"\nRouteur Ingress : {self.nom}")
        print(f"{'Requête':<45} {'Résultat'}")
        print("-" * 75)
        for host, path in requetes:
            resultat = self.router(host, path)
            url = f"{host}{path}"
            if resultat:
                svc, port, regle = resultat
                print(f"  {url:<43}{svc}:{port}  (via règle '{regle}')")
            else:
                print(f"  {url:<43} → ✗ 404 Not Found (aucune règle ne correspond)")


# Construire un routeur Ingress de démonstration
routeur = RouterIngress("ingress-production")

# Ajouter les règles (de la plus spécifique à la moins spécifique)
routeur.ajouter_regle(RegleIngress("api.exemple.com", "/v2/users", "Exact",    "api-v2-svc", 8081))
routeur.ajouter_regle(RegleIngress("api.exemple.com", "/v2",       "Prefix",   "api-v2-svc", 8081))
routeur.ajouter_regle(RegleIngress("api.exemple.com", "/",         "Prefix",   "api-v1-svc", 8080))
routeur.ajouter_regle(RegleIngress("exemple.com",     "/app",      "Prefix",   "web-svc",    3000))
routeur.ajouter_regle(RegleIngress("exemple.com",     "/admin",    "Prefix",   "admin-svc",  9000))
routeur.ajouter_regle(RegleIngress("exemple.com",     "/",         "Prefix",   "homepage-svc", 80))
routeur.ajouter_regle(RegleIngress("*.exemple.com",   "/health",   "Exact",    "healthcheck-svc", 8888))

# Tests
requetes_test = [
    ("api.exemple.com",   "/v2/users"),          # Exact match
    ("api.exemple.com",   "/v2/orders/123"),     # Prefix /v2
    ("api.exemple.com",   "/v1/legacy"),         # Prefix /
    ("exemple.com",       "/app/dashboard"),     # Prefix /app
    ("exemple.com",       "/admin/users"),       # Prefix /admin
    ("exemple.com",       "/"),                  # Catch-all
    ("blog.exemple.com",  "/health"),            # Wildcard host
    ("autre-domaine.io",  "/api"),               # Pas de règle
    ("exemple.com",       "/inexistant/page"),   # Path inexistant → catch-all /
]

routeur.tester(requetes_test)
Routeur Ingress : ingress-production
Requête                                       Résultat
---------------------------------------------------------------------------
  api.exemple.com/v2/users                    → api-v2-svc:8081  (via règle 'api.exemple.com/v2/users')
  api.exemple.com/v2/orders/123               → api-v2-svc:8081  (via règle 'api.exemple.com/v2')
  api.exemple.com/v1/legacy                   → api-v1-svc:8080  (via règle 'api.exemple.com/')
  exemple.com/app/dashboard                   → web-svc:3000  (via règle 'exemple.com/app')
  exemple.com/admin/users                     → admin-svc:9000  (via règle 'exemple.com/admin')
  exemple.com/                                → homepage-svc:80  (via règle 'exemple.com/')
  blog.exemple.com/health                     → healthcheck-svc:8888  (via règle '*.exemple.com/health')
  autre-domaine.io/api                        → ✗ 404 Not Found (aucune règle ne correspond)
  exemple.com/inexistant/page                 → homepage-svc:80  (via règle 'exemple.com/')

Hide code cell source

# Visualisation des règles de routage
fig, ax = plt.subplots(figsize=(14, 6))
ax.set_xlim(0, 14)
ax.set_ylim(0, 7)
ax.axis("off")
ax.set_title("Règles de routage Ingress — arbres de décision par host",
             fontsize=12, fontweight="bold", pad=10)

# Host 1 : api.exemple.com
h1_x = 3.5
h1_box = FancyBboxPatch((h1_x - 3.0, 5.5), 6, 0.9, boxstyle="round,pad=0.1",
                          facecolor="#e8eaf6", edgecolor="#3949ab", linewidth=2)
ax.add_patch(h1_box)
ax.text(h1_x, 5.95, "api.exemple.com", ha="center", va="center",
        fontsize=10.5, fontweight="bold", color="#1a237e")

paths_h1 = [
    (h1_x - 2.0, 3.8, "/v2/users (Exact)", "api-v2-svc:8081", "#c8e6c9"),
    (h1_x,       3.8, "/v2/* (Prefix)",     "api-v2-svc:8081", "#bbdefb"),
    (h1_x + 2.0, 3.8, "/* (Prefix)",        "api-v1-svc:8080", "#fff9c4"),
]
for px, py, path_label, svc, fc in paths_h1:
    b = FancyBboxPatch((px - 1.1, py - 0.42), 2.2, 0.84, boxstyle="round,pad=0.06",
                        facecolor=fc, edgecolor="#546e7a", linewidth=1.2)
    ax.add_patch(b)
    ax.text(px, py + 0.1, path_label, ha="center", va="center", fontsize=8, fontweight="bold")
    ax.text(px, py - 0.2, svc, ha="center", va="center", fontsize=7.5, color="#1a237e")
    ax.annotate("", xy=(px, py + 0.42), xytext=(h1_x, 5.5),
                arrowprops=dict(arrowstyle="->", color="#3949ab", lw=1.2,
                                connectionstyle="arc3,rad=0.0"))

# Host 2 : exemple.com
h2_x = 10.5
h2_box = FancyBboxPatch((h2_x - 3.0, 5.5), 6, 0.9, boxstyle="round,pad=0.1",
                          facecolor="#e8f5e9", edgecolor="#2e7d32", linewidth=2)
ax.add_patch(h2_box)
ax.text(h2_x, 5.95, "exemple.com", ha="center", va="center",
        fontsize=10.5, fontweight="bold", color="#1b5e20")

paths_h2 = [
    (h2_x - 2.0, 3.8, "/app/* (Prefix)",   "web-svc:3000",  "#e3f2fd"),
    (h2_x,       3.8, "/admin (Prefix)",   "admin-svc:9000","#ffcdd2"),
    (h2_x + 2.0, 3.8, "/* (Prefix)",       "homepage:80",   "#fff9c4"),
]
for px, py, path_label, svc, fc in paths_h2:
    b = FancyBboxPatch((px - 1.1, py - 0.42), 2.2, 0.84, boxstyle="round,pad=0.06",
                        facecolor=fc, edgecolor="#546e7a", linewidth=1.2)
    ax.add_patch(b)
    ax.text(px, py + 0.1, path_label, ha="center", va="center", fontsize=8, fontweight="bold")
    ax.text(px, py - 0.2, svc, ha="center", va="center", fontsize=7.5, color="#1b5e20")
    ax.annotate("", xy=(px, py + 0.42), xytext=(h2_x, 5.5),
                arrowprops=dict(arrowstyle="->", color="#2e7d32", lw=1.2))

# Ingress Controller au sommet
ic_box = FancyBboxPatch((4, 6.6), 6, 0.8, boxstyle="round,pad=0.08",
                          facecolor="#fff3e0", edgecolor="#e65100", linewidth=2)
ax.add_patch(ic_box)
ax.text(7, 7.0, "Ingress Controller — distribue selon Host:", ha="center", va="center",
        fontsize=10, fontweight="bold", color="#bf360c")

ax.annotate("", xy=(h1_x, 6.4), xytext=(5.5, 6.6),
            arrowprops=dict(arrowstyle="->", color="#e65100", lw=2))
ax.annotate("", xy=(h2_x, 6.4), xytext=(8.5, 6.6),
            arrowprops=dict(arrowstyle="->", color="#e65100", lw=2))

# Requête entrante
ax.text(7, 0.4,
        "Résolution : 1) Trouver le host → 2) Parcourir les paths du plus spécifique au moins spécifique",
        ha="center", va="center", fontsize=9,
        bbox=dict(boxstyle="round,pad=0.3", facecolor="#fffde7", edgecolor="#f9a825"))

plt.tight_layout()
plt.savefig("_static/14_routing_rules.png", dpi=130, bbox_inches="tight")
plt.show()
_images/8612658250110a8bb571bc5d89c43f331633d0c0e6e5d542fe1ae8c9c14e0ae9.png

Ingress vs Gateway API : tableau comparatif#

Hide code cell source

fig, ax = plt.subplots(figsize=(14, 5))
ax.axis("off")
ax.set_title("Ingress vs Gateway API — comparaison", fontsize=12, fontweight="bold", pad=10)

headers = ["Critère", "Ingress", "Gateway API"]
rows = [
    ["Stabilité",          "Stable (GA)",              "Stable depuis K8s 1.24"],
    ["Séparation des rôles", "Non (tout dans Ingress)", "Oui (GatewayClass / Gateway / Route)"],
    ["Routage par header", "Dépend du contrôleur",     "Natif (HTTPRoute)"],
    ["Routage TCP/UDP",    "Non natif",                "Oui (TCPRoute, UDPRoute)"],
    ["Canary / weight",    "Annotations spécifiques",  "Natif (weights dans backendRefs)"],
    ["Portabilité",        "Annotations non portables","Portable entre contrôleurs"],
    ["Adoption",           "Très répandue (mature)",   "En progression rapide"],
    ["Recommandation",     "Projet existant",          "Nouveaux projets"],
]

col_widths = [0.25, 0.37, 0.38]
x_starts = [0.0, 0.25, 0.62]

y_header = 0.91
for i, (h, x, w) in enumerate(zip(headers, x_starts, col_widths)):
    colors_h = ["#37474f", "#e65100", "#2e7d32"]
    rect = FancyBboxPatch((x + 0.003, y_header), w - 0.006, 0.07,
                           transform=ax.transAxes,
                           boxstyle="round,pad=0.005", facecolor=colors_h[i],
                           edgecolor="white", clip_on=False)
    ax.add_patch(rect)
    ax.text(x + w/2, y_header + 0.035, h, transform=ax.transAxes,
            ha="center", va="center", fontsize=9.5, fontweight="bold", color="white")

row_h = 0.10
for r_idx, row in enumerate(rows):
    y = y_header - (r_idx + 1) * row_h
    for c_idx, (cell, x, w) in enumerate(zip(row, x_starts, col_widths)):
        if c_idx == 0:
            fc = "#f5f5f5"
        elif c_idx == 1:
            fc = "#fff3e0"
        else:
            fc = "#e8f5e9" if r_idx < 7 else "#e8f5e9"
        rect = FancyBboxPatch((x + 0.003, y), w - 0.006, row_h - 0.005,
                               transform=ax.transAxes,
                               boxstyle="round,pad=0.003", facecolor=fc,
                               edgecolor="#e0e0e0", clip_on=False)
        ax.add_patch(rect)
        color = "#1a237e" if c_idx == 0 else ("#bf360c" if c_idx == 1 else "#1b5e20")
        ax.text(x + w/2, y + row_h/2, cell, transform=ax.transAxes,
                ha="center", va="center", fontsize=8.5,
                color=color, fontweight="bold" if c_idx == 0 else "normal")

plt.tight_layout()
plt.savefig("_static/14_ingress_vs_gw_table.png", dpi=130, bbox_inches="tight")
plt.show()
_images/98d0911b223a6e531bbd8bbec402e16711a555b20b1e84637cf55a02db4212b5.png

Points clés à retenir#

  • L”Ingress centralise l’exposition HTTP/HTTPS : un seul point d’entrée pour plusieurs applications

  • L”Ingress Controller (Nginx, Traefik…) est le composant qui lit les ressources Ingress et fait réellement le routage — il faut l’installer séparément

  • Les règles de routage combinent host et path : la règle la plus spécifique (path le plus long) est appliquée en premier

  • cert-manager automatise la gestion des certificats TLS avec Let’s Encrypt (renouvellement automatique)

  • La Gateway API est la nouvelle génération : elle sépare les responsabilités (infra / cluster / dev) et supporte nativement le routage TCP/UDP, les poids de trafic (canary), et le routage par header

  • Pour les nouveaux projets, préférer la Gateway API ; l’Ingress reste pertinent pour les projets existants