Kubernetes Ingress to Gateway API

Paste one or more Ingress objects and get the Gateway API equivalent: one Gateway, one HTTPRoute per Ingress and host, and the ReferenceGrants the cross-namespace references need. Every controller-specific annotation is classified, and the ones with no Gateway API equivalent are named rather than dropped, because after the migration those fail without an error anywhere.

Paste below, or drop a file anywhere on this panel

Or drop a file anywhere on this panel. Nothing is uploaded: the analysis runs in this tab.

The answer appears here

Paste on the left and press Convert. Nothing leaves this tab.

Examples

Real input you can load into the tool above. Each one shows a different thing going wrong, because that is what the tool is for.

An nginx rewrite annotation

Which annotations have a Gateway API equivalent and which do not translate

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
    - host: example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api
                port:
                  number: 80

A plain Ingress

A host and path with no controller annotations, which translates cleanly

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web
spec:
  ingressClassName: nginx
  rules:
    - host: example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  number: 80

Common mistakes

These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.

  1. Expecting controller annotations to translate

    Annotations like rewrite-target are nginx-specific extensions with no place in the Gateway API spec. Some map to a filter, some have no equivalent at all.

    Instead:Check each annotation individually. The ones with no equivalent need a rethink, not a translation.

  2. Assuming a HTTPRoute attaches automatically

    A route must reference a Gateway through parentRefs, and the Gateway must allow the route's namespace. Both sides have to agree or the route is ignored silently.

    Instead:Set parentRefs, and check allowedRoutes on the Gateway listener.

  3. Treating GatewayClass as optional

    A Gateway with no GatewayClass matching an installed controller is accepted and never programmed. There is no error, only a Gateway that never gets an address.

    Instead:Confirm the GatewayClass exists and its controller is running before debugging routes.

What it catches

The host and path routing ports over without much thought. These are the parts where the converted manifest applies cleanly, reports every condition as True, and behaves differently.

The annotations are the migration

The routing translates in an afternoon. auth-url and auth-signin do not: there is no external auth filter in the standard channel, so the HTTPRoute applies, reports Accepted: True, and the route is open. configuration-snippet and server-snippet can contain anything and translate to nothing. whitelist-source-range, the basic auth pair and the ModSecurity keys go the same way. Every annotation on your input is classified as having a direct equivalent, needing an implementation-specific extension, or having none at all.

The HTTP to HTTPS redirect was a default you never wrote

ingress-nginx sets ssl-redirect to true by itself the moment spec.tls covers the host, so most Ingresses redirect without an annotation anywhere. Gateway API has no such default and no such annotation. Miss it and port 80 serves the application in the clear, or 404s, and nothing in any status says a redirect used to exist. A redirect route pinned to the HTTP listener is emitted for you.

backendRefs has no port name field

Ingress lets a backend name a port: service.port.name: http. BackendObjectReference has port and only port, typed as a number. There is no name anywhere in it. The output leaves the port off rather than guessing, so an unresolved backend reports ResolvedRefs: False instead of applying cleanly and sending traffic to whatever happened to be on the number somebody picked.

ReferenceGrant is new, and it is why the route does nothing

Consolidating twenty Ingresses onto one Gateway moves the Gateway away from the TLS Secrets. A certificateRefs entry reaching into another namespace needs a ReferenceGrant published in that namespace, which usually means another team applies it. Ingress had no cross-namespace references at all, so there is no habit to fall back on. The grants are emitted, in the right namespaces.

What changes when routing becomes four objects instead of one

An Ingress is one object holding a class, a certificate, a host, some paths and a pile of controller-specific annotations. Gateway API splits that across a GatewayClass, a Gateway, an HTTPRoute and sometimes a ReferenceGrant, and it gives the pieces owners: the controller owns the class, the platform team owns the Gateway and its listeners, the application team owns the route. The routing survives the split. The annotations are what decide how much work this actually is.

One IngressClass becomes two objects, and you write one of them

GatewayClass is cluster-scoped and names the controller by controllerName. The controller almost always installs its own, so creating a second one pointing at the same controller is a good way to end up with a Gateway nothing reconciles. Gateway is namespaced and is the half you write: it owns the listeners, the addresses and the certificates. One Gateway serves many HTTPRoutes, so five Ingresses become one Gateway and five routes, not five Gateways. There is also no default GatewayClass: spec.gatewayClassName is required and has no fallback, unlike the IngressClass marked is-default-class.

Ingress                     Gateway API

IngressClass "nginx"    ->  GatewayClass "nginx"
  controller: k8s.io/       controllerName: ...
  ingress-nginx             (installed for you)

                        ->  Gateway "nginx-gateway"
                              listeners: [...]
                              (you write this)

Ingress x5              ->  HTTPRoute x5
                              parentRefs: -> Gateway

kubectl get gatewayclass
NAME    CONTROLLER                        ACCEPTED
nginx   gateway.nginx.org/nginx-gateway   True

The certificate moves from the route to the listener

spec.tls[].secretName was per Ingress, so every team carried its own certificate. A listener is per Gateway, so the certificate is now the Gateway owner's, and the hostname moves onto the listener next to it. certificateRefs is per listener, which is why one TLS hostname is one listener and why the 64-listener cap is reachable. Listener selection is by the most specific hostname: an exact listener beats a wildcard listener for a name they both cover, and only after a listener is chosen does the route's own hostname matter. Two listeners cannot share a hostname on one port, so two Ingresses claiming one hostname with different Secrets is a collision that Ingress hid.

# Ingress
spec:
  tls:
    - hosts: [app.example.com]
      secretName: web-tls

# Gateway
spec:
  listeners:
    - name: https-app-example-com
      protocol: HTTPS
      port: 443
      hostname: app.example.com
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: web-tls

hostname: "*.example.com"  loses to
hostname: "app.example.com" for that name

pathType: two map, one cannot

Prefix becomes PathPrefix and Exact becomes Exact, and Prefix is faithful because both match on whole path segments split by /: neither is a string prefix, so /api never matches /apiary in either API. ImplementationSpecific is the one that cannot be translated, because it means whatever the controller decided. ingress-nginx reads it as a PCRE regular expression when use-regex or rewrite-target is set and as a prefix otherwise. The AWS Load Balancer Controller reads it as an ALB path pattern with * wildcards. GCE reads it as a prefix. That is a fact about a binary, not about the manifest.

Prefix                 ->  PathPrefix
Exact                  ->  Exact
ImplementationSpecific ->  you decide

path: /api      Prefix / PathPrefix
  /api          match
  /api/         match
  /api/v1       match
  /apiary       no: not a segment boundary

RegularExpression is Custom support, so a
conformant implementation may not have it,
and where it exists the engine is usually
RE2. RE2 has no lookahead, no lookbehind
and no backreferences. nginx uses PCRE.

A route with no sectionName attaches to every listener it fits

parentRefs names the Gateway. Without a sectionName it means the whole Gateway, so the route lands on the HTTP listener as well as the HTTPS one and the application answers unencrypted on port 80 beside its own redirect. That, plus the ssl-redirect default that no longer exists, is how an HTTPS-only application quietly starts serving plain text after a migration that applied cleanly. Pin the route with sectionName, then check port 80 with curl rather than assuming.

# attaches to http AND https
parentRefs:
  - name: nginx-gateway

# attaches to https only
parentRefs:
  - name: nginx-gateway
    sectionName: https-app-example-com

# and port 80 gets its own route:
rules:
  - filters:
      - type: RequestRedirect
        requestRedirect:
          scheme: https
          statusCode: 301   # 301 or 302 only

curl -sI http://app.example.com/ | head -1
HTTP/1.1 301 Moved Permanently

ReferenceGrant, and the two things it is not

Gateway API refuses a reference that crosses a namespace unless the target namespace publishes a ReferenceGrant permitting it. It covers a Gateway reaching a Secret for certificateRefs and an HTTPRoute reaching a Service for backendRefs. It does not cover an HTTPRoute attaching to a Gateway in another namespace: that is the listener's allowedRoutes.namespaces, which defaults to Same, and a grant has no effect on it. The failure in both cases is the same shape and it is not an apply error. The object is accepted and its status carries ResolvedRefs: False with reason RefNotPermitted, or Accepted: False with reason NotAllowedByListeners.

# in the namespace being referenced
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
  namespace: payments
spec:
  from:
    - group: gateway.networking.k8s.io
      kind: HTTPRoute
      namespace: frontend
  to:
    - group: ""
      kind: Service
      name: checkout

# a different problem, a different field
listeners:
  - allowedRoutes:
      namespaces:
        from: Selector     # Same is default
        selector:
          matchExpressions:
            - key: kubernetes.io/metadata.name
              operator: In
              values: [prod, staging]

kubectl get httproute web -o \
  jsonpath='{.status.parents[*].conditions}'

Merging a host is now specified, and order is not how it works

Several Ingresses claiming one host are merged by the controller today, with no rule in the API saying how. ingress-nginx settles a genuine collision by creation timestamp, oldest wins, and logs a line most people never read. Gateway API specifies it: Exact path match first, then the PathPrefix with the most characters, then method, then the most header matches, then the most query parameter matches. Ties go to the older creationTimestamp and then to the alphabetically first namespace and name. The consequence worth internalising is that rule order inside an HTTPRoute decides nothing. A rule you want to win needs a more specific match, and applying converted routes in a different order than the Ingresses were created in can swap which one wins a genuine collision.

precedence, in order:

1  path type Exact
2  longest PathPrefix, by characters
3  method match present
4  most header matches
5  most query param matches
6  oldest creationTimestamp
7  alphabetically first "{namespace}/{name}"

rules:
  - matches: [{path: {type: PathPrefix, value: /}}]
  - matches: [{path: {type: PathPrefix, value: /api}}]

/api/x hits the second one. Listing it
first would change nothing.

Canary Ingresses stop being separate objects

An ingress-nginx canary is a second Ingress carrying canary: true that shadows the primary for the same host and path. Gateway API has no shadowing, so a traffic split is one rule with two backendRefs and a weight each. Weights are relative rather than percentages: the implementation sums them and divides, so 80 and 20 is the same split as 8 and 2. A backendRef with weight 0 stays a valid reference and receives nothing, which is the clean way to leave a canary parked. canary-by-header becomes a header match on a second rule, which wins by specificity wherever it sits in the list. canary-by-cookie has no equivalent: there is no cookie match type, only path, method, headers and query parameters.

# two Ingresses become one rule
backendRefs:
  - name: web-stable
    port: 80
    weight: 80
  - name: web-next
    port: 80
    weight: 20

# header canary: a more specific rule
matches:
  - path: {type: PathPrefix, value: /}
    headers:
      - name: x-canary
        type: Exact
        value: always
backendRefs:
  - name: web-next
    port: 80

defaultBackend, and the half of it that does not survive

There is no defaultBackend. A PathPrefix rule on / is the lowest-precedence path match, so it does the same job for the hostnames on its route, and that is a deliberate choice rather than an automatic mapping. It does not do the other half: an Ingress default backend also caught requests for hosts the Ingress never listed, and a route scoped to hostnames cannot. If that behaviour mattered, it needs one more HTTPRoute with no hostnames at all, attached to the same listener, which matches anything no other route claims.

# Ingress
spec:
  defaultBackend:
    service: {name: fallback, port: {number: 80}}

# HTTPRoute, for this route's hostnames
rules:
  - matches:
      - path: {type: PathPrefix, value: /}
    backendRefs:
      - name: fallback
        port: 80

# and for hosts nothing claims:
spec:
  parentRefs: [{name: nginx-gateway}]
  # no hostnames at all
  rules:
    - backendRefs: [{name: fallback, port: 80}]

More kubernetes tools

Kubernetes YAML Generator Answer a few questions, get a manifest Kubernetes YAML Validator Is this YAML valid, and where is it wrong? Kubernetes Rollout & Probe Timing Restart loops, capacity dips and stuck drains Kubernetes Service Checker Will this Service have any endpoints? Kubernetes Network Policy Viewer What can reach what, and what just broke Kubernetes Manifest Diff What actually changed Kubernetes Resource Calculator What these manifests actually reserve Kubernetes RBAC Viewer Who can actually do what Kubernetes Deployment Generator See the capacity dip before you deploy Kubernetes Service Generator port, targetPort, and which one you meant Kubernetes StatefulSet Generator Stable names, and volumes that outlive you Kubernetes QoS Class Calculator And where you sit in the eviction order Kubernetes Node Capacity Planner Allocatable is not capacity Kubernetes Max Pods per Node Calculator The ENI limit, not the 110 default Kubernetes Label Selector Tester Which key failed, not just that one did Kubernetes Taint & Toleration Tester Tolerations permit, they do not attract Kubernetes Resource Name Validator RFC 1123 subdomain, label, or 1035 Kubernetes Label & Annotation Validator Why your image tag is not a legal label Kubernetes CrashLoopBackOff Guide The status is a timer, not a cause Kubernetes TLS Certificate Decoder When it expires, and what it covers Kubernetes ServiceAccount Token Decoder Bound or legacy, and when it dies Kubernetes imagePullSecret Generator The registry URL nobody gets right Kubernetes cert-manager Certificates Issuer scope, and the rate limit that costs a week Kubernetes Flux HelmRelease Generator With the source it cannot work without Kubernetes Helm Chart Generator Chart.yaml, values, and working templates Kubernetes Argo CD Application Generator The defaults that are off, turned on Kubernetes Ingress Generator pathType, and the TLS secret that fails quietly Kubernetes HPA Generator Why it says unknown and never scales Kubernetes SecurityContext Generator Pod level, container level, and which wins Kubernetes Probe Generator How long it gets to start before liveness kills it Kubernetes PVC Generator Access modes your storage can actually do Kubernetes ResourceQuota Generator And the LimitRange that stops it breaking deploys Kubernetes CronJob Generator The fields that decide whether it runs Kubernetes NetworkPolicy Generator With the DNS rule already in it Kubernetes RBAC Generator And what it actually grants Kubernetes PodDisruptionBudget Generator Can this budget ever be satisfied? Kubernetes API Deprecation Checker What breaks on the next upgrade Kubernetes Kubeconfig Viewer Read it without handing it to anyone Kubernetes Quantity Parser What 512Mi, 100m and 1e3 actually mean Kubernetes Multi-Document YAML Splitter One file per object, in apply order Kubernetes .env to ConfigMap & Secret Split the credentials out on the way Kubernetes Secret Decoder See what is actually in there Kubernetes Manifest Visualizer What points at what, and what points at nothing Kubernetes Patch Tester Which --type, and what it deletes Kubernetes kubectl JSONPath Tester kubectl's dialect, not generic JSONPath Kubernetes Node Affinity Tester Required excludes, preferred only scores Kubernetes Helm Values Diff A missing key is a third value Kubernetes Pod and Service CIDR Fixed at cluster creation Kubernetes Secret Encoder base64 is not encryption Kubernetes API Version Checker Will this apply on that cluster Kubernetes Go Template Tester Go truthiness is not JavaScript's Kubernetes Cluster Cost Estimator At your rates, not a price table Kubernetes Kustomize Build Transformers run in kustomize's order Kubernetes Helm Chart Validator version is SemVer, appVersion is not

Elsewhere on the site