Module 4: Networking Ingress And Networkpolicy
8. Project: `status-api-service` exposed and protected
Description
This project closes Module 4 with the test the previous seven lessons prepared for: Ingress (lessons 2-5) and NetworkPolicy (lessons 6-7) working together, verified in the same session, side by side. There's no new manifest in this project — every piece already exists, applied in its own lesson. What this project adds is the complete view: the path that should work, working; the path that should be blocked, blocked; and a checklist confirming neither is a coincidence. Everything that follows ran against andes-cargo-cluster.
Connection to the module
Each lesson in this module built one piece: the networking model (lesson 2), Ingress explained and built (lessons 3-5), NetworkPolicy explained and built (lessons 6-7). This project is the first time you see this module's two halves — "incoming traffic" and "who can generate it" — confirmed in the same place, closing the arc lesson 1 opened: from "only kubectl port-forward" to a service with a permanent HTTP door and a real network guardrail.
Step 1 — Confirm the complete state before verifying anything
kubectl get all,ingress,networkpolicy,configmap,secret -n andes-cargo
What to expect (Pod names, IPs, hpa's NodePort, and AGE are your variable values — the rest, including every object's name and the three-replica pattern, is literal):
NAME READY STATUS RESTARTS AGE
pod/andes-cargo-status-api-548966dd97-bpqjq 1/1 Running 0 5m56s
pod/andes-cargo-status-api-548966dd97-jwx5r 1/1 Running 0 5m56s
pod/andes-cargo-status-api-548966dd97-ls2bc 1/1 Running 0 5m56s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/status-api-service ClusterIP 10.96.239.125 <none> 80/TCP 5m56s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/andes-cargo-status-api 3/3 3 3 5m56s
NAME DESIRED CURRENT READY AGE
replicaset.apps/andes-cargo-status-api-548966dd97 3 3 3 5m56s
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
horizontalpodautoscaler.autoscaling/andes-cargo-status-api-hpa Deployment/andes-cargo-status-api cpu: <unknown>/50% 2 6 3 5m56s
NAME CLASS HOSTS ADDRESS PORTS AGE
ingress.networking.k8s.io/status-api-ingress nginx andes-cargo.local localhost 80 4m53s
NAME POD-SELECTOR AGE
networkpolicy.networking.k8s.io/allow-from-ingress-nginx app=andes-cargo-status-api 53s
networkpolicy.networking.k8s.io/default-deny-ingress <none> 3m24s
NAME DATA AGE
configmap/andes-cargo-status-api-config 3 5m56s
configmap/kube-root-ca.crt 1 5m57s
NAME TYPE DATA AGE
secret/andes-cargo-status-api-secrets Opaque 2 5m56s
HorizontalPodAutoscalershowscpu: <unknown>/50%because this cluster was recreated in lesson 4 andmetrics-server(Module 3, lesson 7) hasn't been reinstalled yet — the HPA itself remains configured and working, just with no metrics to read yet. This doesn't affect anything this project verifies;IngressandNetworkPolicydon't depend onmetrics-serverat all.
Nine object types, one single namespace: Module 2-3's Deployment/Service/HPA, Module 3's ConfigMap/Secret, and this module's Ingress/two NetworkPolicys — this guide's complete, cumulative state as of today.
Step 2 — Verify the allowed path: Ingress → Service
curl -i -s --max-time 8 --resolve andes-cargo.local:80:127.0.0.1 http://andes-cargo.local/health
What to expect (literal, executed — Date is your variable value):
HTTP/1.1 200 OK
Date: Fri, 14 Aug 2026 20:42:41 GMT
Content-Type: application/json
Content-Length: 51
Connection: keep-alive
{"service":"andes-cargo-status-api","status":"ok"}
200, with no kubectl port-forward, exactly as lesson 5 confirmed — and now, on top of that, with default-deny-ingress and allow-from-ingress-nginx both active at the same time. This result is proof the module's two halves coexist with no conflict: the HTTP door stays open for legitimate traffic, even with the network guardrail active.
Also confirm, once again and with the same honesty as always, the limit this module never promised to resolve:
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 — unchanged from lesson 5; still not an error in this project):
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>
Step 3 — Verify the blocked path: any other Pod → Service directly
kubectl exec -n andes-cargo test-client -- curl -s -o /dev/null -w "HTTP_STATUS:%{http_code}\n" --max-time 8 http://status-api-service.andes-cargo.svc.cluster.local/health
What to expect (literal, executed — the same test-client from lesson 7, still running):
HTTP_STATUS:000
command terminated with exit code 28
Blocked — for real, with the same mechanism lesson 7 investigated in depth. No Pod not coming from the ingress-nginx namespace can reach status-api-service, no matter that it's in the same namespace as Andes Cargo, no matter that it uses the correct DNS name, no matter what — except the single explicit exception you declared.
The visual summary: both paths, side by side
MODULE 4'S FINAL STATE, VERIFIED
External client (curl, your machine)
│
│ --resolve andes-cargo.local:80:127.0.0.1
▼
┌─────────────────┐
│ Ingress │ status-api-ingress
│ (extraPortMappings │ host: andes-cargo.local
│ → control-plane) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ ingress-nginx │ namespace: ingress-nginx
│ controller │
└────────┬────────┘
│ allowed by allow-from-ingress-nginx
▼
┌─────────────────┐ test-client
│ status-api- │◄── X ── (namespace: andes-cargo,
│ service │ BLOCKED bypassing Ingress)
│ (andes-cargo- │ by default-deny-ingress
│ status-api Pods) │
└─────────────────┘
/health → 200 OK (both paths verified)
/shipments/<id> → 500 (DynamoDB backend outside $0 scope —
representative throughout this guide)
Module 4's final checklist
Three verifications, all confirmed by this project, none hypothetical:
-
ingress-nginxinstalled and healthy, running onandes-cargo-cluster-control-plane(the only node withextraPortMappings), confirmed withkubectl get pods -n ingress-nginx. -
status-api-ingressroutes real traffic, with nokubectl port-forward, confirmed withcurlagainsthttp://andes-cargo.local/healthresponding200. -
default-deny-ingress+allow-from-ingress-nginxblock and allow exactly what's expected, verified in both directions: theIngresspath works, any other Pod stays blocked — with lesson 7's honest correction aboutkindnetddocumented and cited.
Note for Module 5: what GitOps inherits from this state
Module 5 is going to take namespace.yaml, deployment.yaml, service.yaml, configmap.yaml, secret.yaml, hpa.yaml, ingress.yaml, and this module's two NetworkPolicys, and hand them over to ArgoCD as a Git repository's source of truth — the same cumulative pattern you already saw in every previous module, now with eight manifests instead of six. Two things are worth noting before getting there:
- A real DynamoDB backend (LocalStack or another option), if some team added it, wouldn't collide with
default-deny-ingress. This lesson's policy hasmetadata.namespace: andes-cargo— it only affects Pods in that namespace. LocalStack would live in its own namespace (localstack), completely outside thisNetworkPolicy's scope. This guide doesn't install that piece in any module — the data layer stays outside its $0 scope — but it's worth noting that this module's network architecture wouldn't put up any obstacle to it. andes-cargo-status-apicould reach that backend with no change to this module's policies. Both of this module'sNetworkPolicys only declarepolicyTypes: [Ingress]— explicitly confirmed in lesson 7'skubectl describe(Not affecting egress traffic) — soandes-cargo-status-api's outgoing connection toward any external service, if one existed, wouldn't run into any new restriction from this module.
Cleanup: remove the test Pod
test-client served its purpose (lessons 7 and 8) and isn't part of the lab's permanent state — unlike andes-cargo-status-api, ingress-nginx, or the two NetworkPolicys, which remain necessary for the rest of the guide:
kubectl delete pod test-client -n andes-cargo
What to expect:
pod "test-client" deleted
Common mistakes
Considering the module closed without running this project's verifications, trusting "each lesson already worked separately" (workflow, the same pattern you already saw in Module 1). What happens: someone assumes that, because lessons 5 and 7 already showed correct results separately, there's no need to confirm both pieces still work together. How to spot it: if you can't paste, right now, the real output of this project's Steps 2 and 3. How to fix it: that's exactly this project's reason for being — confirming Ingress and NetworkPolicy, applied in different lessons, don't interfere with each other. Repeating the verification with both active at the same time isn't redundant.
Deleting default-deny-ingress or allow-from-ingress-nginx "to simplify" before continuing to Module 5 (operational, with real consequences for the guide's continuity). What happens: someone, thinking the NetworkPolicys already served their pedagogical purpose in this module, deletes them before moving on. How to spot it: kubectl get networkpolicy -n andes-cargo shows neither object. How to fix it: both policies are part of the lab's permanent state from here on — Module 5 inherits them as-is, as part of the Git repository ArgoCD syncs. If you deleted them, reapply networkpolicy-default-deny.yaml and networkpolicy-allow-ingress-nginx.yaml (lesson 7) before continuing.
Not noticing the HPA shows <unknown> after recreating the cluster, and assuming something broke (continuity, already explained in Step 1 but easy to be alarmed by if seen with no context). What happens: someone sees cpu: <unknown>/50% in kubectl get hpa and worries autoscaling stopped working. How to spot it: if you compare it against Module 3's result, where a real percentage did show up. How to fix it: recreating the cluster in this module's lesson 4 didn't reinstall metrics-server — out of this module's scope — the HPA itself remains correctly configured and is going to show real metrics again once metrics-server is available again. No later module in this guide depends on this.
Exercises
Exercise 1 — Reconstruct the final-state diagram from memory. Without going back to the lesson, draw (in text, with arrows) the complete path from curl on your machine to an andes-cargo-status-api Pod, marking where each piece of this module (Ingress, ingress-nginx, NetworkPolicy) intervenes.
See solution
curl (your machine) → extraPortMappings (kind-config.yaml, toward control-plane) → ingress-nginx (reads the Ingress rule) → status-api-service → NetworkPolicy evaluates the origin (namespace ingress-nginx, allowed by allow-from-ingress-nginx) → andes-cargo-status-api Pod. A Pod attempting the same path while bypassing ingress-nginx reaches the NetworkPolicy step, and stops there — default-deny-ingress blocks any origin not matching the explicit exception.
Exercise 2 — Explain why an external data backend wouldn't need any change to these policies. A colleague asks whether, in case andes-cargo-status-api connected to a real DynamoDB backend (via LocalStack or a real AWS account — something this guide doesn't do, but a real team could add), default-deny-ingress or allow-from-ingress-nginx would need modifying. Explain to them, in two or three sentences, why not.
See solution
A reasonable explanation: "Neither policy touches outgoing traffic — both declare only policyTypes: [Ingress], explicitly confirmed with kubectl describe in lesson 7 (Not affecting egress traffic). The connection toward an external backend is a connection andes-cargo-status-api initiates outward, not one it receives — that kind of traffic is completely outside what this module restricted."
Exercise 3 — Design the verification for a third path. If Andes Cargo added, in the future, a third namespace (monitoring, with an observability tool that needs to curl /health to check the service is alive), which new policy would you need, and which verification — following this project's pattern — would confirm it works without opening the door to anything else?
See solution
You'd need a third NetworkPolicy (or an additional rule inside allow-from-ingress-nginx, though splitting it into its own object is clearer) with podSelector: app: andes-cargo-status-api and a from rule with namespaceSelector: kubernetes.io/metadata.name: monitoring, restricted to port 8080 — the exact same pattern as allow-from-ingress-nginx, with the source namespace changed. The verification would follow this project's same two-step pattern: a test Pod inside monitoring confirming 200 against status-api-service, and test-client (or any Pod outside ingress-nginx/monitoring) confirming it's still blocked — proof the new exception didn't open the door any wider than necessary.
Summary and next step
This project confirmed, with real evidence and both halves active at the same time, Module 4's complete state: Ingress routing real external traffic toward status-api-service (200 on /health, with no kubectl port-forward), and NetworkPolicy completely blocking any path bypassing that door (test-client, blocked with literal evidence), while the legitimate path keeps working with no friction. As of today, andes-cargo-status-api is a service with a permanent HTTP door and a real network guardrail — the two pieces "3 balanced replicas" (Module 2) and "configuration/health/autoscaling" (Module 3) still didn't cover.
Before moving on you should be able to: run the complete checklist from memory; explain why an external data backend, if some team added it, wouldn't need any change to this module's NetworkPolicys; and describe, using this lesson's diagram, a real HTTP request's complete path from your machine to a Pod, including the two network security pieces protecting it.
Next module: GitOps with ArgoCD. Module 5 takes exactly the eight manifests that exist today — namespace.yaml, deployment.yaml, service.yaml, configmap.yaml, secret.yaml, hpa.yaml, ingress.yaml, and the two NetworkPolicys — and turns them into a real Git repository's source of truth, served by Gitea inside the cluster itself, synced by an operator that watches instead of waiting for someone to run kubectl apply by hand.
Resources
kubernetes-and-eks-in-production-guide(NIEVA), Module 4, lessons 4, 5, and 7 — the exact origin of every manifest and every verification this project reuses unchanged.- Kubernetes — Debug Services — official connectivity diagnostics guide, useful as a general reference for any network problem you encounter beyond this guide's scope.
- Kubernetes — Ingress and Kubernetes — Network Policies — this whole module's two central references, both confirmed with real evidence in this project.