kgateway migration
kgateway migration patterns: TLS, redirects, rewrites, and canaries
High-value route conversions with explicit portable Gateway API fields versus kgateway policies—and what Universal Helm Chart route.* can (and cannot) express.
How to read examples
Runnable: commands and YAML you can apply after review. Illustrative: shapes taken from official kgateway migration examples—re-run ingress2gateway against your cluster version before production. Chart keys cited only when they exist in application 0.4.3.
Chart 0.4.3 — args / multi-port / service.enabled
From application 0.4.3: main container args (default []); Deployment revisionHistoryLimit (default 10); multi-port via containerPorts and service.ports; optional chart-managed Service via service.enabled (default true). When service.ports is set, Ingress / simple HTTPRoute / NOTES use the first entry (templates/_helpers.tpl application.servicePort). Legacy single-port service.name / service.port / service.protocol / service.appProtocol remain supported. Set service.enabled: false when the workload does not need a chart-managed Service. Do not invent other port keys.
Chart 0.4.3 — route.spec
From application 0.4.3, set route.spec to a complete HTTPRoute.spec (parentRefs, hostnames, rules, filters, multi-backendRefs). When route.spec is non-empty, the chart renders it unchanged and ignores the simple route.hostname / route.path / route.gateway generator. Simple route.* remains the default path; route.gateway is required only when route.enabled: true and route.spec is empty. Do not invent shorthand keys like route.headers or route.cors.
Chart 0.4.3 — service.appProtocol
Optional service.appProtocol (default "") sets Service.spec.ports[].appProtocol on the chart-managed Service (templates/7_service.yaml). For Services created by ingressPlain paths with createService: true, set paths[].service.appProtocol (since 0.4.0). Leave empty when you do not need a protocol hint (for example grpc). Do not invent other service.* protocol keys.
Tested versions
- Chart
application 0.4.3; Gateway API API group gateway.networking.k8s.io/v1
- Conversion patterns sourced from kgateway migration examples
- Battle evidence (historical): Kubernetes v1.35.4—not the disposable lab pin
- Reproducible lab (DEV-157 lockfile):
kindest/node:v1.33.1; Gateway API v1.6.1 standard; kgateway Helm v2.4.3; platform NS kgateway-system
Shared convert command
ingress2gateway print \
--providers=ingress-nginx \
--emitter=kgateway \
--input-file CASE.yaml > CASE-kgateway.yaml
# Review, edit parentRefs to your shared Gateway, then apply
TLS termination at the Gateway
Portable: TLS usually lives on the Gateway listener (certificateRefs), not on each HTTPRoute. Chart apps set route.sectionName to the HTTPS listener name.
# Platform-owned Gateway (illustrative)
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: external
namespace: kgateway-system
spec:
gatewayClassName: kgateway
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: "*.example.com"
tls:
mode: Terminate
certificateRefs:
- name: wildcard-example-tls
# Secret provisioned out-of-band (cert-manager / External Secrets / sealed); never commit PEM
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
allow-external-gateway: "true"
# App chart values (runnable contract)
route:
enabled: true
gateway: external
gatewayNamespace: kgateway-system
sectionName: https
hostname: app.example.com
path: /
Platform listeners may use wildcard hostnames; app HTTPRoute / chart route.hostname should stay exact to limit hijack risk.
Passthrough: nginx.ingress.kubernetes.io/ssl-passthrough maps toward TLSRoute + passthrough listener in the kgateway emitter—not an HTTPRoute, and not a chart route.* feature. See ingress-nginx provider.
Validate:
kubectl get gateway -n kgateway-system
kubectl describe gateway -n kgateway-system external
# Expect: Accepted=True and Programmed=True
kubectl describe httproute -n apps app
# Expect parents: Accepted=True, ResolvedRefs=True
curl -sS -o /dev/null -w '%{http_code}\n' https://app.example.com/
HTTP → HTTPS redirect
Old Ingress (runnable input):
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ssl-redirect-demo
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
rules:
- host: secure.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-app
port:
number: 8080
Reviewed HTTPRoute filter (portable Gateway API; illustrative from kgateway SSL redirect example):
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: ssl-redirect-demo-secure-example-com
spec:
hostnames:
- secure.example.com
parentRefs:
- name: nginx
rules:
- matches:
- path:
type: PathPrefix
value: /
filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
Known difference: ingress-nginx often uses 308; Gateway API redirect examples use 301. Chart route.* has no redirect filter—keep redirect on a reviewed HTTPRoute or listener design. Docs: SSL Redirect.
curl -sS -o /dev/null -w '%{http_code} %{redirect_url}\n' http://ADDRESS/ -H 'Host: secure.example.com'
Path rewrite
Old Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: rewrite-demo
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /api/v1(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: backend-svc
port:
number: 80
Reviewed portable filter (illustrative):
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /
kgateway emitter may also emit rewrite-related TrafficPolicy for some annotation shapes—treat those as kgateway-specific and review. Chart limitation: route.path only selects the match path; it does not emit URLRewrite. Docs: URL Rewriting.
Edge case: bare /api/v1 with ReplacePrefixMatch: / can yield an empty backend path—add an extra match or normalize in the app.
Header matching and modifiers
Portable: HTTPRoute matches[].headers and RequestHeaderModifier / ResponseHeaderModifier filters (HTTPRoute API). NGINX header annotations convert via the emitter to header modifier filters—see Header Modifiers.
# Illustrative portable match (not a chart values key)
matches:
- path:
type: PathPrefix
value: /
headers:
- name: X-Canary
value: "true"
Simple route.* has no header-match shorthand. From 0.4.3, put header matches under route.spec.rules[].matches[].headers (complete HTTPRoute.spec), or ship a reviewed extraManifests HTTPRoute. Do not invent route.headers.
Weighted canaries
Old Ingress pair (runnable input from kgateway canary example):
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-primary
spec:
ingressClassName: nginx
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app-v1
port:
number: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-canary
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "20"
spec:
ingressClassName: nginx
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app-v2
port:
number: 80
Reviewed HTTPRoute (portable weights; illustrative):
backendRefs:
- name: app-v1
port: 80
weight: 80
- name: app-v2
port: 80
weight: 20
Chart note: one release renders a single backendRefs entry with route.backendWeight (default 1). Multi-backend canaries need a reviewed multi-ref HTTPRoute (or two Services coordinated outside the single-backend template). Docs: Canary Deployments.
# Spot-check split (statistical; not exact in one curl)
for i in $(seq 1 50); do curl -sS -H 'Host: app.example.com' http://ADDRESS/version; echo; done | sort | uniq -c
Status, curl, rollback (every case)
kubectl describe httproute -n apps NAME
kubectl get httproute -n apps NAME -o jsonpath='{range .status.parents[*]}{.conditions}{"\n"}{end}'
curl -sS -D- -o /dev/null -H 'Host: HOST' http://ADDRESS/PATH
- Rollback: re-apply previous Ingress (or chart
ingress.enabled: true), detach HTTPRoute parentRefs / delete route, restore DNS to NGINX.
- Failure modes: wrong weights summing unexpectedly; redirect loops if both NGINX and Gateway redirect; rewrite empty-path; header match never hits because clients omit the header.
Annotation → destination map (partial)
| NGINX annotation / behavior | Destination | Kind |
| host / path rules | HTTPRoute hostnames + matches | Portable |
| canary-weight | HTTPRoute backendRefs.weight | Portable |
| ssl-redirect / force-ssl-redirect | RequestRedirect filter (301) | Portable (status-code difference) |
| rewrite-target | URLRewrite and/or TrafficPolicy | Mixed—review emitter output |
| configuration-snippet / server-snippet | Often unsupported | Gap—do not invent parity |
| cors-* | TrafficPolicy | kgateway-specific |
| limit-rps / rate limit | TrafficPolicy | kgateway-specific |
| affinity / session | BackendConfigPolicy (per docs) | kgateway-specific |
| auth-url / oauth | GatewayExtension + policy | kgateway-specific |
Policy depth (CORS, rate limit, external auth, backend TLS) is intentionally out of scope here—schedule as the next series article after tech-review of this trio.
Security
- Do not paste real TLS private keys or basic-auth Secrets into Git.
- Canary without auth on the canary Service can leak unreleased builds—protect both backends.
- Header-based canaries are forgeable from clients; prefer weight-based or mesh identity for sensitive splits.
Hands-on migration → · Gateway API vs Ingress → · Official examples · Chart HTTPRoute