Aller au contenu

02 - Gateway API avec Envoy Gateway

Dans l'épisode 01, on a monté un cluster kind piloté par Flux, avec podinfo exposé en NodePort. On passe à l'étape suivante : remplacer ce NodePort par une vraie porte d'entrée, la Gateway API, avec Envoy Gateway.

C'est aussi la phase de Proof of Concept que je te recommandais dans l'état des lieux de la fin d'Ingress NGINX avant toute migration.

À la fin, http://podinfo.localhost:8080 sera servi par Envoy et routé par une HTTPRoute.

Ce qu'on va monter

flowchart LR
    U["curl / navigateur<br>podinfo.localhost:8080"] --> PM["Port mapping kind<br>8080 → 30080"]
    PM --> NP["NodePort 30080<br>(control-plane)"]
    NP -->|kube-proxy| E["Pod Envoy<br>(worker)"]
    E -->|"HTTPRoute<br>podinfo.localhost"| S["Service podinfo<br>(ClusterIP)"]
    S --> P["Pods podinfo"]
    EG["Contrôleur<br>Envoy Gateway"] -.->|config xDS| E
Brique Version testée
kind 0.33 (Kubernetes 1.37)
Flux 2.10
Envoy Gateway 1.9.3
podinfo 6.17

Envoy Gateway en deux mots

  • Le contrôleur (envoy-gateway) surveille les ressources Gateway API (GatewayClass, Gateway, HTTPRoute…) et les traduit en configuration Envoy, poussée via xDS.
  • Le data plane : pour chaque Gateway, le contrôleur crée lui-même un Deployment Envoy et son Service. Tu ne déploies jamais Envoy à la main.
  • EnvoyProxy : une CRD propre à Envoy Gateway pour personnaliser ce data plane (type de Service, replicas, ressources…), rattachée à la GatewayClass.

On retrouve la séparation des rôles qui fait la force de la Gateway API :

Ressource Qui la gère Ce qu'elle décrit
GatewayClass Équipe infra Quel contrôleur, avec quels paramètres
Gateway Équipe plateforme Un point d'entrée : ports, protocoles, namespaces autorisés
HTTPRoute Équipe applicative Quel hostname ou path va vers quel Service

Point de départ

On repart du dépôt de l'épisode 01 : un cluster kind dont le port mapping envoie localhost:8080 sur le NodePort 30080 du control-plane, deux couches Flux (infra-controllers puis apps) et podinfo qui occupe ce NodePort. Voici ce qu'on modifie :

.
├── clusters/kind/
│   ├── infrastructure.yaml   # + Kustomization infra-configs
│   └── apps.yaml             # dependsOn: infra-configs
├── infrastructure/
│   ├── controllers/          # + envoy-gateway.yaml
│   └── configs/              # nouveau : EnvoyProxy, GatewayClass, Gateway
└── apps/kind/                # + HTTPRoute, podinfo repasse en ClusterIP

1. Une couche de plus : infra-configs

On ajoute une Kustomization entre l'infrastructure et les apps, à la suite de infra-controllers :

clusters/kind/infrastructure.yaml
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: infra-configs
  namespace: flux-system
spec:
  interval: 1h
  retryInterval: 1m
  timeout: 5m
  dependsOn:
    - name: infra-controllers
  sourceRef:
    kind: GitRepository
    name: flux-system
  path: ./infrastructure/configs
  prune: true

Et la Kustomization apps dépend désormais de cette nouvelle couche :

clusters/kind/apps.yaml
spec:
  dependsOn:
    - name: infra-configs

Pourquoi une couche de plus ?

Les CRDs de la Gateway API sont installées par la HelmRelease d'Envoy Gateway, de façon asynchrone par le helm-controller. Si la Gateway était dans la même Kustomization, Flux tenterait de l'appliquer avant que sa CRD existe, et le dry-run échouerait. Avec wait: true sur infra-controllers puis dependsOn, chaque couche attend que la précédente soit Ready.

2. Installer Envoy Gateway

infrastructure/controllers/envoy-gateway.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: envoy-gateway-system
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
  name: envoy-gateway
  namespace: envoy-gateway-system
spec:
  interval: 1h
  url: oci://docker.io/envoyproxy/gateway-helm
  layerSelector:
    mediaType: "application/vnd.cncf.helm.chart.content.v1.tar+gzip"
    operation: copy
  ref:
    semver: "1.x"
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: envoy-gateway
  namespace: envoy-gateway-system
spec:
  interval: 30m
  chartRef:
    kind: OCIRepository
    name: envoy-gateway
  install:
    crds: CreateReplace
  upgrade:
    crds: CreateReplace

Pense à ajouter envoy-gateway.yaml dans le kustomization.yaml de infrastructure/controllers/.

helm install envoy-gateway oci://docker.io/envoyproxy/gateway-helm \
  --version 1.9.3 \
  --namespace envoy-gateway-system --create-namespace

kubectl -n envoy-gateway-system wait deployment/envoy-gateway \
  --for=condition=Available --timeout=5m

Les CRDs et les upgrades

Le chart livre les CRDs de la Gateway API et d'Envoy Gateway dans le dossier crds/ d'un sous-chart. Helm les installe au premier install, mais ne les met jamais à jour lors d'un upgrade. Avec Flux, crds: CreateReplace règle le problème. En Helm CLI, Envoy publie un chart dédié aux CRDs, gateway-crds-helm, pour les gérer à part.

3. Le data plane : EnvoyProxy, GatewayClass et Gateway

Ces trois ressources vont dans infrastructure/configs/, avec un kustomization.yaml qui les liste.

EnvoyProxy

infrastructure/configs/envoy-proxy.yaml
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
  name: kind-nodeport
  namespace: envoy-gateway-system
spec:
  provider:
    type: Kubernetes
    kubernetes:
      envoyService:
        type: NodePort
        externalTrafficPolicy: Cluster
        patch:
          type: StrategicMerge
          value:
            spec:
              ports:
                - port: 80
                  nodePort: 30080

Pas de LoadBalancer sur kind

Par défaut, Envoy Gateway expose chaque Gateway via un Service LoadBalancer. Sur kind, sans contrôleur de load balancer, il reste en <pending>. On passe donc en NodePort et on fixe le port à 30080, celui du port mapping, grâce au patch en strategic merge : l'entrée de ports est fusionnée sur sa clé port: 80.

Le piège de l'externalTrafficPolicy

Envoy Gateway règle externalTrafficPolicy: Local par défaut. Avec Local, kube-proxy n'envoie le trafic d'un NodePort qu'aux pods du nœud qui l'a reçu. Or ici, tout le trafic entre par le control-plane (le port mapping) et les pods Envoy tournent sur les workers : les paquets sont jetés et curl reste pendu.

Avec Cluster, kube-proxy relaie le trafic vers n'importe quel nœud. La contrepartie : Envoy voit l'IP d'un nœud au lieu de celle du client. Sans importance en lab, mais en production derrière un load balancer cloud, garde Local pour préserver l'IP source.

GatewayClass

infrastructure/configs/gatewayclass.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: envoy
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller
  parametersRef:
    group: gateway.envoyproxy.io
    kind: EnvoyProxy
    name: kind-nodeport
    namespace: envoy-gateway-system

Le controllerName désigne le contrôleur qui prend en charge cette classe, et parametersRef lui rattache notre EnvoyProxy : toutes les Gateway de la classe envoy hériteront du Service NodePort.

Gateway

infrastructure/configs/gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: homelab
  namespace: envoy-gateway-system
spec:
  gatewayClassName: envoy
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces:
          from: All

Dès que cette Gateway existe, Envoy Gateway crée le Deployment et le Service Envoy correspondants dans envoy-gateway-system.

allowedRoutes.namespaces.from: All laisse n'importe quel namespace s'attacher à la Gateway : pratique en lab. En production, restreins avec from: Selector et un label sur les namespaces autorisés.

4. Basculer podinfo derrière la Gateway

La Gateway reprend le NodePort 30080 : podinfo repasse en ClusterIP (la valeur par défaut du chart), on retire donc la section service de son overlay.

apps/kind/podinfo-values.yaml
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: podinfo
  namespace: podinfo
spec:
  values:
    replicaCount: 2

Puis on lui ajoute une HTTPRoute, déclarée dans l'overlay :

apps/kind/podinfo-httproute.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: podinfo
  namespace: podinfo
spec:
  parentRefs:
    - name: homelab
      namespace: envoy-gateway-system
  hostnames:
    - podinfo.localhost
  rules:
    - backendRefs:
        - name: podinfo
          port: 9898
apps/kind/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../base/podinfo
  - podinfo-httproute.yaml
patches:
  - path: podinfo-values.yaml
  • parentRefs : la route s'attache à la Gateway homelab, dans un autre namespace. C'est possible parce que la Gateway l'autorise via allowedRoutes.
  • hostnames : les noms en *.localhost sont résolus en 127.0.0.1 par curl et par les navigateurs Chromium et Firefox, sans toucher à /etc/hosts. Chaque nouvelle app aura sa propre HTTPRoute et son hostname, derrière la même Gateway et le même port 8080.
  • backendRefs : le Service est dans le même namespace que la route. Pour viser un Service d'un autre namespace, il faudrait en plus un ReferenceGrant dans le namespace du Service.
  • La route vit dans l'overlay apps/kind/ et pas dans apps/base/ : son hostname et sa Gateway dépendent du cluster, alors que la base reste réutilisable.

Le conflit de port pendant la bascule

Si tu pousses tout dans un seul commit, Flux applique infra-configs avant apps : Envoy Gateway essaie de créer son Service sur le NodePort 30080 alors que podinfo l'occupe encore. Les logs du contrôleur affichent alors :

spec.ports[0].nodePort: Invalid value: 30080: provided port is already allocated

Dès que la couche apps repasse podinfo en ClusterIP, le port se libère et Envoy Gateway réussit à sa tentative suivante : une trentaine de secondes dans mon lab. Rien à faire, mais ne t'affole pas en lisant les logs. Pour une bascule sans erreur, libère le port dans un premier commit.

5. Vérifier

flux get kustomizations
flux get helmreleases -A
kubectl get gatewayclass,gateway -A
kubectl get httproute -A

Les conditions à surveiller dans le status de chaque ressource :

Ressource Condition Si elle est à False
GatewayClass Accepted Le controllerName ne correspond à aucun contrôleur installé, ou le parametersRef est invalide
Gateway Programmed Le data plane Envoy n'est pas prêt (pod en erreur, Service non créé)
HTTPRoute Accepted La Gateway est introuvable, ou le namespace de la route n'est pas autorisé par allowedRoutes
HTTPRoute ResolvedRefs Le Service backend est introuvable, ou un ReferenceGrant manque

Puis le test de bout en bout :

curl http://podinfo.localhost:8080   # 200, réponse JSON de podinfo
curl -i http://localhost:8080        # 404 : aucune HTTPRoute ne correspond à ce hostname
{
  "hostname": "podinfo-c6497595d-zgwsx",
  "version": "6.17.0",
  "message": "greetings from podinfo v6.17.0"
}

Debug

kubectl describe sur une Gateway ou une HTTPRoute affiche le message associé à chaque condition, souvent suffisant pour comprendre le problème. Sinon, direction les logs du contrôleur : kubectl -n envoy-gateway-system logs deploy/envoy-gateway.

Les pièges en résumé

Symptôme Cause Solution
Service Envoy en <pending> Service LoadBalancer sans contrôleur de load balancer sur kind EnvoyProxy avec type: NodePort
curl pendu sur localhost:8080 externalTrafficPolicy: Local alors que le trafic entre par le control-plane externalTrafficPolicy: Cluster
provided port is already allocated dans les logs d'Envoy Gateway Le NodePort 30080 est encore tenu par un autre Service Attendre la bascule, ou libérer le port dans un commit séparé
Flux ne connaît pas Gateway ou HTTPRoute au premier déploiement CRDs installées de façon asynchrone par une HelmRelease Kustomization séparée avec wait: true puis dependsOn
CRDs pas à jour après un upgrade d'Envoy Gateway Helm ne met jamais à jour le dossier crds/ crds: CreateReplace avec Flux, ou le chart gateway-crds-helm
404 sur une URL attendue Le header Host ne correspond à aucun hostnames Vérifie le hostname de l'HTTPRoute et l'URL appelée
HTTPRoute avec Accepted=False Le namespace de la route n'est pas autorisé par la Gateway Ajuste allowedRoutes sur le listener

Pour aller plus loin

  • HTTPS : ajoute un listener HTTPS sur la Gateway avec un certificat géré par cert-manager. Le port mapping 8443 → 30443 est déjà prêt, il suffit d'une deuxième entrée dans le patch de l'EnvoyProxy.
  • Les policies Envoy Gateway : ClientTrafficPolicy et BackendTrafficPolicy pour les timeouts, retries et le rate limiting, SecurityPolicy pour JWT, OIDC, CORS ou basic auth. Avec Envoy Gateway, l'authentification OIDC se fait nativement, là où Istio passe par oauth2-proxy, que je détaille dans Sécurise tes apps avec OAuth2 Proxy, Istio Gateway API & OIDC.