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 à laGatewayClass.
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 :
---
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 :
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¶
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/.
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¶
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¶
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¶
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.
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 :
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
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 Gatewayhomelab, dans un autre namespace. C'est possible parce que la Gateway l'autorise viaallowedRoutes.hostnames: les noms en*.localhostsont résolus en127.0.0.1par curl et par les navigateurs Chromium et Firefox, sans toucher à/etc/hosts. Chaque nouvelle app aura sa propreHTTPRouteet son hostname, derrière la même Gateway et le même port8080.backendRefs: le Service est dans le même namespace que la route. Pour viser un Service d'un autre namespace, il faudrait en plus unReferenceGrantdans le namespace du Service.- La route vit dans l'overlay
apps/kind/et pas dansapps/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 :
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
HTTPSsur la Gateway avec un certificat géré par cert-manager. Le port mapping8443 → 30443est déjà prêt, il suffit d'une deuxième entrée dans lepatchde l'EnvoyProxy. - Les policies Envoy Gateway :
ClientTrafficPolicyetBackendTrafficPolicypour les timeouts, retries et le rate limiting,SecurityPolicypour 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.