Migrate from Kubernetes Ingress to Gateway API on a K3s Cluster

Updated on Sep 2, 2026
Mila H
11 MINS READ
Table of Contents
Migrate Kubernetes Ingress to Gateway API v1.6 on K3s

If you run K3s in production, you may use Traefik Ingress for web traffic. It works well for HTTP, but the Gateway API offers clearer routing rules, better TLS handling, and more stable TCP and UDP routes. This guide shows how to migrate Kubernetes Ingress to Gateway API v1.6 on K3s with Cilium.

Why You Should Migrate Kubernetes Ingress to Gateway API

The classic Ingress resource has served Kubernetes well, but it was not built for multi-protocol or multi-team clusters. Controllers often need their own annotations for TLS redirects, headers, and path rules. That makes it harder to move between Traefik, NGINX, and cloud controllers.

Gateway API separates the work into clear resources:

  • GatewayClass selects the controller that runs a Gateway.

  • Gateway opens ports and sets protocols and TLS.

  • HTTPRoute, TCPRoute, UDPRoute, TLSRoute, and GRPCRoute define routing rules.

  • ReferenceGrant controls safe cross-namespace access to Services and Secrets.

In Gateway API v1.6, TCPRoute and UDPRoute became stable v1 resources. The old v1alpha2 versions are deprecated. If you have old test manifests, convert them to v1 during this migration.

Architecture for This Migration

This guide starts with a standard K3s Ingress setup and ends with Gateway API resources that you can test next to the old Ingress.

Before: Traefik Ingress routes demo.example.com to a Kubernetes Service. It uses a TLS Secret, an HTTP-to-HTTPS redirect annotation, and a header-related Traefik middleware annotation.

After: Cilium runs the Gateway API controller. An HTTP Gateway and HTTPRoute handle web traffic, HTTPS, redirects, and a response header. A separate TCP Gateway exposes a TCP Service, and a separate UDP Gateway exposes a UDP Service.

Cilium is used because it supports Gateway API v1.6.1, including HTTPRoute, TCPRoute, and UDPRoute. K3s includes Traefik, but the bundled Traefik Gateway API support may not provide the stable v1.6 TCP and UDP route support used in this guide.

If you're hosting this yourself instead of using a managed cloud cluster, a high-performance dedicated server gives you steady CPU, network, and system-level access that tools like Cilium need to run Gateway API properly.

Prerequisites

Before you migrate Kubernetes Ingress to Gateway API, confirm the following:

  • A K3s cluster that already runs Cilium, or a new K3s cluster where Cilium will be the CNI.

  • kubectl, helm, curl, and tar on your admin machine.

  • A working app Service named demo-web in the default namespace, listening on port 80.

  • A TLS Secret named demo-web-tls in the default namespace. It must include a valid certificate for demo.example.com.

  • A LoadBalancer solution. For self-hosted K3s, use Cilium LB IPAM, MetalLB, or another supported option. The Gateway needs an external address before public DNS can point to it.

  • A TCP Service named demo-postgres on port 5432, if you want to use the TCPRoute example.

  • A UDP Service named demo-coredns on port 5353, if you want to use the UDPRoute example.

Before you migrate Kubernetes Ingress to Gateway API, check your current app, Service, endpoints, and TLS Secret:

Bash
kubectl get deploy,svc -n defaultkubectl get endpointslices -n default -l kubernetes.io/service-name=demo-webkubectl get secret demo-web-tls -n default

Step 1: Save Your Current Ingress Setup

Before you change anything, save a copy of your current setup so you can compare it later. This backup step is what makes it safe to migrate Kubernetes Ingress to Gateway API without breaking live traffic:

Bash
kubectl get ingress -A -o yaml > ingress-backup.yamlkubectl get svc,deploy -n default -l app=demo-web -o yaml > demo-web-backup.yaml

Here is a typical old Ingress file. Create it for reference:

Bash
nano 01-demo-web-ingress.yaml

Add:

YAML
apiVersion: networking.k8s.io/v1kind: Ingressmetadata:  name: demo-web-ingress  namespace: default  annotations:    traefik.ingress.kubernetes.io/redirect-entry-point: https    traefik.ingress.kubernetes.io/router.middlewares: default-add-headers@kubernetescrdspec:  ingressClassName: traefik  tls:  - hosts:    - demo.example.com    secretName: demo-web-tls  rules:  - host: demo.example.com    http:      paths:      - path: /        pathType: Prefix        backend:          service:            name: demo-web            port:              number: 80

Do not delete the old Ingress yet. The Gateway API resources will run beside it while you test the migration.

Step 2: Install Gateway API CRDs

Gateway API uses new Kubernetes resource types such as Gateway, HTTPRoute, TCPRoute, and UDPRoute. Kubernetes cannot use these resources until their CRDs are installed.

Create the install script:

Bash
nano 02-gateway-api-crds.sh

Paste this content:

Bash
#!/usr/bin/env bashset -euo pipefail VERSION=v1.6.1BASE=https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/${VERSION}/config/crd/standard kubectl apply --server-side -f ${BASE}/gateway.networking.k8s.io_gatewayclasses.yamlkubectl apply --server-side -f ${BASE}/gateway.networking.k8s.io_gateways.yamlkubectl apply --server-side -f ${BASE}/gateway.networking.k8s.io_httproutes.yamlkubectl apply --server-side -f ${BASE}/gateway.networking.k8s.io_grpcroutes.yamlkubectl apply --server-side -f ${BASE}/gateway.networking.k8s.io_tlsroutes.yamlkubectl apply --server-side -f ${BASE}/gateway.networking.k8s.io_referencegrants.yamlkubectl apply --server-side -f ${BASE}/gateway.networking.k8s.io_backendtlspolicies.yaml # Gateway API v1.6 Standard APIs used in this guide.kubectl apply --server-side -f ${BASE}/gateway.networking.k8s.io_tcproutes.yamlkubectl apply --server-side -f ${BASE}/gateway.networking.k8s.io_udproutes.yaml

Make the script executable and run it:

Bash
chmod +x 02-gateway-api-crds.sh./02-gateway-api-crds.sh

Verify the installation:

Bash
kubectl get crd | grep gateway.networking.k8s.io

You should see at least gatewayclasses, gateways, httproutes, tcproutes, udproutes, and referencegrants.

Step 3: Enable Gateway API in Cilium

Cilium is the Gateway API controller in this guide. It reads Gateway API objects and creates the Envoy proxy setup that routes traffic.

This step is only for a cluster that already uses Cilium. Do not run it as a fast way to change a live Flannel-based K3s cluster into a Cilium cluster. Replacing a CNI changes all Pod networking and needs a proper migration plan.

Add the Cilium Helm repository if you do not already have it:

Bash
helm repo add cilium https://helm.cilium.io/helm repo update

Check the current Cilium installation:

Bash
helm list -n kube-systemkubectl -n kube-system get daemonset cilium

Enable Gateway API in an existing Cilium installation:

Bash
helm upgrade cilium cilium/cilium \  --namespace kube-system \  --version 1.20.1 \  --reuse-values \  --set kubeProxyReplacement=true \  --set gatewayAPI.enabled=true

Restart Cilium so it reloads the Gateway API feature:

Bash
kubectl -n kube-system rollout restart deployment/cilium-operatorkubectl -n kube-system rollout restart daemonset/cilium kubectl -n kube-system rollout status deployment/cilium-operator --timeout=10mkubectl -n kube-system rollout status daemonset/cilium --timeout=10m

Install the Cilium CLI if needed:

Bash
CILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)CLI_ARCH=amd64 if [ "$(uname -m)" = "aarch64" ]; then  CLI_ARCH=arm64fi curl -L --fail --remote-name-all \  https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-${CLI_ARCH}.tar.gz{,.sha256sum} sha256sum --check cilium-linux-${CLI_ARCH}.tar.gz.sha256sumsudo tar xzvfC cilium-linux-${CLI_ARCH}.tar.gz /usr/local/binrm cilium-linux-${CLI_ARCH}.tar.gz{,.sha256sum}

Check Cilium health and confirm that Cilium created the GatewayClass:

Bash
cilium status --waitkubectl get gatewayclass cilium

The GatewayClass named cilium tells Kubernetes that Cilium owns Gateways that use gatewayClassName: cilium.

If the GatewayClass is missing, check Cilium operator logs:

Bash
kubectl logs -n kube-system deployment/cilium-operator --tail=200 | grep -i gateway

For Cilium basics, policies, and traffic visibility after the migration, see our Cilium Hubble Kubernetes network policy guide.

Step 4: Create the HTTP and HTTPS Gateway

A Gateway opens ports and defines how traffic enters the cluster. In this step, it listens on port 80 for HTTP and port 443 for HTTPS.

Create the file:

Bash
nano 03-demo-web-gateway.yaml

Add:

YAML
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata:  name: demo-web-gateway  namespace: defaultspec:  gatewayClassName: cilium  listeners:  - name: http    protocol: HTTP    port: 80    allowedRoutes:      namespaces:        from: Same  - name: https    protocol: HTTPS    port: 443    tls:      mode: Terminate      certificateRefs:      - kind: Secret        name: demo-web-tls    allowedRoutes:      namespaces:        from: Same

Apply it:

Bash
kubectl apply -f 03-demo-web-gateway.yaml

Check its status:

Bash
kubectl get gateway demo-web-gateway -n defaultkubectl describe gateway demo-web-gateway -n default

The HTTP listener receives normal traffic on port 80. The HTTPS listener receives encrypted traffic on port 443 and uses the demo-web-tls Secret to terminate TLS.

Wait until the Gateway shows Accepted=True and Programmed=True. It also needs an external ADDRESS before you can point public DNS to it. If no address appears, configure Cilium LB IPAM, MetalLB, or another LoadBalancer option first.

Step 5: Create HTTP Redirect and HTTPS Routes

An HTTPRoute holds the routing rules. This step creates two routes: one redirects HTTP to HTTPS, and the other sends HTTPS requests to the existing demo-web Service.

Create the file:

Bash
nano 04-demo-web-routes.yaml

Add:

YAML
# Redirect HTTP traffic to HTTPS.apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata:  name: demo-web-http-redirect  namespace: defaultspec:  parentRefs:  - name: demo-web-gateway    sectionName: http  hostnames:  - demo.example.com  rules:  - filters:    - type: RequestRedirect      requestRedirect:        scheme: https        statusCode: 301---# Send HTTPS traffic to the existing Service.apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata:  name: demo-web-https  namespace: defaultspec:  parentRefs:  - name: demo-web-gateway    sectionName: https  hostnames:  - demo.example.com  rules:  - matches:    - path:        type: PathPrefix        value: /    filters:    - type: ResponseHeaderModifier      responseHeaderModifier:        add:        - name: X-App-Version          value: v2    backendRefs:    - name: demo-web      port: 80

Apply the file:

Bash
kubectl apply -f 04-demo-web-routes.yaml

Check both routes:

Bash
kubectl get httproute -n defaultkubectl describe httproute demo-web-http-redirect -n defaultkubectl describe httproute demo-web-https -n default

The first route replaces the Traefik HTTP-to-HTTPS redirect annotation. The second route sends https://demo.example.com/ traffic to demo-web on port 80.

The X-App-Version: v2 header is just a test. It helps prove that Gateway API filters are working. Do not manually add X-Forwarded-Proto unless your application truly needs a custom value; Cilium Envoy normally handles forwarding headers.

Step 6: Create a TCP Gateway

Ingress cannot route raw TCP services. Gateway API v1.6 adds stable TCPRoute support for PostgreSQL, Redis, MQTT, custom TCP applications, and similar services.

Use a separate Gateway for TCP. This keeps Layer 4 traffic separate from HTTP and HTTPS traffic and avoids controller-specific limits.

Security warning: Do not expose a database to the public internet unless it is required. Prefer private networking, VPN access, strict firewall rules, TLS, and strong database authentication.

Create the TCP Gateway file:

Bash
nano 05-demo-tcp-gateway.yaml

Add:

YAML
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata:  name: demo-tcp-gateway  namespace: defaultspec:  gatewayClassName: cilium  listeners:  - name: postgres    protocol: TCP    port: 5432    allowedRoutes:      namespaces:        from: Same      kinds:      - kind: TCPRoute

Apply:

Bash
kubectl apply -f 05-demo-tcp-gateway.yaml

Check with:

Bash
kubectl get gateway demo-tcp-gateway -n defaultkubectl describe gateway demo-tcp-gateway -n default

This Gateway opens TCP port 5432. It accepts only TCPRoute objects from the same namespace.

Step 7: Create the TCPRoute

The TCPRoute forwards connections from the TCP Gateway to a Kubernetes Service. This example routes PostgreSQL-style traffic to a Service named demo-postgres.

Create the file:

Bash
nano 06-demo-postgres-tcproute.yaml

Add:

YAML
apiVersion: gateway.networking.k8s.io/v1kind: TCPRoutemetadata:  name: demo-postgres  namespace: defaultspec:  parentRefs:  - name: demo-tcp-gateway    sectionName: postgres  rules:  - backendRefs:    - name: demo-postgres      port: 5432

Apply:

Bash
kubectl apply -f 06-demo-postgres-tcproute.yaml

Verify the route:

Bash
kubectl get tcproute -n defaultkubectl describe tcproute demo-postgres -n default

The parentRefs block attaches the route to the postgres listener on demo-tcp-gateway. The backendRefs block points to the Kubernetes Service that receives the connections.

The demo-postgres Service must already exist and expose TCP port 5432. Change its name and port to match your real Service.

Step 8: Create a UDP Gateway

UDPRoute is useful for UDP services such as DNS, game servers, VoIP, metrics agents, or IoT applications. Gateway API v1.6 makes UDPRoute stable.

Create the UDP Gateway file:

Bash
nano 07-demo-udp-gateway.yaml

Add:

YAML
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata:  name: demo-udp-gateway  namespace: defaultspec:  gatewayClassName: cilium  listeners:  - name: udp-service    protocol: UDP    port: 5353    allowedRoutes:      namespaces:        from: Same      kinds:      - kind: UDPRoute

Apply it:

Bash
kubectl apply -f 07-demo-udp-gateway.yaml

Verify with:

Bash
kubectl get gateway demo-udp-gateway -n defaultkubectl describe gateway demo-udp-gateway -n default

This Gateway accepts UDP traffic on port 5353. Change this port to the port used by your own UDP service.

Step 9: Create the UDPRoute

The UDPRoute forwards UDP packets from the UDP Gateway to a Kubernetes Service.

Create the file:

Bash
nano 08-demo-dns-udproute.yaml

Add:

YAML
apiVersion: gateway.networking.k8s.io/v1kind: UDPRoutemetadata:  name: demo-dns  namespace: defaultspec:  parentRefs:  - name: demo-udp-gateway    sectionName: udp-service  rules:  - backendRefs:    - name: demo-coredns      port: 5353

Apply and verify:

Bash
kubectl apply -f 08-demo-dns-udproute.yamlkubectl get udproute -n defaultkubectl describe udproute demo-dns -n default

This sends UDP traffic from port 5353 to the demo-coredns Service. Replace demo-coredns and port 5353 with the name and port of your own UDP Service.

Never expose the default CoreDNS Service used by your cluster to the public internet.

Step 10: Test the Gateway Before Changing DNS

Test the new Gateway before changing public DNS. This lets you leave the old Traefik Ingress active while you check the new path.

Get the HTTP Gateway address:

Bash
GW_IP=$(kubectl get gateway demo-web-gateway -n default \  -o jsonpath='{.status.addresses[0].value}') echo "$GW_IP"

Test the HTTP redirect without changing DNS:

Bash
curl -i --resolve demo.example.com:80:${GW_IP} \  http://demo.example.com/

You should receive a 301 response and a Location: https://demo.example.com/ header.

Test HTTPS, the certificate, the backend app, and the response header:

Bash
curl -ik --resolve demo.example.com:443:${GW_IP} \  https://demo.example.com/

Check for:

  • A successful TLS connection. Remove -k once the certificate is trusted.

  • A 200, 301, or another expected app response.

  • The X-App-Version: v2 response header.

Check all Gateway API resources once more:

Bash
kubectl get gateway,httproute,tcproute,udproute -n defaultkubectl describe gateway demo-web-gateway -n default

Only after the tests pass should you update the public DNS A or AAAA record for demo.example.com to the address of demo-web-gateway.

Step 11: Remove the Old Ingress

Do not remove the old Ingress immediately after you change DNS. First, monitor application logs, access logs, error rates, TLS errors, and normal user traffic.

When the new Gateway API route is stable, delete the old Ingress:

Bash
kubectl delete ingress demo-web-ingress -n default

Check whether other workloads still need Traefik:

Bash
kubectl get ingress -Akubectl get ingressclass

Only remove Traefik from K3s if no other application depends on it.

Conclusion

You can move from Kubernetes Ingress to Gateway API step by step, without changing everything at once. Install the CRDs, enable a supported controller, and create Gateway API objects next to the old Ingress. Test with curl --resolve, switch DNS, and remove the old Ingress only after validation.

Gateway API v1.6 also gives you stable TCPRoute and UDPRoute support for services that classic Ingress cannot handle. You can run production K3s and Gateway API workloads on a PerLod dedicated server with predictable networking.

We hope you enjoy this guide.

For more details about routing TCP traffic with Gateway API, read the official Gateway API TCP routing docs.