65.5 Gateway API: Gateway, HTTPRoute, HTTPS and traffic splitting
Why: the problem
Ingress was designed in 2015 for simple cases, and it shows. Anything beyond host and path, such as header matching, traffic weights, redirects or timeouts, lives in controller-specific annotations, so an Ingress written for one controller silently does something else on another. There is also only one object, so the platform team that owns the load balancer and the app team that owns a route must edit the same YAML. When the community ingress-nginx project was retired on 24 March 2026, teams at companies like Zepto and CRED had to rewrite those annotations anyway, and most chose Gateway API as the target. Gateway API is the official successor, built by Kubernetes SIG Network and generally available since v1.0 in October 2023. It splits the job into roles. A GatewayClass, installed by the infrastructure provider, names an implementation such as Envoy Gateway, Istio, Cilium, Contour, NGINX Gateway Fabric or the AWS Gateway API Controller. A Gateway, owned by the platform team, opens listeners: port 443 for *.shop.example.com with this certificate, and says which namespaces may attach routes. An HTTPRoute, owned by an app team in its own namespace, attaches to that Gateway and says: for host api.shop.example.com and path /orders, send 90% to orders-v1 and 10% to orders-v2. Matching on headers, query parameters and methods, redirects, rewrites, header changes, mirroring and timeouts are all standard fields, not annotations. Cross-namespace references are explicit. A route can only attach to a Gateway whose allowedRoutes permits its namespace, and a route can only send traffic to a Service in another namespace if that namespace publishes a ReferenceGrant. cert-manager can read Gateway listeners and issue certificates for them, and ExternalDNS can read HTTPRoute host names, so the tools from 65.3 and 65.4 keep working. In this lesson we install the Gateway API CRDs and Envoy Gateway on the ch65 cluster, open an HTTP and an HTTPS listener, route by host, path and header, split traffic by weight for a canary, redirect HTTP to HTTPS, and translate one Ingress into an HTTPRoute.
What: every term in plain English
- Gateway API — Kubernetes standard API for traffic routing that replaces Ingress for new work.
- GatewayClass — Cluster object naming a controller implementation.
- Gateway — Load balancer listeners: ports, protocols, host names, certificates and which routes may attach.
- Listener — One port and protocol on a Gateway, optionally with a hostname and TLS settings.
- HTTPRoute — Rules matching host, path, headers or query and sending to weighted backends.
- parentRefs — Which Gateway, and optionally which listener, a route attaches to.
- backendRefs — Services and weights a matched request is sent to.
- allowedRoutes — Gateway setting controlling which namespaces may attach routes.
- ReferenceGrant — Permission object allowing references into another namespace.
- Filter — Standard action on a rule such as RequestRedirect, URLRewrite or RequestHeaderModifier.
How: step by step
- Install Gateway API CRDs and Envoy Gateway. GatewayClass Accepted.
- Create a Gateway with HTTP and HTTPS listeners. Gateway Programmed.
- Attach an HTTPRoute by host and path. Route Accepted.
- Add header matching for testers. X-Beta header goes to v2.
- Split traffic 90 and 10. Canary by weight.
- Redirect HTTP to HTTPS. 301 on port 80.
- Translate an Ingress to an HTTPRoute. Same behaviour, no annotations.
- Check status conditions. Accepted and ResolvedRefs True.
Example 1: Install Envoy Gateway and open listeners
Goal. Install Envoy Gateway and open listeners.
install.sh
# envoy-gateway-v1.5.1-install.yaml: the release manifest (with Gateway API v1.3 standard CRDs) saved from the official Envoy Gateway docs, pinned and reviewed
kubectl apply --server-side -f envoy-gateway-v1.5.1-install.yaml
kubectl wait -n envoy-gateway-system --for=condition=available deploy/envoy-gateway --timeout=180s
kubectl get crd | grep gateway.networking.k8s.io
gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: eg
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: public
namespace: infra
annotations:
cert-manager.io/cluster-issuer: lab-ca
spec:
gatewayClassName: eg
listeners:
- name: http
port: 80
protocol: HTTP
allowedRoutes: {namespaces: {from: All}}
- name: https
port: 443
protocol: HTTPS
hostname: "*.shop.example.com"
tls:
mode: Terminate
certificateRefs: [{name: shop-wildcard-tls}]
allowedRoutes:
namespaces:
from: Selector
selector: {matchLabels: {shop-routes: "yes"}}
What each line does
--server-side— The CRDs are large; server-side apply avoids the annotation size limit.GatewayClass eg— Points at the Envoy Gateway controller.Gateway in infra— Owned by the platform team, separate from apps.cert-manager annotation— cert-manager reads HTTPS listeners and issues shop-wildcard-tls from the lab CA.allowedRoutes selector— Only namespaces labelled shop-routes=yes may attach to 443.
Green output
$ bash install.sh
deployment.apps/envoy-gateway condition met
gatewayclasses.gateway.networking.k8s.io
gateways.gateway.networking.k8s.io
httproutes.gateway.networking.k8s.io
referencegrants.gateway.networking.k8s.io
$ kubectl create ns infra && kubectl label ns shop shop-routes=yes
$ kubectl apply -f gateway.yaml
$ kubectl get gatewayclass,gateway -A
NAME CONTROLLER ACCEPTED
gatewayclass.gateway.networking.k8s.io/eg gateway.envoyproxy.io/gatewayclass-controller True
NAMESPACE NAME CLASS ADDRESS PROGRAMMED
infra gateway.gateway.networking.k8s.io/public eg 172.18.0.240 True
Expected result. The GatewayClass was Accepted and the Gateway Programmed: Envoy Gateway created an Envoy proxy Deployment and a LoadBalancer Service for it (MetalLB from 62.4 gave the address). Apps can now attach routes.
Example 2: Route by host, path and header, then split by weight
Goal. Route by host, path and header, then split by weight.
route.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: orders
namespace: shop
spec:
parentRefs: [{name: public, namespace: infra, sectionName: https}]
hostnames: ["api.shop.example.com"]
rules:
- matches:
- path: {type: PathPrefix, value: /orders}
headers: [{name: X-Beta, value: "yes"}]
backendRefs: [{name: orders-v2, port: 80}]
- matches:
- path: {type: PathPrefix, value: /orders}
backendRefs:
- {name: orders-v1, port: 80, weight: 90}
- {name: orders-v2, port: 80, weight: 10}
timeouts: {request: 5s}
count.sh
GW=172.18.0.240
for i in $(seq 1 200); do
curl -sk --resolve api.shop.example.com:443:$GW https://api.shop.example.com/orders
done | sort | uniq -c
curl -sk -H "X-Beta: yes" --resolve api.shop.example.com:443:$GW https://api.shop.example.com/orders
What each line does
sectionName: https— Attach only to the 443 listener.Header rule first— More specific matches win: testers with X-Beta always get v2.weight 90 and 10— Exact traffic split by the proxy, independent of replica counts.timeouts— A standard field; with Ingress this was an annotation.-k— Skips trust only because this is a lab CA; production uses Let's Encrypt.
Green output
$ kubectl create deployment orders-v1 -n shop --image=hashicorp/http-echo:1.0 -- /http-echo -listen=:8080 -text="orders v1"
$ kubectl create deployment orders-v2 -n shop --image=hashicorp/http-echo:1.0 -- /http-echo -listen=:8080 -text="orders v2"
$ kubectl expose deploy orders-v1 -n shop --port=80 --target-port=8080 && kubectl expose deploy orders-v2 -n shop --port=80 --target-port=8080
$ kubectl apply -f route.yaml
$ kubectl get httproute orders -n shop -o jsonpath='{.status.parents[0].conditions[*].type}'
Accepted ResolvedRefs
$ bash count.sh
181 orders v1
19 orders v2
orders v2
Expected result. About 90 and 10 percent, as declared, and the tester header always reached v2. This is the canary from 64.5 with no controller-specific annotations at all.
Example 3: Redirect HTTP to HTTPS and translate an Ingress
Goal. Redirect HTTP to HTTPS and translate an Ingress.
redirect.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: http-to-https
namespace: infra
spec:
parentRefs: [{name: public, sectionName: http}]
rules:
- filters:
- type: RequestRedirect
requestRedirect: {scheme: https, statusCode: 301}
storefront-route.yaml
# Same behaviour as the storefront Ingress plus its rewrite from 65.2
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: storefront
namespace: shop
spec:
parentRefs: [{name: public, namespace: infra, sectionName: https}]
hostnames: ["store.shop.example.com", "www.shop.example.com"]
rules:
- matches: [{path: {type: PathPrefix, value: /api}}]
filters:
- type: URLRewrite
urlRewrite: {path: {type: ReplacePrefixMatch, replacePrefixMatch: /}}
backendRefs: [{name: api, port: 80}]
- backendRefs: [{name: web, port: 80}]
What each line does
Route in infra— The platform team owns the redirect for every host on port 80.RequestRedirect— Standard filter: scheme https, code 301.URLRewrite ReplacePrefixMatch— Replaces the old regex rewrite annotation with a typed field.Rule with no matches— Matches everything, so it is the default for these hosts.ingress2gateway— A Kubernetes SIGs tool that drafts HTTPRoutes from existing Ingress objects; always review its output.
Green output
$ kubectl apply -f redirect.yaml -f storefront-route.yaml
$ curl -sI --resolve www.shop.example.com:80:172.18.0.240 http://www.shop.example.com/cart | head -2
HTTP/1.1 301 Moved Permanently
location: https://www.shop.example.com/cart
$ curl -sk --resolve www.shop.example.com:443:172.18.0.240 https://www.shop.example.com/api/orders
api says hi
$ curl -sk --resolve www.shop.example.com:443:172.18.0.240 https://www.shop.example.com/
web says hi
$ kubectl get httproute -A
NAMESPACE NAME HOSTNAMES
infra http-to-https
shop orders ["api.shop.example.com"]
shop storefront ["store.shop.example.com","www.shop.example.com"]
Expected result. Port 80 now redirects every host to HTTPS, and the storefront Ingress with its rewrite became a portable HTTPRoute. Moving to another Gateway implementation later means changing one GatewayClass, not every route.
Every command, step by step (kind lab with kubectl)
-
CRDs
kubectl apply --server-side -f envoy-gateway-v1.5.1-install.yamlWhat it does: Gateway API and controller.
customresourcedefinition.apiextensions.k8s.io/httproutes.gateway.networking.k8s.io serverside-applied -
Ready
kubectl wait -n envoy-gateway-system --for=condition=available deploy/envoy-gatewayWhat it does: Running.
condition met -
Label
kubectl label ns shop shop-routes=yesWhat it does: Allowed.
namespace/shop labeled -
Gateway
kubectl apply -f gateway.yamlWhat it does: Listeners.
gateway.gateway.networking.k8s.io/public created -
Status
kubectl get gateway -n infraWhat it does: Programmed.
public eg 172.18.0.240 True -
Route
kubectl apply -f route.yamlWhat it does: Attached.
httproute.gateway.networking.k8s.io/orders created -
Split
bash count.shWhat it does: 90 and 10.
181 orders v1 -
Redirect
kubectl apply -f redirect.yamlWhat it does: 301.
httproute.gateway.networking.k8s.io/http-to-https created -
Check
curl -sI --resolve www.shop.example.com:80:172.18.0.240 http://www.shop.example.com/What it does: Moved.
HTTP/1.1 301 Moved Permanently -
Describe
kubectl describe httproute orders -n shopWhat it does: Conditions.
Reason: Accepted
When it is red 1
Gateway PROGRAMMED False, reason AddressNotAssigned
Cause. No load balancer address on kind.
Fix, step by step
- Install MetalLB as in 62.4.
- Or use Envoy Gateway's NodePort setting.
When it is red 2
HTTPRoute condition Accepted False, reason NotAllowedByListeners
Cause. Namespace not allowed by allowedRoutes, or hostname outside the listener hostname.
Fix, step by step
- Label the namespace.
- Match hostnames to *.shop.example.com.
When it is red 3
ResolvedRefs False, reason RefNotPermitted
Cause. backendRef to another namespace without a ReferenceGrant.
Fix, step by step
- Create a ReferenceGrant in the target namespace.
When it is red 4
no matches for kind HTTPRoute in version gateway.networking.k8s.io/v1
Cause. Gateway API CRDs not installed.
Fix, step by step
- Apply the CRDs with --server-side.
When it is red 5
HTTPS listener ResolvedRefs False, InvalidCertificateRef
Cause. Certificate Secret missing.
Fix, step by step
- kubectl get certificate -n infra.
- Check the cert-manager annotation.
Practice tasks
Easy Task 1: Install
Why: Setup.
What to do: Install CRDs and Envoy Gateway.
- --server-side.
Solution
kubectl get gatewayclassEasy Task 2: Gateway
Why: Open.
What to do: Apply gateway.yaml.
- Two listeners.
Solution
kubectl get gateway -n infraEasy Task 3: Route
Why: Attach.
What to do: Apply route.yaml.
- sectionName.
Solution
kubectl get httproute -n shopMedium Task 4: Header
Why: Testers.
What to do: Send X-Beta: yes.
- v2 always.
Solution
curl -sk -H "X-Beta: yes" --resolve api.shop.example.com:443:172.18.0.240 https://api.shop.example.com/ordersMedium Task 5: Weights
Why: Canary.
What to do: Count 200 requests.
- uniq -c.
Solution
bash count.shMedium Task 6: Redirect
Why: Secure.
What to do: Apply redirect.yaml.
- 301.
Solution
curl -sI --resolve www.shop.example.com:80:172.18.0.240 http://www.shop.example.com/Hard Task 7: Translate
Why: Migrate.
What to do: Rewrite the 65.2 rewrite Ingress as an HTTPRoute.
- URLRewrite.
Solution
kubectl get httproute storefront -n shop -o yamlHard Task 8: Zepto
Why: Main task.
What to do: Full Gateway API setup.
- Status conditions True.
Solution
kubectl get gateway,httproute -AHomework: try at home
For Zepto's delivery API, move from an Ingress to Gateway API: one Gateway in infra with HTTP and HTTPS listeners for *.zepto-lab.example.com, an HTTPRoute with a header rule for internal testers, a 95 and 5 weight split for a new version, a request timeout, and a global HTTP-to-HTTPS redirect, proved with curl counts.
Recap
- Gateway API splits GatewayClass, Gateway and HTTPRoute across provider, platform and app teams.
- Header matching, weights, redirects, rewrites and timeouts are standard fields, not annotations.
- allowedRoutes and ReferenceGrant make cross-namespace access explicit.
- cert-manager and ExternalDNS both understand Gateway API objects.
- With ingress-nginx retired, Gateway API is the recommended path for new and migrated clusters.
FAQ
Q. Is Ingress deprecated?
The Ingress API is frozen but not removed, and many controllers still support it. New features go only to Gateway API.
Q. Which implementation should I choose?
Pick by platform: AWS Gateway API Controller for VPC Lattice, Cilium or Istio if you already run them, Envoy Gateway, Contour or NGINX Gateway Fabric as standalone options. Check the conformance report for each.
Q. Does Gateway API support gRPC and TCP?
GRPCRoute is in the standard channel; TCPRoute, UDPRoute and TLSRoute are in the experimental channel in many versions.
Q. Can Argo Rollouts use Gateway API?
Yes, through its Gateway API traffic router plugin, which edits HTTPRoute weights.
Interview questions
-
Why was Gateway API created when Ingress existed?
Model answer: Ingress lacked standard features and role separation, so behaviour lived in incompatible annotations.
-
Explain the three main resources.
Model answer: GatewayClass picks the implementation, Gateway defines listeners and certificates, HTTPRoute defines matching and backends.
-
How do you do a canary with Gateway API?
Model answer: Two backendRefs with weights, adjusted step by step or by Argo Rollouts.
-
How do you migrate from ingress-nginx?
Model answer: Inventory annotations, draft routes with ingress2gateway, test behaviour such as regex and redirects, run both in parallel, then switch DNS.
Quick quiz
Q1. Which object does the app team own?
HTTPRoute.
Q2. Which object opens ports and holds certificates?
Gateway.
Q3. How do you split traffic?
weight on backendRefs.
Q4. What allows cross-namespace backends?
ReferenceGrant.
Q5. Which filter redirects to HTTPS?
RequestRedirect.
Samajla ka?