---
title: 'Migrate Ingress to Kubernetes Gateway API: 2026 Tutorial'
description: Step-by-step migration from ingress-nginx to Kubernetes Gateway API with ingress2gateway 1.0, HTTPRoute, canary splits, and a tested rollback in 2026.
url: https://nerdleveltech.com/migrate-ingress-to-kubernetes-gateway-api-tutorial
language: en
date: '2026-05-10'
category: cloud-devops
tags:
  - kubernetes
  - gateway api
  - ingress
  - httproute
  - envoy gateway
  - ingress2gateway
  - migration
  - cloud native
markdown_url: https://nerdleveltech.com/migrate-ingress-to-kubernetes-gateway-api-tutorial.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/migrate-ingress-to-kubernetes-gateway-api-tutorial'
terms: https://nerdleveltech.com/llms.txt
---

# Migrate Ingress to Kubernetes Gateway API: 2026 Tutorial

## TL;DR

**To migrate Ingress to Gateway API in 2026, you install the v1.5.1 Standard channel CRDs, run a conformant Gateway controller, and convert each existing Ingress with the `ingress2gateway` v1.0.0 CLI before cutting traffic over with weighted `HTTPRoute` rules.** This guide walks through that exact sequence. You will migrate a working `ingress-nginx` deployment to **Kubernetes Gateway API v1.5.1** end-to-end on a local **kind v0.31.0** cluster. The walkthrough installs the Standard channel CRDs, runs **Envoy Gateway v1.7.2** as the conformant controller, converts your existing `Ingress` YAML with **`ingress2gateway` v1.0.0**, lands the new `Gateway` and `HTTPRoute` resources, executes a **weighted canary split** between two app versions, and finishes with a verified rollback path. Every version is pinned, every command is copy-paste runnable, and the same pattern moves cleanly to GKE, EKS, AKS, or any conformant cluster.[^1][^2][^3]

This guide exists because `ingress-nginx` reached end-of-life in **March 2026** — best-effort maintenance stopped, with no further releases, bug fixes, or security patches for the project.[^4] The community ingress-nginx controller was a default ingress path for a large share of cloud-native clusters, and the official replacement path is Gateway API. Most existing tutorials predate `ingress2gateway` 1.0 (released March 20, 2026, with support for over 30 ingress-nginx annotations[^2]), and the official sigs.k8s.io guide is reference-style rather than copy-paste runnable. This post fills that gap.

## What you will learn

- How to **set up a local kind cluster** that mirrors a real production layout for ingress-to-gateway migration
- How to **install Gateway API v1.5.1 CRDs** from the Standard channel
- How to **install Envoy Gateway v1.7.2** as a conformant Gateway controller via Helm
- How to deploy a sample app behind **ingress-nginx** the way a typical 2024-era cluster does it
- How to use **`ingress2gateway` v1.0.0** to convert that Ingress into a `Gateway` + `HTTPRoute` automatically
- How to run **both controllers in parallel** with separate external IPs so the cutover is risk-free
- How to perform a **weighted canary deployment** with `HTTPRoute` `backendRefs` (no annotations required)
- How to **roll back** safely when the canary regresses, and how to clean up the legacy Ingress only after `Gateway` `Programmed=True`
- A **migration checklist** plus the five most common gotchas teams hit during real cutovers

## Prerequisites

| Tool | Version pinned in this tutorial | Why this version |
|---|---|---|
| Docker (or Podman with the docker shim) | 25.x or newer | Required by kind v0.31.0 |
| `kind` | v0.31.0 | Default node image is `kindest/node:v1.35.0` (Kubernetes 1.35), released December 18, 2025[^5][^6] |
| `kubectl` | matching v1.35.x | Match the cluster minor version |
| `helm` | v3.18.0 or newer (3.20.2 recommended) | Envoy Gateway Helm chart targets Helm 3.x; 3.20.2 (released April 9, 2026) was the latest 3.x at time of writing[^7] |
| `go` (only if installing `ingress2gateway` from source) | 1.22+ | Needed for `go install` |
| `ingress2gateway` | v1.0.0 | First release with broad ingress-nginx annotation coverage[^2] |
| Gateway API CRDs | v1.5.1 (Standard channel) | Latest patch as of writing; v1.5.0 cut on February 27, 2026; v1.5.1 patch on March 14, 2026[^1][^8] |
| Envoy Gateway | v1.7.2 | Released April 17, 2026; per Envoy Gateway's official compatibility matrix, v1.7.x is compiled and tested against Gateway API v1.4.1 — the Standard channel v1.5.1 CRDs used in this tutorial are a newer minor version than Envoy Gateway v1.7 officially targets. Gateway API is designed to be backward compatible, and this pairing works in practice, but if you want an Envoy Gateway release actually conformant against v1.5.1, use v1.8.x instead[^3][^18] |

You also need roughly 4 GB of free RAM for the kind cluster, the demo app, and Envoy Gateway, plus port 80/443 free on `localhost`.

## Step 1: Create a kind cluster with port mapping

Save this as `kind-config.yaml`. The `extraPortMappings` block lets the host reach `LoadBalancer` services without `cloud-provider-kind` so the tutorial stays self-contained.

```yaml
# kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
    image: kindest/node:v1.35.0
    kubeadmConfigPatches:
      - |
        kind: InitConfiguration
        nodeRegistration:
          kubeletExtraArgs:
            node-labels: "ingress-ready=true"
    extraPortMappings:
      - containerPort: 80
        hostPort: 8080
        protocol: TCP
      - containerPort: 443
        hostPort: 8443
        protocol: TCP
  - role: worker
    image: kindest/node:v1.35.0
```

The `ingress-ready=true` label on the control-plane node is mandatory: the kind-specific `ingress-nginx` manifest schedules the controller pod with a `nodeSelector` that requires this label, so without it the controller pod will sit `Pending` indefinitely.[^17]

Create the cluster:

```bash
kind create cluster --name gw-migration --config kind-config.yaml
kubectl cluster-info --context kind-gw-migration
```

Expected output ends with:

```
Kubernetes control plane is running at https://127.0.0.1:<port>
CoreDNS is running at https://127.0.0.1:<port>/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
```

If `kind create cluster` hangs at "Creating control plane", make sure Docker has at least 4 CPUs and 6 GB of RAM in its resource settings — the default 2 GB is not enough for a two-node cluster plus the workloads we add later.

## Step 2: Deploy the legacy ingress-nginx setup

This is the "before" state — what your existing cluster probably looks like. We use the deprecated controller intentionally so the migration step has something real to convert.

```bash
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.13.7/deploy/static/provider/kind/deploy.yaml

kubectl wait --namespace ingress-nginx \
  --for=condition=ready pod \
  --selector=app.kubernetes.io/component=controller \
  --timeout=180s
```

Now deploy a sample app — two versions of an HTTP "echo" so we can exercise canary splitting later:

```yaml
# echo-app.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo-v1
  labels: { app: echo, version: v1 }
spec:
  replicas: 2
  selector:
    matchLabels: { app: echo, version: v1 }
  template:
    metadata:
      labels: { app: echo, version: v1 }
    spec:
      containers:
        - name: echo
          image: ghcr.io/mendhak/http-https-echo:40
          ports: [{ containerPort: 8080 }]
          env:
            - name: HTTP_PORT
              value: "8080"
            - name: ECHO_INCLUDE_ENV_VARS
              value: "1"
            - name: VERSION
              value: "v1"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo-v2
  labels: { app: echo, version: v2 }
spec:
  replicas: 2
  selector:
    matchLabels: { app: echo, version: v2 }
  template:
    metadata:
      labels: { app: echo, version: v2 }
    spec:
      containers:
        - name: echo
          image: ghcr.io/mendhak/http-https-echo:40
          ports: [{ containerPort: 8080 }]
          env:
            - name: HTTP_PORT
              value: "8080"
            - name: ECHO_INCLUDE_ENV_VARS
              value: "1"
            - name: VERSION
              value: "v2"
---
apiVersion: v1
kind: Service
metadata: { name: echo-v1 }
spec:
  selector: { app: echo, version: v1 }
  ports: [{ port: 80, targetPort: 8080 }]
---
apiVersion: v1
kind: Service
metadata: { name: echo-v2 }
spec:
  selector: { app: echo, version: v2 }
  ports: [{ port: 80, targetPort: 8080 }]
```

```bash
kubectl apply -f echo-app.yaml
```

And the "before" Ingress that the migration will convert. Note the ingress-nginx-specific annotations — these are exactly the kind of cruft that motivates the move to Gateway API:

```yaml
# legacy-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: echo-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/enable-cors: "true"
    nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, OPTIONS"
spec:
  ingressClassName: nginx
  rules:
    - host: echo.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: echo-v1
                port:
                  number: 80
```

```bash
kubectl apply -f legacy-ingress.yaml
```

Verify it works:

```bash
curl -H "Host: echo.local" http://localhost:8080/  | head -20
```

You should see a JSON dump that includes `"VERSION": "v1"`. If you instead see `default backend - 404`, give the ingress controller another 30 seconds — `extraPortMappings` only routes the request once the LoadBalancer service has assigned the port to the controller pod.

## Step 3: Install Gateway API v1.5.1 CRDs

The Gateway API ships its own CRDs separately from any controller. Install the **Standard channel** (stable, GA APIs):[^1][^9]

```bash
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.5.1/standard-install.yaml
```

Confirm:

```bash
kubectl get crd | grep gateway.networking.k8s.io
```

Expected output (in any order):

```
backendtlspolicies.gateway.networking.k8s.io   2026-05-10T12:00:00Z
gatewayclasses.gateway.networking.k8s.io       2026-05-10T12:00:00Z
gateways.gateway.networking.k8s.io             2026-05-10T12:00:00Z
grpcroutes.gateway.networking.k8s.io           2026-05-10T12:00:00Z
httproutes.gateway.networking.k8s.io           2026-05-10T12:00:00Z
referencegrants.gateway.networking.k8s.io      2026-05-10T12:00:00Z
tlsroutes.gateway.networking.k8s.io            2026-05-10T12:00:00Z
```

`TLSRoute` is the new arrival in v1.5 — it was promoted from Experimental to Standard alongside `ListenerSet`, the `HTTPRoute` CORS filter, Client Certificate Validation, and Certificate Selection for Gateway TLS Origination, with `ReferenceGrant` also moving to v1 in this release.[^10] `BackendTLSPolicy` graduated earlier (v1.4). If you only see six CRDs, you installed an older 1.4 manifest.

If you also need the experimental APIs (e.g., `XBackendTrafficPolicy`), `kubectl apply` will reject the manifest because the experimental CRDs exceed the standard request size — use `kubectl apply --server-side=true` instead, per the same "CEL rule-cost-budget" error class documented in earlier Gateway API CRD installs.[^11] We do not need experimental APIs for this tutorial.

## Step 4: Install Envoy Gateway v1.7.2 as the controller

Gateway API is a CRD specification; behavior depends on the controller. Envoy Gateway is one of several conformant choices (NGINX Gateway Fabric, Cilium, Istio, Kong, Traefik). It is vendor-neutral, ships an OCI Helm chart, and has a small moving-parts footprint for a tutorial.[^3][^12]

Install the control plane with **`--skip-crds`** because we already installed the v1.5.1 Standard channel CRDs in Step 3. The default Envoy Gateway Helm chart still bundles a copy of the Gateway API Experimental channel CRDs (a transition that is in progress upstream), so omitting `--skip-crds` would overwrite the Standard CRDs you just applied:[^16]

```bash
helm install eg oci://docker.io/envoyproxy/gateway-helm \
  --version v1.7.2 \
  -n envoy-gateway-system \
  --create-namespace \
  --skip-crds

kubectl wait --timeout=5m \
  -n envoy-gateway-system \
  deployment/envoy-gateway \
  --for=condition=Available
```

The Helm chart does not create a `GatewayClass` for you — that is left to the user so the same controller can be reused under different class names. Apply one explicitly:

```yaml
# gatewayclass.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: eg
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller
```

```bash
kubectl apply -f gatewayclass.yaml
kubectl get gatewayclass
```

Expected:

```
NAME   CONTROLLER                                      ACCEPTED   AGE
eg     gateway.envoyproxy.io/gatewayclass-controller   True       10s
```

Note: you now have **two** ingress paths in the cluster:

- The legacy `ingress-nginx` controller bound to `IngressClass: nginx`, fronting `echo-v1` via the existing `Ingress`.
- The new `GatewayClass: eg` waiting for a `Gateway` and `HTTPRoute` to reference it.

Each gets its own external endpoint. This is the safety pattern that the official migration guide explicitly recommends — run both, validate the new path with isolated traffic, then cut over.[^13]

## Step 5: Convert the Ingress with ingress2gateway 1.0

Install the converter. Either method works:

```bash
# Option A — Go install (requires Go 1.22+)
go install github.com/kubernetes-sigs/ingress2gateway@v1.0.0

# Option B — Homebrew (macOS / Linuxbrew)
brew install ingress2gateway

ingress2gateway --version
```

Convert the live Ingress into Gateway API resources. The tool reads from your kubeconfig context and translates ingress-nginx annotations using its 30+ supported mappings (CORS, rewrite-target, regex matching, backend TLS, and more):[^2]

```bash
ingress2gateway print \
  --providers=ingress-nginx \
  --namespace=default \
  > gwapi.yaml
```

Open `gwapi.yaml` and you will see something like this (cleaned up for readability):

```yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: echo-ingress
  namespace: default
spec:
  gatewayClassName: nginx          # <-- you must change this
  listeners:
    - name: http
      port: 80
      protocol: HTTP
      hostname: echo.local
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: echo-ingress
  namespace: default
spec:
  parentRefs:
    - name: echo-ingress
  hostnames:
    - echo.local
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      filters:
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplaceFullPath
              replaceFullPath: /
        - type: CORS
          cors:
            allowMethods: ["GET", "POST", "OPTIONS"]
      backendRefs:
        - name: echo-v1
          port: 80
          weight: 100
```

Two things to know about the output:

1. **`gatewayClassName: nginx` is wrong** — the converter copies `ingressClassName` verbatim. You must change it to `eg` (or whichever GatewayClass your controller installed) before applying. This is one of the easiest mistakes to make during real migrations because the conversion looks complete.[^2]
2. The `URLRewrite` filter and `CORS` filter come directly from the original ingress-nginx annotations. The CORS filter graduated to Standard in Gateway API v1.5, so this is now stable across conformant controllers.[^10]

Edit the file and apply:

```bash
sed -i.bak 's/gatewayClassName: nginx/gatewayClassName: eg/' gwapi.yaml
kubectl apply -f gwapi.yaml
```

Wait for the Gateway to become `Programmed=True` — this is the gate that tells you the controller has actually wired Envoy to listen on the requested port. Do **not** delete the legacy Ingress until this condition is `True`:[^13]

```bash
kubectl wait --timeout=2m \
  gateway/echo-ingress \
  --for=condition=Programmed
```

Expected:

```
gateway.gateway.networking.k8s.io/echo-ingress condition met
```

## Step 6: Test the new Gateway path in parallel with the old Ingress

The Envoy Gateway service is a `LoadBalancer` in `envoy-gateway-system`. Find its assigned port on the host (kind sets up `NodePort` ranges, and the controller's service is reachable inside the cluster):

```bash
kubectl get svc -n envoy-gateway-system
```

The service named `envoy-default-echo-ingress-...` is the data plane. We will hit it directly with `kubectl port-forward` so we don't need a kind LoadBalancer integration:

```bash
GW_SVC=$(kubectl get svc -n envoy-gateway-system \
  -l gateway.envoyproxy.io/owning-gateway-name=echo-ingress \
  -o jsonpath='{.items[0].metadata.name}')

kubectl port-forward -n envoy-gateway-system "svc/$GW_SVC" 9080:80 &
```

Test the new Gateway path:

```bash
curl -H "Host: echo.local" http://localhost:9080/ | grep VERSION
```

Expected:

```
"VERSION": "v1",
```

Now hit the old Ingress simultaneously — different port, same host header:

```bash
curl -H "Host: echo.local" http://localhost:8080/ | grep VERSION
```

Both return `"VERSION": "v1"`. You now have two independent ingress paths into the same backend, with separate external endpoints. This is the moment to point a small percentage of real traffic (DNS round-robin, an external load balancer, or an internal canary header) at the new path and watch metrics for an hour or a day before continuing.[^13]

## Step 7: Add a weighted canary split with HTTPRoute

Here is where Gateway API earns its keep. With ingress-nginx you would need either a separate `Ingress` per version plus the `nginx.ingress.kubernetes.io/canary-weight` annotation, or a service mesh. With Gateway API, the `HTTPRoute` `backendRefs` field accepts weights natively as a first-class API.[^14]

Edit `gwapi.yaml` and replace the single backendRef with a weighted split:

```yaml
# gwapi.yaml — HTTPRoute section only
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: echo-ingress
  namespace: default
spec:
  parentRefs:
    - name: echo-ingress
  hostnames:
    - echo.local
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      filters:
        - type: CORS
          cors:
            allowMethods: ["GET", "POST", "OPTIONS"]
      backendRefs:
        - name: echo-v1
          port: 80
          weight: 90
        - name: echo-v2
          port: 80
          weight: 10
```

Apply and verify the split:

```bash
kubectl apply -f gwapi.yaml

for i in $(seq 1 100); do
  curl -s -H "Host: echo.local" http://localhost:9080/ \
    | grep -oE '"VERSION": *"v[12]"'
done | sort | uniq -c
```

Expected (approximate — small samples have noise):

```
  90 "VERSION": "v1"
  10 "VERSION": "v2"
```

The split is enforced by Envoy on every request. There is no annotation, no second Ingress, no service mesh sidecar — just a single `HTTPRoute` whose `backendRefs` weights add up to whatever you want them to. Bumping `v2` to 50/50, then 10/90, then a final cut to 100/0 is just three `kubectl apply` operations against the same file.

For automated promotion, **Flagger** integrates natively with Gateway API HTTPRoute resources and works with any conformant controller.[^15] That is out of scope for this migration walkthrough but is the correct next step once the manual cutover succeeds.

## Step 8: Verify and roll back

A canary that regresses needs a single command to undo. Because the migration kept the original Ingress in place, your rollback is a two-step retreat:

```bash
# Step 1 — flip the HTTPRoute back to v1 only
kubectl patch httproute echo-ingress \
  --type='merge' \
  -p '{"spec":{"rules":[{"matches":[{"path":{"type":"PathPrefix","value":"/"}}],"backendRefs":[{"name":"echo-v1","port":80,"weight":100}]}]}}'

# Step 2 — if the Gateway itself is misbehaving, point external traffic
# back at the legacy ingress endpoint (port 8080 in this kind setup,
# or the ingress-nginx LoadBalancer in production)
```

The "old Ingress is still there" is the entire reason for the parallel-deployment pattern. **Do not delete the legacy Ingress until you have a full traffic shift, the Gateway is `Programmed=True`, and you have monitored production metrics for at least one full traffic cycle (typically 24 hours).** The official migration guide explicitly calls this out: delete Ingress resources iteratively, one at a time, verifying after each.[^13]

## Step 9: Decommission ingress-nginx

Once the new Gateway has carried 100% of production traffic for a representative window, clean up:

```bash
# 1. Delete the legacy Ingress
kubectl delete ingress echo-ingress

# 2. Confirm no other Ingress objects remain in the cluster
kubectl get ingress -A

# 3. Once that returns "No resources found", uninstall ingress-nginx
kubectl delete -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.13.7/deploy/static/provider/kind/deploy.yaml
```

If `kubectl get ingress -A` still shows objects in other namespaces, **stop**. Repeat the migration for each one before removing the controller — `ingress-nginx` failing while a single legacy Ingress points at it will black-hole that traffic.

## Migration checklist

Use this checklist every time you migrate Ingress to Gateway API in production, not just on kind:

| Step | Command / action | Done? |
|---|---|---|
| 1 | Inventory all `Ingress` resources: `kubectl get ingress -A -o yaml > all-ingresses.yaml` | |
| 2 | Audit ingress-nginx annotations: `grep nginx.ingress.kubernetes.io all-ingresses.yaml \| sort -u` | |
| 3 | Confirm every annotation is on the ingress2gateway 1.0 supported list, or has a manual translation plan[^2] | |
| 4 | Install Gateway API v1.5.1 Standard channel CRDs[^1] | |
| 5 | Install a conformant controller (Envoy Gateway, NGINX Gateway Fabric, Cilium, Istio, etc.) **alongside** ingress-nginx | |
| 6 | Run `ingress2gateway print --all-namespaces --providers=ingress-nginx > gwapi.yaml`[^2] | |
| 7 | Manually fix `gatewayClassName` (always wrong by default) | |
| 8 | Apply, then `kubectl wait --for=condition=Programmed gateway/<name>`[^13] | |
| 9 | Send canary traffic via DNS, an external LB, or a header rule | |
| 10 | Monitor for at least one full traffic cycle | |
| 11 | Cut over the remaining traffic with `HTTPRoute` weight changes | |
| 12 | Delete each `Ingress` one at a time; verify availability between deletions[^13] | |
| 13 | Uninstall ingress-nginx **only** after `kubectl get ingress -A` returns nothing | |

## Troubleshooting: real errors from the GitHub issue tracker

**Error: `Forbidden: estimated rule cost exceeds budget`** when applying CRDs.
This is the symptom of trying to install the **experimental** channel manifest with a regular `kubectl apply`. The experimental CRDs ship more validation rules than the default request budget allows. Either install the **standard** channel (this tutorial's path) or use `kubectl apply --server-side=true`.[^11]

**Error: HTTPRoute is `Accepted=True` but `ResolvedRefs=False`.**
The `Service` referenced in `backendRefs` does not exist or is in a different namespace without a `ReferenceGrant`. Run `kubectl describe httproute <name>` and read the `ResolvedRefs` condition message. The two common causes are typos in the `Service` name and cross-namespace refs that need an explicit `ReferenceGrant` (the resource that authorizes cross-namespace references — graduated to v1 / GA in Gateway API v1.5).[^10]

**Error: Gateway is `Accepted=True` but never reaches `Programmed=True`.**
The controller can't allocate the requested listener — usually a port conflict on the data-plane LoadBalancer service or a missing TLS secret for an HTTPS listener. Check `kubectl get svc -n envoy-gateway-system` for stuck `<pending>` external IPs, and `kubectl logs -n envoy-gateway-system deployment/envoy-gateway` for the controller's view.

**Error: `gatewayClassName: nginx` not found** after `ingress2gateway` conversion.
You forgot Step 5's `sed`. The converter copies the source `ingressClassName` literally because it cannot know which Gateway controller you actually run. Open `gwapi.yaml`, change to the GatewayClass name from `kubectl get gatewayclass`, re-apply.

**Warning: `weight` field rounds requests unevenly at low sample sizes.**
This is expected — weights are probabilistic per-request, not a strict round-robin. With weights `90/10`, ten requests will not deterministically produce nine v1 + one v2; you might see ten v1 in a row. Sample size matters; over 1,000 requests the ratio converges. If you need deterministic routing, use header-matched rules instead of weights.[^14]

## Next steps

Now that the migration runs end-to-end on kind, the production checklist is:

- Wire **Flagger or Argo Rollouts** to automate canary promotion against the same `HTTPRoute`.[^15]
- Add **observability** to the data plane: Envoy Gateway exposes Prometheus metrics out of the box; the v1.5 stable resources (`BackendTLSPolicy`, `Client Certificate Validation`) extend the same model to mTLS between Gateway and pods.[^10]
- Read the deeper API gateway design context in our [API gateway patterns guide](https://nerdleveltech.com/mastering-api-gateway-patterns-architecture-security-scale) before promoting beyond a single cluster.
- For teams still onboarding to Kubernetes itself, our [Kubernetes hands-on guide](https://nerdleveltech.com/kubernetes-explained-a-2026-hands-on-guide) covers the cluster-level concepts that Gateway API builds on.
- If you came from a Docker-first workflow, the [Docker vs Kubernetes overview](https://nerdleveltech.com/docker-vs-kubernetes-the-real-world-guide-to-containers-and-orchestration) explains where this controller layer sits in the stack.

Teams who migrate Ingress to Gateway API now will be ahead of the curve when the last `ingress-nginx` security fix expires; teams who wait will be patching a dead controller. Gateway API is no longer the future of Kubernetes networking — as of 2026 it is the present. ingress-nginx is retired, the conformance ecosystem is mature, and `ingress2gateway` 1.0 makes the conversion mechanical for the common case. The remaining work is the careful operational dance: parallel deployment, gradual cutover, and one Ingress deletion at a time.

[^1]: Kubernetes SIG Network, "Gateway API v1.5: Moving features to Stable", April 21, 2026 (post edited to correct the v1.5.0 release date to February 27, 2026). https://kubernetes.io/blog/2026/04/21/gateway-api-v1-5/
[^2]: Kubernetes SIG Network, "Announcing Ingress2Gateway 1.0: Your Path to Gateway API", March 20, 2026. https://kubernetes.io/blog/2026/03/20/ingress2gateway-1-0-release/
[^3]: Envoy Gateway releases (v1.7.2 published April 17, 2026). https://github.com/envoyproxy/gateway/releases
[^4]: Kubernetes SIG Network, "Ingress NGINX Retirement: What You Need to Know", November 11, 2025. https://kubernetes.io/blog/2025/11/11/ingress-nginx-retirement/
[^5]: kind v0.31.0 release page (default node image `kindest/node:v1.35.0`). https://github.com/kubernetes-sigs/kind/releases/tag/v0.31.0
[^6]: kind Quick Start. https://kind.sigs.k8s.io/docs/user/quick-start/
[^7]: Helm releases (v3.20.2, April 9, 2026). https://github.com/helm/helm/releases
[^8]: Gateway API v1.5.1 release tag. https://github.com/kubernetes-sigs/gateway-api/releases/tag/v1.5.1
[^9]: Gateway API getting started — installation. https://gateway-api.sigs.k8s.io/guides/getting-started/
[^10]: Gateway API v1.5 stable promotions: ListenerSet, TLSRoute, HTTPRoute CORS Filter, Client Certificate Validation, Certificate Selection for Gateway TLS Origination, and ReferenceGrant moving to v1. https://kubernetes.io/blog/2026/04/21/gateway-api-v1-5/
[^11]: Gateway API issue #3256 — "estimated rule cost exceeds budget" with non-server-side apply. https://github.com/kubernetes-sigs/gateway-api/issues/3256
[^12]: Gateway API implementations list. https://gateway-api.sigs.k8s.io/implementations/
[^13]: Gateway API migration guide — "Migrating from Ingress-NGINX". https://gateway-api.sigs.k8s.io/guides/getting-started/migrating-from-ingress-nginx/
[^14]: Gateway API HTTP traffic splitting guide — weighted backendRefs. https://gateway-api.sigs.k8s.io/guides/traffic-splitting/
[^15]: "How to Configure Flagger with Gateway API HTTPRoute", March 13, 2026. https://oneuptime.com/blog/post/2026-03-13-how-to-configure-flagger-with-gateway-api-httproute/view
[^16]: Envoy Gateway issue #7238 — "Move away from defaulting from the Gateway API Experimental Channel to the Standard Channel for CRDs". https://github.com/envoyproxy/gateway/issues/7238
[^17]: kind ingress installation guide — `ingress-ready=true` label is required by the kind-specific ingress-nginx manifest. https://kind.sigs.k8s.io/docs/user/ingress/
[^18]: Envoy Gateway release compatibility matrix — v1.7 is compiled against Gateway API v1.4.1; v1.8 and later target v1.5.1. https://gateway.envoyproxy.io/news/releases/matrix/
