A beginner-friendly Kubernetes networking story about Gateway API, Services, HTTPRoutes, DNS, a few misleading errors, and the joy of finally seeing a successful response.
The mission
The goal of this exercise was simple to say and slightly less simple to do: deploy NGINX Gateway Fabric in my Kubernetes cluster, create a small NGINX web application, and make the application accessible through a Kubernetes Gateway and HTTPRoute.
The intended request path was:
My Kali terminal
|
| HTTP request to NodePort :30080
v
NGINX Gateway
|
| HTTPRoute matches path /
v
Kubernetes Service: frontend-svc:80
|
v
Pod: frontend-app (NGINX listens on port 80)
|
v
HTTP/1.1 200 OK
Think of it like sending a parcel. The Gateway is the front desk, the HTTPRoute is the instruction that says where the parcel should go, the Service is the stable address for the destination, and the Pod is the actual person receiving it.
This post walks through what I did, the problems I hit, and how I worked through them. The useful lesson was not just getting the page to load. It was learning to test one layer at a time instead of randomly changing things until Kubernetes takes pity on me.
1. Installing the Gateway API building blocks
I started in my nginx-gateway-fabric working directory and applied the standard Gateway API CRDs:
kubectl apply -k config/crd/gateway-api/standard
CRD means CustomResourceDefinition. Kubernetes comes with built-in resources such as Pods, Deployments, and Services. CRDs teach Kubernetes about additional resource types. The Gateway API CRDs add resources such as GatewayClass, Gateway, and HTTPRoute.
I then installed the NGINX Gateway Fabric-specific CRDs and the NodePort deployment for version 1.6.1:
kubectl apply -f https://raw.githubusercontent.com/nginx/nginx-gateway-fabric/v1.6.1/deploy/crds.yaml
kubectl apply -f https://raw.githubusercontent.com/nginx/nginx-gateway-fabric/v1.6.1/deploy/nodeport/deploy.yaml
The NodePort deployment gave me a way to reach the Gateway using a port on a Kubernetes node. In this lab, HTTP was exposed on port 30080.
Problem 1: Kustomize timed out while fetching the CRDs
My first attempt to fetch the Gateway API CRDs directly from GitHub using kubectl kustomize failed with a Git fetch timeout. Because the command failed to produce the manifests, the following kubectl apply command reported that no objects had been passed to it.
That second message was a consequence of the first failure, not a separate mystery.
What I did: I worked with the repository locally and applied the Kustomize directory from the checked-out source:
kubectl apply -k config/crd/gateway-api/standard
Lesson: When a command downloads configuration and then applies it, check the download/rendering step first. If it produces nothing, kubectl apply cannot magically apply it.
2. The Gateway Pod was Pending
After applying the NGINX Gateway Fabric deployment, I checked the Pod:
kubectl get pods -n nginx-gateway
At first, it showed Pending. My first attempt to describe it failed too:
kubectl describe pod nginx-gateway-55f96886f7-j4kzq
The Pod was not found because I had left out its namespace. It lived in nginx-gateway, not the default namespace.
The corrected command was:
kubectl describe pod nginx-gateway-55f96886f7-j4kzq -n nginx-gateway
The important event was:
0/2 nodes are available: 2 node(s) had untolerated taint(s).
What does that mean?
A taint is a rule on a Kubernetes node that says, in effect, “Don't schedule Pods here unless they tolerate this condition.” A toleration is a matching permission in a Pod's specification.
The Gateway Pod was not necessarily broken. Kubernetes was saying it could not find a node where the Pod was allowed to run. I also saw that k8s-worker1 briefly became NotReady before returning to Ready.
What I learned: Check the node status and the Pod's scheduling events before changing the deployment. Taints may be protecting the cluster for a reason, so don't remove them blindly just to make a Pod start.
Useful commands:
kubectl get nodes -o wide
kubectl describe pod nginx-gateway-55f96886f7-j4kzq -n nginx-gateway
kubectl describe node k8s-worker1
Eventually, the Gateway Pod was scheduled to k8s-worker1 and progressed toward running.
3. Creating the frontend application
Next, I created a simple NGINX Pod:
kubectl run frontend-app --image=nginx --port=8080
One subtle detail caused a later problem: --port=8080 does not reconfigure NGINX to listen on port 8080. It records container-port metadata in the Pod specification. The NGINX image still listens on port 80 by default.
I exposed the Pod as a Service:
kubectl expose pod frontend-app \
--name=frontend-svc \
--port=80 \
--target-port=8080
A Service gives a stable network address to a group of Pods. This is useful because Pods can be replaced, and their IP addresses can change. Clients can keep talking to the Service instead of tracking individual Pod IPs.
I checked the Service and its endpoints:
kubectl describe svc frontend-svc -n default
kubectl get endpoints frontend-svc -n default
Initially, the endpoint showed port 8080. That looked consistent with the Service configuration, but it was not consistent with the port NGINX actually used.
Problem 2: The Service pointed to the wrong port
I tested NGINX from inside its own Pod:
kubectl exec frontend-app -n default -- \
curl -I --max-time 5 http://127.0.0.1:8080/
It failed:
curl: (7) Failed to connect to 127.0.0.1 port 8080
Instead of guessing, I tested the default NGINX port:
kubectl exec frontend-app -n default -- \
curl -I --max-time 5 http://127.0.0.1:80/
That returned:
HTTP/1.1 200 OK
Server: nginx/1.31.7
There was the clue. NGINX was alive; I had simply sent it to the wrong port.
Fix: I changed the Service's targetPort to 80:
kubectl patch svc frontend-svc -n default \
-p '{"spec":{"ports":[{"port":80,"targetPort":80}]}}'
Then I checked it again:
kubectl describe svc frontend-svc -n default
The endpoint now pointed to the Pod on port 80.
Lesson: The Service's port is the port clients use to reach the Service. The targetPort is the port on the backend Pod that receives the traffic. Those values can be different, but targetPort must match the application that is actually listening.
4. Building the HTTPRoute
The next step was to tell the Gateway where to send incoming HTTP requests. I created an HTTPRoute called frontend-route.
The important parts of the route were:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: frontend-route
namespace: default
spec:
parentRefs:
- name: nginx-gateway
namespace: nginx-gateway
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: frontend-svc
port: 80
In plain English:
- Attach this route to the
nginx-gatewayGateway in thenginx-gatewaynamespace. - Match requests whose path starts with
/. - Forward matching requests to the Service named
frontend-svcon port80.
I checked the result with:
kubectl describe httproute frontend-route -n default
The good news was:
Type: Accepted
Status: True
Type: ResolvedRefs
Status: True
Message: All references are resolved
These conditions mean the Gateway accepted the route and Kubernetes could resolve its references. They are useful signals, but I still needed to test actual traffic.
Problem 3: The HTTPRoute could not find the backend Service
My first route was in the nginx-gateway namespace and referred to frontend-svc. Kubernetes returned:
BackendNotFound
spec.rules[0].backendRefs[0].name: Not found: "frontend-svc"
I had created frontend-svc in the default namespace. A Service reference in an HTTPRoute is normally resolved in the route's own namespace. Giving the Service the correct name was not enough; the namespace mattered too.
I edited the route YAML and placed the working frontend-route in default, while keeping its parentRefs pointing to the Gateway in nginx-gateway.
That fixed the reference. The route then reported both Accepted=True and ResolvedRefs=True.
Lesson: In Kubernetes, always check both the resource's name and namespace. Two resources can share a name in different namespaces, and a route can be accepted while still having an unresolved backend reference.
5. Testing the Service before testing the Gateway
Before troubleshooting the Gateway, I tested the Service directly. First, I checked that CoreDNS was running:
kubectl get pods -n kube-system
kubectl get svc -n kube-system kube-dns
Then I created a short-lived diagnostic Pod:
kubectl run dns-test --image=busybox:1.36 \
--restart=Never --command -- sleep 300
kubectl wait --for=condition=Ready pod/dns-test \
-n default --timeout=60s
I inspected its DNS settings:
kubectl exec dns-test -- cat /etc/resolv.conf
The file showed the cluster DNS Service IP, 10.96.0.10. I then tested DNS resolution:
kubectl exec dns-test -- \
nslookup kubernetes.default.svc.cluster.local
kubectl exec dns-test -- \
nslookup frontend-svc.default.svc.cluster.local
Both names resolved. The frontend Service resolved to 10.104.158.51.
Problem 4: One curl test could not resolve the Service name
An earlier temporary curl Pod returned:
curl: (6) Could not resolve host: frontend-svc.default.svc.cluster.local
That was confusing because the DNS Service and CoreDNS Pods looked healthy.
Rather than restarting DNS immediately, I used a separate diagnostic Pod to test the basics. Its nslookup commands successfully resolved both the Kubernetes API Service and frontend-svc. Then I tested the Service itself:
kubectl exec dns-test -- wget -S -O- \
http://frontend-svc.default.svc.cluster.local/
This returned the default NGINX welcome page and an HTTP/1.1 200 OK response.
What I can conclude: DNS resolution and Service connectivity worked from the diagnostic Pod. The earlier curl test failed, but the evidence did not establish its exact cause. The right move was to reproduce the test and inspect the layers separately rather than assume CoreDNS was broken.
6. The Gateway was programmed, but my first request returned a 500
I checked the Gateway and its Service:
kubectl get svc -n nginx-gateway
kubectl describe gateway nginx-gateway -n nginx-gateway
The Service showed:
TYPE NodePort
PORT(S) 80:30080/TCP,443:30081/TCP
The Gateway status showed Accepted=True and Programmed=True. That meant the Gateway configuration had been accepted and programmed, but an end-to-end test was still needed.
I checked the route in nginx-gateway and discovered that a second route with the same name existed there. Unlike the working route in default, this duplicate route still referenced a backend Service that was not in its namespace. Its status included BackendNotFound.
I deleted the broken duplicate route:
kubectl delete httproute frontend-route -n nginx-gateway
I kept the working route in default.
My first request to the NodePort returned 500 Internal Server Error. After removing the broken duplicate route, I tried again. The request succeeded.
The logs show that the bad duplicate route existed and that the response changed after I removed it. That makes the duplicate route a strong suspect in the earlier failure, although the response alone does not prove it was the only cause.
Problem 5: Two routes, one name, different namespaces
Kubernetes permits resources with the same name in different namespaces. That is not automatically an error, but it made this troubleshooting session confusing: one frontend-route was correctly configured, while the other had an unresolved backend.
Lesson: When a route behaves unexpectedly, check all routes across namespaces:
kubectl get httproute -A
Don't check only the namespace where you expect the route to live.
7. The final test: success through the NodePort
I checked the node addresses:
kubectl get nodes -o wide
The worker node's internal IP was 10.0.2.4. The Gateway's HTTP NodePort was 30080, so I tested:
curl -i --max-time 10 http://10.0.2.4:30080/
At first, I mistakenly typed the placeholder literally:
curl -i --max-time 10 http://WORKER_IP:30080/
The terminal returned Could not resolve host: WORKER_IP. The shell did exactly what I asked: it treated WORKER_IP as a hostname. Placeholders must be replaced with actual values before running a command.
The final request returned:
HTTP/1.1 200 OK
Server: nginx
Content-Length: 896
And, below the headers, the familiar “Welcome to nginx!” HTML page appeared.
That was the end-to-end confirmation I wanted: a request from my Kali terminal entered through the node's NodePort, reached the NGINX Gateway, matched the HTTPRoute, and was forwarded to the frontend Service and Pod.
Top comments (0)