Dernier article sur Envoy Gateway ! Aujourd'hui, comment éviter d'avoir un Service LoadBalancer par Gateway déployée. La réponse d'Envoy Gateway : mergeGateways.

Lorsque j'ai migré depuis Ingress NGINX, j'ai été un peu dérouté avec la notion de HTTPRoutes, Gateways, etc. Et j'ai remarqué que pour chaque Gateway définie dans le cluster, Envoy Gateway génère son propre déploiement Envoy et son propre Service de type LoadBalancer. Si sur le principe c'est cohérent avec la spec, j'aimerais un moyen de mutualiser les LoadBalancers entre les Gateways du cluster, pour notamment économiser des coûts de cloud (et les IPs publiques).

La problématique : un LoadBalancer par Gateway

En théorie, les équipes devs n'ont pas à gérer les Gateways, seulement les Routes, c'est aux ops de gérer cette partie. Mais parfois on peut donner de l'autonomie aux équipes, et qu'elles gèrent leurs propres Gateways avec leur policies. Sans mergeGateways, le schéma est le suivant : chaque équipe qui déploie une Gateway pour son application se retrouve avec son propre LB provisionné côté cloud, sa propre IP.

1Gateway "eg" (envoy-gateway-system)      → Service LB → IP 203.0.113.10
2Gateway "app1-gw"                        → Service LB → IP 203.0.113.11
3Gateway "app2-gw"                        → Service LB → IP 203.0.113.12
4Gateway "monitoring-gw" (monitoring)     → Service LB → IP 203.0.113.13
5[...]

Ce comportement est différent d'avant avec les ingress, où j'avais 1 seul LB pour toutes mes ressources. On va voir comment retrouver cette façon de faire.

La solution : mergeGateways

Envoy Gateway propose via la ressource EnvoyProxy un paramètre mergeGateways. Quand il est à true, toutes les Gateway utilisant la même GatewayClass partagent un seul déploiement Envoy et un seul Service LoadBalancer.

Fusion

La ressource EnvoyProxy

C'est ici qu'on active la fusion des Gateways, et qu'on configure le Service LB partagé :

 1# envoyproxy-custom-proxy-config.yaml
 2apiVersion: gateway.envoyproxy.io/v1alpha1
 3kind: EnvoyProxy
 4metadata:
 5  name: custom-proxy-config
 6  namespace: envoy-gateway-system
 7spec:
 8  mergeGateways: true # <-- là
 9  provider:
10    type: Kubernetes
11    kubernetes:
12      envoyService:
13        type: LoadBalancer
14      envoyDeployment:
15        [...]

Le GatewayClass qui référence l'EnvoyProxy

La GatewayClass fait le lien entre les Gateway et notre configuration EnvoyProxy, via parametersRef :

 1# gatewayclass-eg.yaml
 2apiVersion: gateway.networking.k8s.io/v1
 3kind: GatewayClass
 4metadata:
 5  name: eg
 6spec:
 7  controllerName: gateway.envoyproxy.io/gatewayclass-controller
 8  parametersRef:
 9    group: gateway.envoyproxy.io
10    kind: EnvoyProxy
11    name: custom-proxy-config # <-- mergeGateways: true
12    namespace: envoy-gateway-system

Toute Gateway déclarée avec gatewayClassName: eg bénéficiera automatiquement du merge.

La Gateway principale

La gateway "centrale" reste dans envoy-gateway-system, avec ses listeners HTTP/HTTPS habituels :

 1# gateway-eg.yaml
 2apiVersion: gateway.networking.k8s.io/v1
 3kind: Gateway
 4metadata:
 5  name: eg
 6  namespace: envoy-gateway-system
 7spec:
 8  gatewayClassName: eg
 9  listeners:
10    - name: http
11      protocol: HTTP
12      port: 80
13      allowedRoutes:
14        namespaces:
15          from: All
16    - name: https
17      protocol: HTTPS
18      port: 443
19      tls:
20        mode: Terminate
21        certificateRefs:
22          - kind: Secret
23            name: ip-cert-tls # Voir article précédent
24      allowedRoutes:
25        namespaces:
26          from: All

Exemple : App1 avec sa propre Gateway

Avec mergeGateways, chaque application peut avoir sa propre Gateway dans son propre namespace.

La Gateway applicative

App1 déclare sa Gateway dans son namespace app1, avec son listener HTTPS et son certificat Let's Encrypt géré par cert-manager via l'annotation :

 1# app1-gateway.yaml
 2apiVersion: gateway.networking.k8s.io/v1
 3kind: Gateway
 4metadata:
 5  name: app1-gateway
 6  namespace: app1
 7  annotations:
 8    cert-manager.io/cluster-issuer: letsencrypt-prod-eg
 9spec:
10  gatewayClassName: eg
11  listeners:
12    - name: https
13      protocol: HTTPS
14      hostname: app1.gravitek.io
15      port: 443
16      tls:
17        mode: Terminate
18        certificateRefs:
19          - kind: Secret
20            name: app1.gravitek.io-tls
21      allowedRoutes:
22        namespaces:
23          from: Same

La HTTPRoute

 1# app1-httproute.yaml
 2apiVersion: gateway.networking.k8s.io/v1
 3kind: HTTPRoute
 4metadata:
 5  name: app1
 6  namespace: app1
 7spec:
 8  parentRefs:
 9    - group: gateway.networking.k8s.io
10      kind: Gateway
11      name: app1-gateway
12      namespace: app1
13      sectionName: https
14  hostnames:
15    - app1.gravitek.io
16  rules:
17    - matches:
18        - path:
19            type: PathPrefix
20            value: /
21      backendRefs:
22        - kind: Service
23          name: app1
24          port: 8080
25          weight: 1

Résultat : App1 est accessible via la même IP publique que toutes les autres applications, sans LB supplémentaire, avec son propre certificat géré de façon autonome dans son namespace.

Remarques

  • Si besoin particulier, chaque application peut maintenant gérer des règles différentes sur les ClientTrafficPolicy, BackendTrafficPolicy & co.

  • ⚠️ Pour la gestion TLS, dans mon cas d'usage toutes les applis ont des routes HTTPS, pas HTTP. La redirection HTTP vers HTTPS est gérée par la Gateway par défaut, avec un redirect permanent. Cela m'a posé soucis avec cert-manager, et cette configuration marche bien chez moi.

Alternative : ListenerSets

Gateway API v1.3 propose une approche alternative avec les ListenerSets : une Gateway principale expose des listeners de base, et des ListenerSet permettent d'en ajouter depuis d'autres namespaces.

C'est séduisant car c'est natif Gateway API (pas une extension Envoy), avec un modèle de permissions plus granulaire sur les listeners. Mais il y a des limites importantes à date :

  • Seule HTTPRoute supportée : si vous utilisez des GRPCRoute, TCPRoute, elles ne peuvent pas référencer des listeners issus d'un ListenerSet.
  • Encore expérimental : la feature est en alpha dans Gateway API, le support varie selon les implémentations.

Pour l'instant, mergeGateways reste la solution que j'ai choisie, à vous de voir laquelle vous préférez.

Conclusion

Avec mergeGateways, on passe d'un modèle "un LB par Gateway" à "un LB pour tout le cluster", retrouvant à peu près la configuration que j'avais avec Ingress NGINX :

  • 💰 Un seul LoadBalancer partagé entre toutes les Gateways
  • 🌐 Une seule IP publique → gestion DNS simplifiée
  • 🧩 Autonomie conservée : chaque application gère sa propre Gateway dans son namespace, avec ses propres certificats et listeners

Bien sûr, rien ne vous empêche d'avoir plusieurs Gateways quand même, avec ou sans mergeGateways, cela dépend de votre besoin et architecture !

Ressources