Module 4: Networking Ingress And Networkpolicy
5. Hands-on: `Ingress` for `status-api-service`
Description
This is the lesson where, for the first time in this entire guide, you talk to andes-cargo-status-api without kubectl port-forward. You're going to declare status-api-ingress, confirm ingress-nginx — installed and healthy in lesson 4 — detected it, and really curl it against the exact same host and port any external client would use. You're also going to confirm, again with honesty, that /shipments/<id> still has no real data to connect to — Ingress doesn't resolve that limit, and this lesson explains why before you confuse it with a new error.
Connection to the module
This lesson brings together everything lessons 2-4 built: the networking model that makes it possible for ingress-nginx to reach any Pod (lesson 2), the Ingress object explained in theory (lesson 3), and the real controller running on the correct node (lesson 4). It's the first time you see, with real evidence, this module's complete promise: a permanent HTTP door, with no dependence on any open terminal.
Step 1 — The Ingress object: ingress.yaml
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: status-api-ingress
namespace: andes-cargo
spec:
ingressClassName: nginx
rules:
- host: andes-cargo.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: status-api-service
port:
number: 80
Five fields deserve explanation, each directly connected to what you already built:
ingressClassName: nginx— the exact value lesson 4's manifest created as theIngressClass. Without this field, a cluster with more than one Ingress controller installed (Module 7's case, with the AWS Load Balancer Controller) wouldn't know which of the two this rule belongs to.host: andes-cargo.local— the name a client has to use in its HTTP request'sHostheader for this rule to apply. It's not a real public domain and requires no DNS configured — this lesson is going to resolve it directly against127.0.0.1in Step 3 — it's just the valueingress-nginxuses to decide "this request is for Andes Cargo."path: /withpathType: Prefix— any path starting with/(that is, all of them) routes to the same backend.andes-cargo-status-apidoesn't need different routing rules per path yet —/healthand/shipments/<id>go to the same place — so a singlepath: /is enough.backend.service.name: status-api-service— exactly Module 2'sService, with no change. As lesson 3 already confirmed, anIngressnever points directly at Pods, always through an existingService.port.number: 80— the sameport(nottargetPort) you already used withkubectl port-forwardin Module 2 —Ingresstalks to theServiceon its declared port, and it's theServicethat decides, underneath, which containertargetPortto forward to.
kubectl apply -f ingress.yaml
What to expect:
ingress.networking.k8s.io/status-api-ingress created
Step 2 — Verify: the Ingress and its state
kubectl get ingress -n andes-cargo
What to expect (ADDRESS may take a few seconds to appear; on kind it fills in with localhost, unlike lesson 4's Service's EXTERNAL-IP: <pending> — the mechanism by which ingress-nginx calculates this value is internal to the controller, not relevant for the rest of this lesson):
NAME CLASS HOSTS ADDRESS PORTS AGE
status-api-ingress nginx andes-cargo.local localhost 80 0s
Confirm the rule got registered fully, with the correct backend and its real Endpoints:
kubectl describe ingress status-api-ingress -n andes-cargo
What to expect (the three IPs in the last line are your variable values — they correspond to the Deployment's three Pods; the rest is literal):
Name: status-api-ingress
Labels: <none>
Namespace: andes-cargo
Address:
Ingress Class: nginx
Default backend: <default>
Rules:
Host Path Backends
---- ---- --------
andes-cargo.local
/ status-api-service:80 (10.244.2.2:8080,10.244.1.2:8080,10.244.2.3:8080)
Annotations: <none>
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Sync 0s nginx-ingress-controller Scheduled for sync
The Backends line is this lesson's most important confirmation: ingress-nginx already resolved status-api-service all the way down to the three Pods' real IPs — the same Endpoints mechanism you already know from Module 2 — and has them ready to route traffic to, before you've done any curl yet.
Step 3 — Real curl, no port-forward
This is this lesson's central moment. kind-config.yaml (lesson 4) already forwards your machine's ports 80/443 toward andes-cargo-cluster-control-plane, where ingress-nginx runs. All that's left is telling curl to resolve andes-cargo.local toward your own machine, with no need to edit /etc/hosts:
curl -i -s --resolve andes-cargo.local:80:127.0.0.1 http://andes-cargo.local/health
What to expect (literal, executed — Date is your variable value; notice something important: no kubectl port-forward running in any terminal):
HTTP/1.1 200 OK
Date: Fri, 14 Aug 2026 20:38:49 GMT
Content-Type: application/json
Content-Length: 51
Connection: keep-alive
{"service":"andes-cargo-status-api","status":"ok"}
--resolve andes-cargo.local:80:127.0.0.1 tells curl: "when someone asks you for andes-cargo.local on port 80, resolve it toward 127.0.0.1, with no real DNS lookup" — the same result as editing /etc/hosts, with no need for admin permissions or leaving a permanent change on your system. The Host: andes-cargo.local header curl automatically generates from the URL is the piece ingress-nginx reads to decide which rule to apply — exactly the host field you declared in ingress.yaml.
Compare this result against Module 2's lesson 7: the same JSON body, the same 200 code, but this time with no temporary tunnel in the middle — the complete path (your machine → Docker → andes-cargo-cluster-control-plane → ingress-nginx → status-api-service → a real Pod) works end to end, permanently, as long as the cluster keeps running.
Step 4 — The honest limit, again: /shipments/<id>
curl -i -s --max-time 30 --resolve andes-cargo.local:80:127.0.0.1 http://andes-cargo.local/shipments/4471
What to expect (literal, executed — the request takes several seconds for the same reason Module 3 already documented; this result remains the correct, expected behavior at this point in the guide):
HTTP/1.1 500 INTERNAL SERVER ERROR
Date: Fri, 14 Aug 2026 20:43:24 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 265
Connection: keep-alive
<!doctype html>
<html lang=en>
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>
None of this is an Ingress error — it's the same NameResolutionError/EndpointConnectionError Module 3 diagnosed in depth, now traveling through an additional layer (ingress-nginx) that changes nothing about the cause: andes-cargo-status-api still has no LocalStack running inside the cluster to connect to. Ingress resolves how traffic reaches the Pod — it never resolves where the data that Pod needs comes from.
WHAT CHANGED BETWEEN MODULE 2 AND THIS LESSON
Module 2, lesson 7 curl via port-forward /health → 200
/shipments/<id> → 500
(NoCredentialsError)
Module 3, lesson 4 curl via port-forward /health → 200
/shipments/<id> → 500
(NameResolutionError)
Module 4, lesson 5 curl via Ingress, /health → 200
(this lesson) NO port-forward /shipments/<id> → 500
(same cause: no LocalStack)
What's still missing, and why it stays missing:
- LocalStack running inside the cluster itself ──▶ outside this guide's $0 scope
Step 5 — A look at the "wrong header" error
It's worth seeing, even once, what happens when the Host header does not match any Ingress rule — because the error message is different from any you've seen in this guide, and recognizing it saves diagnostic time:
curl -i -s --max-time 8 http://localhost:80/health
What to expect (literal, executed — without --resolve, curl sends Host: localhost, which no status-api-ingress rule recognizes):
HTTP/1.1 404 Not Found
Date: Fri, 14 Aug 2026 20:38:49 GMT
Content-Type: text/html
Content-Length: 146
Connection: keep-alive
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>
This 404 doesn't come from andes-cargo-status-api — Flask never got the request — it comes from ingress-nginx itself, acting as the "default backend" when no rule matches the received Host header. It's direct proof that Ingress routes by host, not just by port: reaching the correct port 80 isn't enough if the Host header doesn't match any declared rule.
Analogy: the visitor who arrives at the front desk, asking by name
Picking back up the building analogy: --resolve andes-cargo.local:80:127.0.0.1 is the equivalent of telling a taxi "take me to the building's address, and once there, ask for 'Andes Cargo'" — the taxi (the TCP connection) drops you at the correct door thanks to extraPortMappings (lesson 4), but the receptionist (ingress-nginx) still needs to hear the exact name (Host: andes-cargo.local) to know which floor to send you to. Asking for a name that isn't on their list of registered companies (Host: localhost, Step 5) rightfully gets you a "no one's here by that name" — the 404 the receptionist themself generated, with no company in the building even finding out you asked.
Common mistakes
Forgetting --resolve (or not editing /etc/hosts) and getting an unexpected 404, not understanding why (this lesson's most common mistake, already previewed on purpose in Step 5). What happens: someone runs curl http://andes-cargo.local/health directly, with no --resolve and no /etc/hosts entry, and curl fails with a DNS resolution error — it doesn't even reach Step 5's 404, because andes-cargo.local isn't a real domain any public DNS knows. How to spot it: curl's error mentions "could not resolve host." How to fix it: use --resolve andes-cargo.local:80:127.0.0.1 in every command (this lesson's pattern), or add a 127.0.0.1 andes-cargo.local line to /etc/hosts if you prefer not to repeat the flag on every command.
Confusing ingress-nginx's 404 (Step 5) with a 404 from the Flask application (diagnostics). What happens: someone sees a 404 and starts reviewing app.py's routes, assuming Flask doesn't recognize the requested URL. How to spot it: look at the response body — ingress-nginx's 404 has <center>nginx</center> in the HTML; a real Flask 404 would have a completely different shape (the same one you'd see if you requested a route that doesn't exist in app.py, like /does-not-exist). How to fix it: if you see nginx's generic HTML, the problem is Ingress routing (wrong Host header, or no matching rule) — the request never reached andes-cargo-status-api.
Expecting /shipments/<id> to work now that there's Ingress (continuity, already anticipated in this module's lesson 1). What happens: someone, seeing curl work for the first time with no port-forward, expects the second endpoint to also respond with real data. How to spot it: if you're surprised to see the same 500 Modules 2 and 3 left. How to fix it: Ingress resolved the "how traffic arrives" — the root cause behind /shipments/<id> (no LocalStack running in the cluster) didn't change, and isn't going to change in any module of this guide: the data layer stays outside its $0 scope. /health is the complete network signal; /shipments/<id> stays representative through the capstone.
Exercises
Exercise 1 — Reconstruct the whole flow from memory. Without going back to the lesson, list the steps, in order, from declaring ingress.yaml to confirming /health with 200 and no port-forward.
See solution
- Declare
ingress.yamlwithingressClassName: nginx,host: andes-cargo.local,path: /(Prefix), backendstatus-api-service:80, and apply it. - Verify with
kubectl get ingress -n andes-cargo(the assignedADDRESS) andkubectl describe ingress(confirm the realBackends). curl -i --resolve andes-cargo.local:80:127.0.0.1 http://andes-cargo.local/health, confirming200 OKwith nokubectl port-forwardrunning.
Exercise 2 — Explain ingress-nginx's 404 to a colleague without using the word "error." A colleague sees a 404 with <center>nginx</center> in the body and asks you what it means. Explain it to them in two or three sentences, focusing on what ingress-nginx checked before rejecting the request.
See solution
A reasonable explanation: "That 404 doesn't come from our application — it comes straight from the receptionist (ingress-nginx), who checked your request's Host header and found no registered rule for that name. It's like walking up to a building's front desk and asking for a company that isn't in their directory — they tell you no one's here by that name, with no need to call any floor."
Exercise 3 — Predict what would happen with a second Ingress, same host, different path. If you added a second Ingress object with the same host: andes-cargo.local, but path: /admin pointing at a different (imaginary, don't create it) Service, would you expect ingress-nginx to combine both rules, or for the second to replace the first? Justify your answer with what you know about the path field.
See solution
It would combine them — ingress-nginx can have multiple rules for the same host, each with a different path, and it evaluates all of them: a request to andes-cargo.local/health would still go to status-api-service (matches path: /), while a request to andes-cargo.local/admin would go to the second Ingress's imaginary Service. This is, in fact, exactly the pattern that makes Ingress useful compared to exposing each Service separately — lesson 3's table anticipated it: several routing rules, all behind the same entry door.
Summary and next step
In this lesson andes-cargo-status-api received, for the first time in this guide, real traffic with no temporary tunnel: you declared status-api-ingress, confirmed ingress-nginx resolved the real Backends, and really curled it against http://andes-cargo.local/health, with a 200 OK response — the exact same body you already saw in Module 2, now served through a permanent HTTP door. /shipments/4471 kept responding 500, for the exact same cause Module 3 documented (no LocalStack in the cluster), and you saw, along the way, what a 404 generated by ingress-nginx itself looks like when the Host header doesn't match any rule.
Before moving on you should be able to: explain every field in ingress.yaml without looking at it; tell an ingress-nginx 404 apart from a real Flask 404 by the response body; and confirm real curl against your cluster, with no kubectl port-forward running.
Next lesson: NetworkPolicy, by default everyone talks to everyone — and why that doesn't last. This module's "incoming traffic" half is resolved. Lesson 6 opens the second half: who, inside the cluster, can talk to status-api-service — and why, today, the answer is "anyone."
Resources
- Kubernetes — Ingress — the same reference from lesson 3, now confirmed with real evidence, including the complete
rules/pathTypesyntax. - curl —
--resolve— official reference for the flag used in Step 3, the privilege-free alternative to editing/etc/hosts. - ingress-nginx — Custom errors — official documentation for Step 5's "default backend" behavior, when no rule matches.
kubernetes-and-eks-in-production-guide(NIEVA), Module 3, lesson 4 — the completeNameResolutionError/EndpointConnectionErrortraceback this lesson confirms unchanged.