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

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

Annotation → destination map (partial)

NGINX annotation / behaviorDestinationKind
host / path rulesHTTPRoute hostnames + matchesPortable
canary-weightHTTPRoute backendRefs.weightPortable
ssl-redirect / force-ssl-redirectRequestRedirect filter (301)Portable (status-code difference)
rewrite-targetURLRewrite and/or TrafficPolicyMixed—review emitter output
configuration-snippet / server-snippetOften unsupportedGap—do not invent parity
cors-*TrafficPolicykgateway-specific
limit-rps / rate limitTrafficPolicykgateway-specific
affinity / sessionBackendConfigPolicy (per docs)kgateway-specific
auth-url / oauthGatewayExtension + policykgateway-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

Hands-on migration → · Gateway API vs Ingress → · Official examples · Chart HTTPRoute