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

HorizontalPodAutoscaler shows cpu: <unknown>/50% because this cluster was recreated in lesson 4 and metrics-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; Ingress and NetworkPolicy don't depend on metrics-server at 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: IngressService

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-nginx installed and healthy, running on andes-cargo-cluster-control-plane (the only node with extraPortMappings), confirmed with kubectl get pods -n ingress-nginx.
  • status-api-ingress routes real traffic, with no kubectl port-forward, confirmed with curl against http://andes-cargo.local/health responding 200.
  • default-deny-ingress + allow-from-ingress-nginx block and allow exactly what's expected, verified in both directions: the Ingress path works, any other Pod stays blocked — with lesson 7's honest correction about kindnetd documented 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 has metadata.namespace: andes-cargo — it only affects Pods in that namespace. LocalStack would live in its own namespace (localstack), completely outside this NetworkPolicy'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-api could reach that backend with no change to this module's policies. Both of this module's NetworkPolicys only declare policyTypes: [Ingress] — explicitly confirmed in lesson 7's kubectl describe (Not affecting egress traffic) — so andes-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

  1. 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.
  2. Kubernetes — Debug Services — official connectivity diagnostics guide, useful as a general reference for any network problem you encounter beyond this guide's scope.
  3. Kubernetes — Ingress and Kubernetes — Network Policies — this whole module's two central references, both confirmed with real evidence in this project.