Die Kubernetes Gateway API ist der offizielle Nachfolger der Ingress API. Neben vieler technischer Neuerungen und Vorteile hat uns allerdings das Abkündigen der Weiterentwicklung des NGINX Ingress Controller zum Handeln gefordert.
Genau dieses Handeln soll Inhalt dieses Beitrags sein und unsere Migration einer produktiven Multi-Environment-Plattform vom NGINX Ingress Controller zur GatewayAPI mittels Envoy Gateway dokumentieren.
Ausgangslage
Unsere Plattform läuft auf provisionierten Kubernetes Clustern auf OVH mit mehreren Umgebungen. Die Infrastruktur wird mittels Pulumi verwaltet und alle Ressourcen auf den Clustern werden deklarativ via Helm und ArgoCD bereitgestellt. Vor der Migration war unser Setup typisch aufgeteilt, mit einem zentralen NGINX Ingress Controller und entsprechender zentraler Konfiguration, so wie die dezentralen Ingresse, welche für die verschiedenen Anwendungen als Einstiegspunkte agierten und jeweils nochmal eigene Konfiguration mitbrachten.
NGINX Ingress hat uns bis dahin gut gedient, hatte aber ein paar Mankos: Annotations sind untypisiert, Security-Features wie IP-Whitelisting und WAF erfordern Annotation-Bloat und das Routing-Modell kennt keine Trennung zwischen Infrastruktur- und Applikationsverantwortung.
Die Architektur der Gateway API
Das zentrale Konzept der Gateway API ist die Rollentrennung:
Diese Trennung haben wir 1:1 umgesetzt: Die Gateway-Infrastruktur liegt im eigenen Namespace und wird zentral verwaltet, während die HTTPRoutes in den jeweiligen App-Namespaces durch die Entwickler:innen definiert werden.
Schritt 1: Envoy Gateway installieren
Wir deployen Envoy Gateway über zwei Helm-Charts. Eines für die CRDs, eines für den Controller:
# Chart.yaml (CRDs)
dependencies:
- name: gateway-crds-helm
version: v1.7.1
repository: oci://docker.io/envoyproxy
# Chart.yaml (Controller)
dependencies:
- name: gateway-helm
version: v1.7.1
repository: oci://docker.io/envoyproxy
Die Trennung von CRDs und Controller ist wichtig: CRDs müssen vor dem Controller existieren. Mittels ArgoCD Sync-Waves kann man die Reihenfolge festlegen, so dass der Ablauf gewährleistet ist.
Schritt 2: GatewayClass und EnvoyProxy konfigurieren
Die GatewayClass verbindet Kubernetes mit der Envoy-Gateway-Implementierung und referenziert eine EnvoyProxy-Ressource für infrastrukturspezifische Einstellungen:
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: eg
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
parametersRef:
group: gateway.envoyproxy.io
kind: EnvoyProxy
name: proxy-config
namespace: envoy-gateway
Die EnvoyProxy-Ressource steuert, wie der Data-Plane-Service provisioniert wird. In unserem Fall als OpenStack LoadBalancer von OVH mit Proxy Protocol:
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
name: proxy-config
spec:
provider:
type: Kubernetes
kubernetes:
envoyService:
type: LoadBalancer
externalTrafficPolicy: Local
annotations:
loadbalancer.openstack.org/proxy-protocol: "v2"
Diese Einstellungen sind notwendig, um die originale IP eines Requests zu übermitteln, um z.B. IP Whitelisting und Rate-Limiting zu realisieren.
Schritt 3: Das Gateway definieren
Das Gateway ist der zentrale Einstiegspunkt, dort definieren wir Listener und zugehörige Einstellungen.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: gateway
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-cert"
external-dns.alpha.kubernetes.io/hostname: "*.my.domain"
spec:
gatewayClassName: eg
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All
- name: https
protocol: HTTPS
port: 443
hostname: "*.my.domain"
allowedRoutes:
namespaces:
from: All
tls:
mode: Terminate
certificateRefs:
- name: letsencrypt-cert
Insbesondere zwei Aspekte fallen auf:
allowedRoutes.namespaces.from: All– HTTPRoutes aus allen Namespaces dürfen dieses Gateway nutzen. Das ermöglicht App-Teams, ihre Routen selbst zu verwalten.TLS-Terminierung geschieht am Gateway. Die Cert-Manager-Integration via Annotation funktioniert identisch wie bei Ingress.
Schritt 4: HTTP-zu-HTTPS-Redirect
Was bei NGINX Ingress eine Annotation war (nginx.ingress.kubernetes.io/ssl-redirect: "true") wird bei der Gateway API eine eigene HTTPRoute:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: http-to-https-redirect
spec:
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: gateway
sectionName: http
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
Über sectionName: http wird der Redirect gezielt nur an den HTTP-Listener gebunden. Expliziter, nachvollziehbarer und im Gegensatz zur Annotation, auch testbar.
Schritt 5: Proxy Protocol und Client-IP
Wer hinter einem LoadBalancer mit Proxy Protocol arbeitet, muss die ClientTrafficPolicy konfigurieren:
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: ClientTrafficPolicy
metadata:
name: enable-proxy-protocol
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: gateway
enableProxyProtocol: true
path:
escapedSlashesAction: KeepUnchanged
Bei NGINX war das use-proxy-protocol: "true" in der ConfigMap. Envoy Gateway macht daraus eine eigene Ressource, die gezielt auf ein Gateway zeigt. Vorteil: Unterschiedliche Gateways können unterschiedliche Policies haben.
Schritt 6: HTTPRoutes für Applikationen
Hier zeigt sich die Stärke der Gateway API im Alltag. Eine HTTPRoute für eine Anwendung könnte wie folgt aussehen:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
annotations:
external-dns.alpha.kubernetes.io/hostname: "thisis.my.domain"
name: http-route
spec:
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: gateway
namespace: envoy-gateway
hostnames:
- "thisis.my.domain"
rules:
- matches:
- path:
type: PathPrefix
value: /
filters:
- type: ResponseHeaderModifier
responseHeaderModifier:
set:
- name: Content-Security-Policy
value: "default-src 'self' ..."
backendRefs:
- name: service
port: 80
Drei Dinge, die wir bei der Migration gelernt haben:
Cross-Namespace-Routing: Die HTTPRoute lebt im App-Namespace, referenziert aber das Gateway aus dem zentral gemanagten Namespace via parentRefs.
D.h. HTTPRouten und Gateway können an unterschiedlichen Orten leben und mittels ReferenceGrants lässt sich z.B. Multi-Tenancy nativ umsetzen.Response Header nativ: CSP-Header werden direkt als
ResponseHeaderModifier-Filter definiert. Bei NGINX wäre die Umsetzung mittels Annotation erfolgt.External-DNS-Annotation: Bleibt auf der HTTPRoute – external-dns unterstützt die Gateway API nativ.
Schritt 7: Traffic-Management mit BackendTrafficPolicy
Envoy Gateway bietet granulares Traffic-Management als eigene Ressource:
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
name: selector-policy
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: http-route
timeout:
tcp:
connectTimeout: 10s
http:
connectionIdleTimeout: 10s
requestTimeout: 10s
rateLimit:
type: Local
local:
rules:
- limit:
requests: 100
unit: Second
circuitBreaker:
maxConnections: 5000
requestBuffer:
limit: "10Mi"
Was bei NGINX Ingress über ein Dutzend verschiedene Annotations verteilt war, wird hier in einer einzigen, typisierten Ressource gebündelt:
Schritt 8: Security Policies – IP-Whitelisting
IP-basierte Zugriffskontrolle war bei NGINX ebenfalls eine Annotation:
nginx.ingress.kubernetes.io/whitelist-source-range: "123.123.123.123/32,456.456.456.456/32"
Bei Envoy Gateway wird dies mittels SecurityPolicy umgesetzt mit einem expliziten Deny-by-Default-Modell:
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
name: portal-security-policy
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: http-route
authorization:
defaultAction: Deny
rules:
- name: allow-internal
action: Allow
principal:
clientCIDRs:
- "123.123.123.123/32"
- "456.456.456.456/32"
Das ist ausdrucksstärker: Man definiert explizite Allow-Regeln mit benannten Principals statt einer flachen Komma-separierten IP-Liste. In Kombination mit Helm-Templating lassen sich so elegant mehrere Umgebungen steuern:
Schritt 9: Web Application Firewall mit Coraza
Zusätzlich brauchten wir eine WAF, welche die Funktionalität von ModSecurity übernimmt. Envoy Gateway integriert Coraza als Wasm-Plugin über die EnvoyExtensionPolicy:
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyExtensionPolicy
metadata:
name: http-route-waf
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: http-route
wasm:
- name: coraza-waf
rootID: "coraza"
code:
type: Image
image:
url: "ghcr.io/corazawaf/coraza-proxy-wasm:0.6.0"
config:
directives_map:
default:
- "Include @recommended-conf"
- "SecRuleEngine On"
- "SecRequestBodyAccess On"
- "SecAuditEngine On"
- "SecAuditLogFormat JSON"
- "SecAuditLog /dev/stdout"
- "Include @crs-setup-conf"
- "Include @owasp_crs/*.conf"
default_directives: default
Die OWASP Core Rule Set (CRS) wird direkt eingebunden. Die WAF lässt sich pro Route aktivieren und entsprechend individuell konfigurieren. Bei NGINX Ingress war ModSecurity ebenfalls wieder über die Annotation der Ingresse konfigurierbar, was leider zu sehr viel Annotation-Bloat führte und schwer testbar war.
Wir sind doch nun fertig, oder?
Wer im Verlauf des Artikels genau gelesen hat, wird sehen, dass der Cert-Manager eingesetzt wird. Damit sind wir aber noch nicht vollständig funktionsfähig, denn wir verwenden zusätzlich noch ExternalDNS, um dynamisch Änderungen an DNS-Einträgen vorzunehmen. Beide Tools sind nativ bereits in der Lage, die GatewayAPI zu bedienen, müssen aber noch entsprechend konfiguriert werden, damit das auch geschieht.
Dazu aktiviert man die Verwaltung von Zertifikaten bei Ressourcen der GatewayAPI wie folgt:
cert-manager:
installCRDs: true
config:
apiVersion: controller.config.cert-manager.io/v1alpha1
kind: ControllerConfiguration
enableGatewayAPI: true
Damit die zugehörigen DNS Einträge auch automatisiert angelegt werden, muss ExternalDNS wissen, welche Ressourcen dafür in Betracht kommen, d.h. man ergänzt gateway-httproute.
external-dns:
sources:
- gateway-httproute
- ingress
- service
Damit sind wir nun in der Lage, automatisiert Zertifikate und DNS Einträge erstellen zu lassen, wenn Entwickler:innen neue Routen anlegen, die mittels Wildcard (*.my.domain) als gültig erachtet werden.
Lessons Learned
Was gut lief:
Die Rollentrennung (Gateway vs. HTTPRoute) passt perfekt zu unserem GitOps-Modell: Zentrale Verwaltung der Infrastruktur, also GatewayClass und Gateway und die Entwickler:innen legen selbstständig ihre benötigten Routen an.
Die typisierten Policies (BackendTrafficPolicy, SecurityPolicy) eliminieren die Notwendigkeit, aufgeblähte Annotations zu verwenden und lassen sich testen.
Cross-Namespace-Routing vereinfacht das Multi-Tenant-Setup erheblich.
Die WAF-Integration via Wasm ist robust, performant und läuft nahezu identisch zu ModSecurity.
Worauf man achten sollte:
CRD-Reihenfolge: Envoy Gateway CRDs müssen vor dem Controller installiert werden. Bei ArgoCD löst man das über separate Applications mit Sync-Waves.
Proxy Protocol: Wenn der LoadBalancer Proxy Protocol v2 nutzt, muss die ClientTrafficPolicy konfiguriert werden – sonst sind alle Client-IPs die des LoadBalancers und wir können kein RateLimiting oder IP Whitelisting umsetzen.
Fazit
Die Migration von NGINX Ingress zu Envoy Gateway war kein Selbstzweck. Getrieben aus der Not heraus, gewann unsere Konfiguration an Ausdrucksstärke, Sicherheit und Wartbarkeit. Die GatewayAPI ist der Kubernetes-Standard der Zukunft und bietet schon jetzt viele Vorteile, jedoch kann ich nicht uneingeschränkt sagen, dass alles von vorherein funktionierte. Gerade die Notwendigkeit, auf die Implementierung eines Anbieters zurückzugreifen, um bestimmte Dinge wie z.B. SecurityPolicies umzusetzen, hat mich ein wenig überrascht, denn genau eine solche Funktionalität erhoffe ich mir in Zukunft als First-Class-Citizen der GatewayAPI und den entsprechenden Implementierungen und nicht als provider-spezifische Ressource.
Zusätzlich gibt es leider nach wie vor bei allen Anbietern noch mehr als genug Kinderkrankheiten (siehe diese Benchmark), auch wenn wir bisher von diesen verschont sind, zeigt sich eindeutig die Notwendigkeit hier auf dem aktuellsten Stand zu bleiben, um einerseits Performance weiter zu ermöglichen, aber auch um Sicherheit und Stabilität zu gewährleisten.
Die vollständige Migration inklusive WAF hat bei uns über alle Umgebungen hinweg ca. eine gute Woche gedauert und bisher sind keinerlei Ausfälle oder Kinderkrankheiten zu beklagen.
Wenn Ihnen dieser Artikel gefallen hat, lesen Sie auch gerne weitere Artikel zum Thema Datensouveränität auf unserem Blog:
Oder informieren Sie sich auf unserer Leistungsseite zu Digitaler Souveränität und melden Sie sich dort zu unserem Themennewsletter an.
Wenn Sie auf der Suche nach Unterstützung für Ihre Internal Developer Platform (IDP) sind, sprechen Sie uns gerne an – wir unterstützen Sie bei der Auswahl und Umsetzung geeigneter Lösungen.