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

Prerequisites

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:

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

Failure modes

Portable vs kgateway-specific

ConcernPreferLabel as
Host / path / weight / standard filtersHTTPRoute fieldsPortable Gateway API
HTTP→HTTPS redirect filterRequestRedirect on HTTPRoutePortable (note NGINX 308 vs Gateway API 301)
CORS, rate limit, basic auth, many timeoutsTrafficPolicy / relatedkgateway-specific
Backend TLS / session affinity (per docs)BackendConfigPolicykgateway-specific
External auth / OIDC extensionsGatewayExtension + policykgateway-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 →