kgateway migration
Ingress NGINX to kgateway: a hands-on migration
Convert, review, attach to a shared Gateway, then flip Universal Helm Chart from ingress.* to route.*—never blind-apply converter output.
What the chart handles
The application cutover below uses Universal Helm Chart application 0.4.3 and its ingress.* and route.* values. The converter may also produce platform resources that the chart does not own. Keep those in your platform repository. See kgateway's migration guide for the converter workflow.
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.
Versions used
- Chart:
application 0.4.3
- Production reference: Kubernetes v1.35.4 (2026-07-23)
- Reproducible lab:
kindest/node:v1.33.1, Gateway API v1.6.1 standard channel, and kgateway Helm v2.4.3 installed in kgateway-system
- Gateway API API group:
gateway.networking.k8s.io/v1
- Tooling: kgateway
ingress2gateway with --providers=ingress-nginx --emitter=kgateway (install per kgateway docs)
Prerequisites
- Cluster with Ingress NGINX serving the sample host (or YAML-only dry-run)
- Gateway API CRDs + kgateway installed; a
GatewayClass (commonly kgateway) and a target Gateway you intend to keep (shared platform Gateway preferred over one Gateway per app)
ingress2gateway from the kgateway migration distribution
helm 3.x; chart repo https://chaser100.github.io/u-helm-chart
1. Capture the old Ingress
Example primary Ingress (no special annotations). Runnable YAML from the kgateway basic example pattern:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-app
namespace: apps
spec:
ingressClassName: nginx
rules:
- host: demo.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: demo-app
port:
number: 8080
Same intent as chart values:
service:
port: 8080
ingress:
enabled: true
className: nginx
hosts:
- host: demo.example.com
paths:
- path: /
pathType: Prefix
2. Convert with ingress2gateway
ingress2gateway print \
--providers=ingress-nginx \
--emitter=kgateway \
--input-file demo-ingress.yaml > demo-kgateway.yaml
# Optional: specific IngressClass
# --ingress-nginx-ingress-class=internal-nginx
# Experimental Gateway API fields only when you knowingly opt in
# --allow-experimental-gw-api
For a live cluster export instead of a file, follow the print modes documented at Migrate from Ingress. Always write output to a review branch—do not pipe straight to kubectl apply.
3. Review generated resources (illustrative shape)
Basic conversion yields a Gateway + HTTPRoute. Reviewed shape (aligned with kgateway basic example; names may differ):
Review before applying
The generated Gateway below listens on HTTP, does not restrict allowedRoutes, and may duplicate a Gateway your platform already owns. In most clusters, the safer change is to point HTTPRoute.parentRefs at the existing Gateway in kgateway-system, then verify its TLS listener and namespace policy.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: nginx
namespace: kgateway-system
spec:
gatewayClassName: kgateway
listeners:
- name: demo-example-com-http
hostname: demo.example.com
port: 80
protocol: HTTP
# Missing in raw converter output — add before apply:
# allowedRoutes:
# namespaces:
# from: Selector
# selector:
# matchLabels:
# allow-external-gateway: "true"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: demo-app-demo-example-com
namespace: apps
spec:
hostnames:
- demo.example.com
parentRefs:
- name: nginx
namespace: kgateway-system
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: demo-app
port: 8080
Before applying the output:
- Prefer attaching the HTTPRoute to your existing shared Gateway (
parentRefs.name / namespace / sectionName) instead of proliferating converter-generated Gateways.
- Confirm listener protocol/port/TLS match production (HTTP-only output is common from annotation-free Ingresses).
- Separate portable fields (
hostnames, matches, backendRefs, standard filters) from kgateway-specific CRDs (TrafficPolicy, BackendConfigPolicy, GatewayExtension) when annotations were present.
- Never claim annotation parity—unsupported annotations must be listed as gaps.
4. Status checks
kubectl apply -f reviewed-demo-httproute.yaml
kubectl get gateway -n kgateway-system
kubectl describe gateway -n kgateway-system <name>
# Expect: Accepted=True and Programmed=True
kubectl get httproute -n apps
kubectl describe httproute -n apps demo-app-demo-example-com
# Healthy parents: Accepted=True and ResolvedRefs=True
kubectl get httproute -n apps demo-app-demo-example-com -o yaml | rg -n 'type:|status:|reason:|message:'
5. Curl validation
# Resolve to Gateway LB / node entry; replace ADDRESS
curl -sS -o /dev/null -w '%{http_code} %{url_effective}\n' \
-H 'Host: demo.example.com' http://ADDRESS/
# After TLS listener cutover:
curl -sS -o /dev/null -w '%{http_code}\n' https://demo.example.com/
6. Cut over Universal Helm Chart values
Once the platform Gateway exists, stop rendering Ingress from the chart and enable HTTPRoute:
# Before (Ingress)
ingress:
enabled: true
className: nginx
hosts:
- host: demo.example.com
paths:
- path: /
pathType: Prefix
# After (HTTPRoute) — route.gateway required
ingress:
enabled: false
route:
enabled: true
gateway: external
gatewayNamespace: kgateway-system
sectionName: https
hostname: demo.example.com
path: /
pathMatchType: PathPrefix
backendWeight: 1
helm upgrade --install demo-app universal/application --version 0.4.3 \
-n apps --create-namespace -f values-route.yaml
kubectl get httproute -n apps
kubectl describe httproute -n apps demo-app
Optional dual-publish: keep Ingress enabled only while DNS/LB still points at NGINX; disable as soon as Gateway serves production traffic.
7. Coexistence and rollback
- Coexistence: different entrypoints (NGINX Service vs Gateway LB) can both host the hostname temporarily; control which receives public DNS.
- Rollback: set
route.enabled: false, restore ingress.enabled: true, helm upgrade, point DNS back to Ingress; delete or detach the HTTPRoute if it conflicts.
- Do not delete the Ingress controller until every hostname is verified on Gateway and rollback window has closed.
Failure modes
Accepted=False — listener/section mismatch, Gateway missing, or namespace not in allowedRoutes.
ResolvedRefs=False — wrong Service name/port, missing ReferenceGrant for cross-namespace backends.
- Converter Gateway per app — operational sprawl; fold into shared Gateway before GitOps.
- Blind apply of
TrafficPolicy from annotations — review auth/CORS/rate-limit semantics against kgateway docs.
- Chart still has
ingress.enabled: true after cutover — dual objects fighting the same host.
Portable vs kgateway-specific
| Concern | Prefer | Label as |
| Host / path / weight / standard filters | HTTPRoute fields | Portable Gateway API |
| HTTP→HTTPS redirect filter | RequestRedirect on HTTPRoute | Portable (note NGINX 308 vs Gateway API 301) |
| CORS, rate limit, basic auth, many timeouts | TrafficPolicy / related | kgateway-specific |
| Backend TLS / session affinity (per docs) | BackendConfigPolicy | kgateway-specific |
| External auth / OIDC extensions | GatewayExtension + policy | kgateway-specific |
Depth examples: kgateway migration patterns. Annotation coverage: ingress-nginx provider, kgateway emitter.
Concepts: Gateway API vs Ingress → · TLS, redirects, rewrites, canaries → · Chart route.* examples →