Lab: Routing Traffic with Gateway API
Objectives
By the end of this lab, you will be able to:
- Install NGINX Gateway Fabric as a Gateway API controller in your Kubernetes cluster
- Create a Gateway resource with an HTTP listener
- Configure HTTPRoutes to route traffic to the Voting App vote and result services
- Implement path-based routing to serve multiple services from a single Gateway
- Explore traffic splitting for canary deployments using weighted backend references
Prerequisites
Before starting this lab, ensure you have:
- A running KIND cluster (from Module 0)
- kubectl CLI configured to communicate with your cluster
- Basic understanding of Kubernetes Services and Pods
- The kind CLI tool installed
Module 3 starts with a clean slate. If you have autoscaling resources from Module 2 (HPA, VPA, KEDA), you can clean them up or leave them - we'll be deploying a fresh Voting App either way to focus on Gateway API routing concepts.
Setup
Follow these steps to prepare your environment for this lab.
Step 1: Verify cluster status
kubectl cluster-info
kubectl get nodes
You should see your KIND cluster running with a control plane and worker nodes.
Step 2: Clean up previous deployments (optional)
If you want a completely fresh start:
kubectl delete all --all -n default
Or, if you want to keep Module 2 resources and work in a separate namespace:
kubectl create namespace gateway-demo
kubectl config set-context --current --namespace=gateway-demo
For this lab, we'll work in the default namespace with a fresh deployment.
Step 3: Deploy the base Voting App
Deploy the base Voting App using the example YAMLs:
# Deploy all Voting App components
kubectl apply -f examples/voting-app/postgres-deployment.yaml
kubectl apply -f examples/voting-app/postgres-service.yaml
kubectl apply -f examples/voting-app/redis-deployment.yaml
kubectl apply -f examples/voting-app/redis-service.yaml
kubectl apply -f examples/voting-app/worker-deployment.yaml
kubectl apply -f examples/voting-app/vote-deployment.yaml
kubectl apply -f examples/voting-app/vote-service.yaml
kubectl apply -f examples/voting-app/result-deployment.yaml
kubectl apply -f examples/voting-app/result-service.yaml
Step 4: Verify the Voting App is running
kubectl get pods
kubectl get svc
Expected output:
NAME READY STATUS RESTARTS AGE
postgres-xxxxxxxxxx-xxxxx 1/1 Running 0 30s
redis-xxxxxxxxxx-xxxxx 1/1 Running 0 30s
result-xxxxxxxxxx-xxxxx 1/1 Running 0 30s
vote-xxxxxxxxxx-xxxxx 1/1 Running 0 30s
worker-xxxxxxxxxx-xxxxx 1/1 Running 0 30s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
postgres ClusterIP 10.96.x.x <none> 5432/TCP 30s
redis ClusterIP 10.96.x.x <none> 6379/TCP 30s
result ClusterIP 10.96.x.x <none> 80/TCP 30s
vote ClusterIP 10.96.x.x <none> 80/TCP 30s
Step 5: Install Gateway API CRDs
Gateway API requires custom resource definitions (CRDs) to be installed:
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.1.0/standard-install.yaml
Expected output:
customresourcedefinition.apiextensions.k8s.io/gatewayclasses.gateway.networking.k8s.io created
customresourcedefinition.apiextensions.k8s.io/gateways.gateway.networking.k8s.io created
customresourcedefinition.apiextensions.k8s.io/httproutes.gateway.networking.k8s.io created
...
Step 6: Verify CRDs are installed
kubectl get crd | grep gateway
Expected output:
gatewayclasses.gateway.networking.k8s.io 2024-xx-xx...
gateways.gateway.networking.k8s.io 2024-xx-xx...
httproutes.gateway.networking.k8s.io 2024-xx-xx...
Your cluster is now ready for Gateway API resources.
Tasks
Task 1: Install NGINX Gateway Fabric
NGINX Gateway Fabric is the enterprise-standard Gateway API implementation from NGINX. It's widely used in production environments and provides excellent performance and reliability.
Step 1: Install NGINX Gateway Fabric CRDs
kubectl apply -f https://raw.githubusercontent.com/nginxinc/nginx-gateway-fabric/v1.4.0/deploy/crds.yaml
Step 2: Install NGINX Gateway Fabric
kubectl apply -f https://raw.githubusercontent.com/nginxinc/nginx-gateway-fabric/v1.4.0/deploy/default/deploy.yaml
This installs NGINX Gateway Fabric in the nginx-gateway namespace with all necessary components, including an automatically created GatewayClass.
Step 3: Wait for NGINX Gateway to be ready
kubectl wait --for=condition=available --timeout=300s deployment/nginx-gateway -n nginx-gateway
Expected output:
deployment.apps/nginx-gateway condition met
Step 4: Verify NGINX Gateway components are running
kubectl get pods -n nginx-gateway
Expected output:
NAME READY STATUS RESTARTS AGE
nginx-gateway-xxxxxxxxxx-xxxxx 2/2 Running 0 2m
Step 5: Verify the GatewayClass was created
kubectl get gatewayclass
Expected output:
NAME CONTROLLER ACCEPTED AGE
nginx gateway.nginx.org/nginx-gateway-controller True 2m
Explanation: NGINX Gateway Fabric automatically registers a GatewayClass named nginx. This tells Kubernetes that NGINX is available to process Gateway resources. The ACCEPTED: True status means NGINX Gateway is ready to handle Gateways.
Task 2: Create a Gateway
Now let's create a Gateway that listens on port 80 for HTTP traffic. This Gateway will accept HTTPRoutes from the same namespace.
Step 1: Create the Gateway resource
Create a file named gateway.yaml:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: voting-app-gateway
namespace: default
spec:
gatewayClassName: nginx
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: Same
Step 2: Apply the Gateway
kubectl apply -f gateway.yaml
Expected output:
gateway.gateway.networking.k8s.io/voting-app-gateway created
Step 3: Verify the Gateway status
kubectl get gateway
Expected output:
NAME CLASS ADDRESS PROGRAMMED AGE
voting-app-gateway nginx 10.96.x.x True 30s
Step 4: Check the Gateway details
kubectl describe gateway voting-app-gateway
Look for these conditions:
Conditions:
Type Status Reason
---- ------ ------
Accepted True Accepted
Programmed True Programmed
Explanation: As a cluster operator, you've defined that HTTP traffic on port 80 is accepted by this Gateway. Application developers can now create HTTPRoutes to route their specific application traffic. The Gateway has been programmed into NGINX Gateway Fabric and is ready to route traffic.
Task 3: Create HTTPRoutes for Vote and Result
Let's create HTTPRoutes to expose the vote and result services using hostname-based routing.
Step 1: Create HTTPRoute for the vote service
Create a file named vote-httproute.yaml:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: vote-route
namespace: default
spec:
parentRefs:
- name: voting-app-gateway
hostnames:
- "vote.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: vote
port: 80
Step 2: Create HTTPRoute for the result service
Create a file named result-httproute.yaml:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: result-route
namespace: default
spec:
parentRefs:
- name: voting-app-gateway
hostnames:
- "result.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: result
port: 80
Step 3: Apply both HTTPRoutes
kubectl apply -f vote-httproute.yaml
kubectl apply -f result-httproute.yaml
Step 4: Verify the HTTPRoutes are accepted
kubectl get httproute
Expected output:
NAME HOSTNAMES AGE
result-route ["result.example.com"] 30s
vote-route ["vote.example.com"] 30s
Step 5: Check route attachment status
kubectl describe httproute vote-route
Look for these conditions:
Conditions:
Type Status Reason
---- ------ ------
Accepted True Accepted
ResolvedRefs True ResolvedRefs
Both Accepted: True and ResolvedRefs: True mean the route successfully attached to the Gateway and found the backend service.
Step 6: Test access using Host headers
First, get the Envoy service address. In KIND, we'll use port-forwarding:
kubectl port-forward -n nginx-gateway svc/nginx-gateway 8080:80
Leave this running in one terminal, and in another terminal, test with curl:
# Test vote service
curl -H "Host: vote.example.com" http://localhost:8080
# Test result service
curl -H "Host: result.example.com" http://localhost:8080
You should see HTML responses from both services.
Explanation: Each service gets its own hostname. The Gateway handles routing based on the Host header. In a production environment with DNS configured, users would access http://vote.example.com and http://result.example.com directly without needing to set headers manually.
Task 4: Path-Based Routing
Hostname-based routing works well, but sometimes you want to serve multiple services from a single hostname using path prefixes. Let's create a combined HTTPRoute with path-based routing.
Step 1: Delete the existing hostname-based routes
kubectl delete httproute vote-route result-route
Step 2: Create a combined path-based HTTPRoute
Create a file named combined-httproute.yaml:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: voting-app-route
namespace: default
spec:
parentRefs:
- name: voting-app-gateway
rules:
- matches:
- path:
type: PathPrefix
value: /vote
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /
backendRefs:
- name: vote
port: 80
- matches:
- path:
type: PathPrefix
value: /result
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /
backendRefs:
- name: result
port: 80
Step 3: Apply the combined route
kubectl apply -f combined-httproute.yaml
Step 4: Verify the route
kubectl get httproute voting-app-route
kubectl describe httproute voting-app-route
Check that both Accepted and ResolvedRefs are True.
Step 5: Test path-based routing
With your port-forward still running (or restart it):
kubectl port-forward -n nginx-gateway svc/nginx-gateway 8080:80
Test both paths:
# This should route to the vote service
curl http://localhost:8080/vote
# This should route to the result service
curl http://localhost:8080/result
Understanding path matching and URL rewrite:
- PathPrefix matches any path that starts with the specified value
/votematches/vote,/vote/,/vote/anything/resultmatches/result,/result/,/result/data- URLRewrite filter is critical here - it strips the path prefix (
/vote→/) before forwarding to the backend - Without URL rewrite, the vote service would receive requests to
/votewhich it doesn't understand (it expects/) - The
ReplacePrefixMatchtype replaces the matched prefix with the specified value (in our case,/)
For exact matches, you would use type: Exact which only matches the exact path specified (no trailing content).
Step 6: Test in browser
Open your browser to:
http://localhost:8080/vote- You should see the voting interfacehttp://localhost:8080/result- You should see the results page
Explanation: Path-based routing lets you expose multiple services from a single hostname and port. This is common in production - example.com/api goes to the API service, example.com/admin goes to the admin service, etc.
Task 5: Traffic Splitting (Canary Pattern)
Gateway API makes it easy to split traffic between multiple backend versions using weights. This enables canary deployments - gradually shifting traffic to a new version while monitoring for errors.
Step 1: Create a canary version of the vote service
Create a file named vote-canary-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: vote-canary
labels:
app: voting-app
tier: frontend
spec:
replicas: 1
selector:
matchLabels:
app: vote-canary
template:
metadata:
labels:
app: vote-canary
tier: frontend
spec:
containers:
- name: vote
image: schoolofdevops/vote:v1
env:
- name: CANARY_VERSION
value: "true"
ports:
- containerPort: 80
name: http
Create a service for the canary:
apiVersion: v1
kind: Service
metadata:
name: vote-canary
labels:
app: voting-app
tier: frontend
spec:
selector:
app: vote-canary
ports:
- port: 80
targetPort: 80
protocol: TCP
name: http
Step 2: Deploy the canary
kubectl apply -f vote-canary-deployment.yaml
kubectl apply -f vote-canary-service.yaml
Verify:
kubectl get pods -l app=vote-canary
kubectl get svc vote-canary
Step 3: Update the HTTPRoute with traffic splitting
Modify combined-httproute.yaml to add weighted backends for the /vote path:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: voting-app-route
namespace: default
spec:
parentRefs:
- name: voting-app-gateway
rules:
- matches:
- path:
type: PathPrefix
value: /vote
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /
backendRefs:
- name: vote
port: 80
weight: 90
- name: vote-canary
port: 80
weight: 10
- matches:
- path:
type: PathPrefix
value: /result
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /
backendRefs:
- name: result
port: 80
Step 4: Apply the updated route
kubectl apply -f combined-httproute.yaml
Step 5: Test traffic distribution
Send multiple requests and observe which backend handles them:
for i in {1..20}; do
curl -s http://localhost:8080/vote | grep -o "vote\|vote-canary" || echo "response $i"
done
You should see approximately 90% going to the stable vote service and 10% to vote-canary.
Explanation: Traffic splitting enables canary deployments. You start with 95/5 or 90/10, monitor error rates and performance metrics, gradually shift to 50/50, then 10/90, and finally 0/100 once the new version is validated. If errors spike, you can instantly roll back by changing the weights.
Step 6: Header-based routing (alternative canary pattern)
Instead of random distribution, you can route specific users to the canary based on headers. Update your HTTPRoute:
rules:
- matches:
- path:
type: PathPrefix
value: /vote
headers:
- name: X-Canary
value: "true"
backendRefs:
- name: vote-canary
port: 80
- matches:
- path:
type: PathPrefix
value: /vote
backendRefs:
- name: vote
port: 80
Now only requests with X-Canary: true header go to the canary:
# Goes to stable version
curl http://localhost:8080/vote
# Goes to canary version
curl -H "X-Canary: true" http://localhost:8080/vote
This lets you give testers a browser extension or modify your mobile app to set the header, allowing them to test the canary while normal users see the stable version.
Challenge: Cross-Namespace Routing with ReferenceGrant
Let's explore Gateway API's namespace isolation and how to enable controlled cross-namespace access.
Step 1: Create a new namespace for a separate team
kubectl create namespace team-b
Step 2: Try to create an HTTPRoute in team-b that references the Gateway in default
Create a file named team-b-httproute.yaml:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: team-b-route
namespace: team-b
spec:
parentRefs:
- name: voting-app-gateway
namespace: default
hostnames:
- "team-b.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: vote
namespace: default
port: 80
Apply it:
kubectl apply -f team-b-httproute.yaml
Step 3: Check if the route attached
kubectl describe httproute -n team-b team-b-route
Look at the conditions. You should see Accepted: False with an error message about namespace references not being permitted.
Step 4: Create a ReferenceGrant to allow cross-namespace access
In the default namespace (where the Gateway lives), create a ReferenceGrant:
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-team-b
namespace: default
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: team-b
to:
- group: gateway.networking.k8s.io
kind: Gateway
Apply it:
kubectl apply -f reference-grant.yaml
Step 5: Verify the route now attaches
kubectl describe httproute -n team-b team-b-route
Now you should see Accepted: True.
Explanation: Gateway API enforces namespace boundaries by default to prevent security issues. ReferenceGrant explicitly allows specific namespaces to reference resources in other namespaces. This enables platform teams to manage shared Gateways while allowing application teams in different namespaces to create routes.
Learning: If your HTTPRoute doesn't attach, always check:
- Are the Gateway and HTTPRoute in the same namespace?
- If not, does a ReferenceGrant exist?
- Are the service references correct?
Verification
Confirm your lab setup is working correctly:
1. Check NGINX Gateway Fabric is running
kubectl get pods -n nginx-gateway
All pods should show STATUS: Running.
2. Verify GatewayClass exists
kubectl get gatewayclass contour
Should show ACCEPTED: True.
3. Verify Gateway is programmed
kubectl get gateway voting-app-gateway
Should show PROGRAMMED: True.
4. Check HTTPRoutes are accepted
kubectl get httproute
All routes should show hostnames or have descriptions.
kubectl describe httproute voting-app-route
Should show Accepted: True and ResolvedRefs: True.
5. Test end-to-end routing
# Port-forward if not already running
kubectl port-forward -n nginx-gateway svc/nginx-gateway 8080:80 &
# Test vote service
curl -s http://localhost:8080/vote | grep -i vote
# Test result service
curl -s http://localhost:8080/result | grep -i result
Both should return HTML content.
6. Verify traffic splitting works
Send 10 requests and observe distribution:
for i in {1..10}; do curl -s http://localhost:8080/vote > /dev/null && echo "Request $i sent"; done
Check pod logs to see which backends handled requests:
kubectl logs -l app=vote --tail=5
kubectl logs -l app=vote-canary --tail=5
You should see approximately 9 requests in vote logs and 1 in vote-canary logs (90/10 split).
7. Test Voting App functionality
Open your browser:
- Navigate to
http://localhost:8080/vote - Cast a vote
- Navigate to
http://localhost:8080/result - Verify your vote appears
The Voting App should be fully functional through the Gateway.
Cleanup
Clean up the Gateway API resources created in this lab:
# Delete HTTPRoutes
kubectl delete httproute voting-app-route
kubectl delete httproute -n team-b team-b-route
# Delete Gateway
kubectl delete gateway voting-app-gateway
# Delete canary deployment
kubectl delete deployment vote-canary
kubectl delete service vote-canary
# Delete ReferenceGrant
kubectl delete referencegrant -n default allow-team-b
# Delete team-b namespace
kubectl delete namespace team-b
# Optionally, uninstall NGINX Gateway Fabric
kubectl delete namespace nginx-gateway
kubectl delete gatewayclass nginx
# Optionally, clean up Voting App
kubectl delete -f examples/voting-app/
Module 4 (Service Mesh Decision) is an evaluation module, not a hands-on implementation. You can keep your KIND cluster running, but Gateway API resources aren't needed for the next module.
Troubleshooting
Issue: HTTPRoute shows Accepted: False
Symptom: When you run kubectl describe httproute, you see Accepted: False in the conditions.
Cause: The HTTPRoute can't attach to the Gateway. Common reasons:
- Gateway and HTTPRoute are in different namespaces without a ReferenceGrant
- GatewayClass doesn't exist or isn't accepted
- Gateway doesn't exist or isn't programmed
Solution:
# Check if Gateway exists and is in the same namespace
kubectl get gateway -A
# Check GatewayClass
kubectl get gatewayclass
# If cross-namespace, check for ReferenceGrant
kubectl get referencegrant -A
# Describe HTTPRoute for detailed error message
kubectl describe httproute <name>
Issue: HTTPRoute shows ResolvedRefs: False
Symptom: Accepted: True but ResolvedRefs: False.
Cause: The backend service referenced in backendRefs doesn't exist or is in the wrong namespace.
Solution:
# Check if the service exists
kubectl get svc vote result
# Check service name spelling in HTTPRoute
kubectl get httproute <name> -o yaml | grep -A 5 backendRefs
# If referencing cross-namespace service, ensure ReferenceGrant allows it
Issue: NGINX Gateway service has no external IP in KIND
Symptom: kubectl get svc -n nginx-gateway nginx-gateway shows <pending> for EXTERNAL-IP.
Cause: KIND doesn't provide LoadBalancer services by default - this is expected behavior in local clusters.
Solution:
Use port-forwarding instead:
kubectl port-forward -n nginx-gateway svc/nginx-gateway 8080:80
Then access via http://localhost:8080.
For production clusters (AWS, GCP, Azure), the LoadBalancer service type will provision a real load balancer with an external IP.
Issue: Gateway shows Programmed: False
Symptom: Gateway exists but PROGRAMMED column shows False.
Cause: The Gateway controller isn't running, or the GatewayClass doesn't match.
Solution:
# Check NGINX Gateway pods are running
kubectl get pods -n nginx-gateway
# Check Gateway references correct GatewayClass
kubectl get gateway <name> -o yaml | grep gatewayClassName
# Check GatewayClass exists and is accepted
kubectl get gatewayclass
# Check NGINX Gateway logs for errors
kubectl logs -n nginx-gateway deployment/nginx-gateway
Issue: curl returns "404 Not Found"
Symptom: Gateway and HTTPRoute both show as working, but curl returns 404.
Cause: The path or hostname doesn't match any route rules.
Solution:
# Check HTTPRoute rules
kubectl get httproute <name> -o yaml
# Ensure you're using the correct Host header (if hostname-based routing)
curl -H "Host: vote.example.com" http://localhost:8080
# Ensure you're using the correct path (if path-based routing)
curl http://localhost:8080/vote
Key Takeaways
-
Gateway API separates concerns - Infrastructure teams manage GatewayClasses, platform teams manage Gateways, app teams manage HTTPRoutes. Clean ownership boundaries.
-
HTTPRoutes are expressive - Path matching, header matching, and traffic splitting are built-in, standardized features. No vendor-specific annotations for common use cases.
-
Namespace isolation is enforced - HTTPRoutes can only reference Gateways in the same namespace unless ReferenceGrant explicitly allows cross-namespace access. This prevents accidental security holes.
-
Traffic splitting enables safe deployments - Weighted backend references make canary deployments and blue-green migrations straightforward. Start with 90/10, monitor, gradually shift traffic.
-
Gateway API is portable - Your HTTPRoutes and Gateways work with any compliant controller. NGINX Gateway Fabric today, Envoy Gateway tomorrow, Istio in production - the core configs stay the same.