Networking, Ingress & storage
Two pillars every real app needs. First networking: how Pods reach each other, how cluster DNS works, how Ingress (plus an ingress controller) puts one HTTPS front door in front of many Services, and how NetworkPolicies firewall Pod-to-Pod traffic. Then storage: why Pods are ephemeral, how PersistentVolumes, PersistentVolumeClaims, and StorageClasses give durable disks, and when you need a StatefulSet instead of a Deployment.
Learning objectives
- Describe the cluster networking model: every Pod gets an IP, all Pods can reach each other.
- Use cluster DNS to resolve a Service by name across namespaces.
- Put an Ingress + ingress controller in front of multiple Services with host/path routing and TLS.
- Restrict traffic with a NetworkPolicy (default-deny then allow).
- Give a workload durable storage with PV / PVC / StorageClass, and know when to use a StatefulSet.
30-second recap
- Pods, Deployments & Services: A Pod runs containers, a Deployment keeps N identical Pods alive and rolls out updates, a Service gives them one stable address.
1 · The cluster networking model
Picture the moment your app first fails in a way that makes no sense. Your frontend Pod is running, your backend Pod is running, both are green in kubectl get pods — and yet the frontend can't reach the backend. You spend an hour suspecting your code before someone asks the real question: "how is one Pod even supposed to find another?" That question — networking — is invisible until it breaks, and then it's the only thing that matters. This section gives you the mental model that turns those mysteries into two-minute diagnoses.
Here's the analogy that makes it click. Think of your cluster as an apartment building where every tenant (Pod) gets their own phone line with a direct number, and any tenant can dial any other tenant with no operator in between. That's the founding promise of Kubernetes networking: a flat, fully-connected space. There's no "wall" between Pods by default, no address translation to untangle — Pod A can talk to Pod B as if they were on the same LAN, even when they're on physically different machines across a datacenter.
The common mistake is assuming that because you're used to Docker on one laptop — where containers share a host and you juggle port mappings — Kubernetes works the same way. It doesn't. There's no -p 8080:80 port-forwarding tax between Pods; each Pod has a real, routable IP. The catch that trips people is the opposite of what they expect: connectivity is too open by default (any Pod can reach any Pod), which is exactly why §4's NetworkPolicies exist to lock it back down.
Kubernetes networking has one founding rule: every Pod gets its own IP, and every Pod can reach every other Pod directly — no NAT between Pods. A CNI plugin (Calico, Cilium, the AWS VPC CNI in EKS) implements this. On top of it sit the abstractions from K2: a Service gives a stable virtual IP, and kube-proxy (or eBPF) makes that IP load-balance to Pod IPs.
Follow the single chain of arrows left-to-right: this is the path one outside request takes to reach your code.
- The leftmost blue box is the Internet — an external client that lives outside the cluster.
- Each arrow is one hop inward: the request first hits the ingress controller (purple), which hands it to a Service (ClusterIP) (amber), the stable virtual IP from K2.
- The rightmost green box is the payoff — the Pods that actually run your app. The Service is what load-balances the request across them, so the client never talks to a Pod directly.
In short: This is the north-south path — client → ingress controller → Service → Pods — the one route external traffic uses to get in.
2 · Cluster DNS
Every flat network in §1 hides a problem: IPs don't hold still. A Pod that gets rescheduled to another node comes back with a brand-new IP; scale a Service from 3 Pods to 10 and there are seven addresses that didn't exist a second ago. If your frontend had hardcoded 10.1.4.7 for the backend, it now points at nothing — or worse, at some unrelated Pod that inherited that address. Something has to translate a stable name into whatever IPs are live right now. That something is cluster DNS.
The analogy is a company phone directory. You don't memorize your colleague's desk-phone number and re-memorize it every time they change desks — you look up "Priya in Payments" and the directory returns wherever she sits today. Cluster DNS is that directory for Services: you ask for backend.payments by name, and it hands back the Service's current virtual IP, which then load-balances across whatever Pods are healthy. The name is the contract; the addresses underneath are free to churn.
The mistake to name is the reflex to debug with raw IPs. When something can't connect, it's tempting to kubectl get pod -o wide, copy a Pod IP, and curl it directly to "prove" the network works. That test lies to you: it might pass this second and fail the next when the Pod moves, and it teaches you nothing about the path real traffic takes. The professional habit is to always address Services by their DNS name and treat a name-resolution failure — not an IP ping — as the signal. The naming pattern below is worth burning into memory, because the same rules explain both "why does the short name work here but not there?" and most cross-namespace connectivity bugs.
Every Service gets a DNS name automatically, so code never hardcodes IPs. The pattern is <service>.<namespace>.svc.cluster.local. Within the same namespace you can use just the short <service> name.
Follow the arrows left-to-right: a client Pod asks for a name, DNS answers with a stable address, and that address fans out (dashed) to the live Pods.
- The left blue box is your code — it holds only a name,
backend.payments, never an IP. - The purple box is cluster DNS (CoreDNS): it translates that name into the Service's fixed virtual IP (the amber box).
- The dashed arrows to the green Pods are the payoff: the Service load-balances across whatever Pod IPs are healthy right now. Those Pod IPs (.7, .9, .3) change on every reschedule — but the name and the Service VIP above them stay put.
In short: Address Services by DNS name and the moving Pod IPs beneath become someone else's problem.
dns.sh# same namespace -> short name works:
curl http://backend/healthz
# cross namespace -> fully-qualified:
curl http://backend.payments.svc.cluster.local/healthz
# see the DNS records a Service exposes:
kubectl run tmp --rm -it --image=busybox -- nslookup backend.payments.svc.cluster.local
backend.payments.svc.cluster.local (or a short name in-namespace) and the cluster handles the moving IPs beneath. This is why the frontend in K2 could hardcode a URL and still survive backend Pods being rescheduled.3 · Ingress and ingress controllers
Imagine giving every service in your company its own street address and its own front-desk receptionist. Ten microservices, ten public IPs, ten cloud load balancers, ten monthly bills, ten TLS certificates to renew. That's what happens if you expose each Service as a LoadBalancer, and it gets absurd fast. What you actually want is one grand entrance to the building, with a receptionist who reads where a visitor is headed and points them down the right hallway. That single front door is an Ingress.
Concretely, an Ingress is a set of routing rules: "requests for app.example.com/api go to the backend Service; everything else on that host goes to the frontend Service; and by the way, terminate HTTPS here so the internal Services can speak plain HTTP." It's Layer 7 — it understands hostnames and URL paths, not just IPs and ports — which is why it can fan one address out to many backends by reading the request. One load balancer, one certificate, many services behind it.
Now the mistake that costs people an entire afternoon, and it's important enough that this section returns to it twice more. An Ingress object is just a document. Writing the rules and running kubectl apply creates the spec, but nothing reads it and nothing routes traffic — because the thing that does the routing is a separate program called an ingress controller (NGINX, Traefik, or the AWS Load Balancer Controller) that you install yourself and that watches for Ingress objects to fulfill. The Ingress is the receptionist's instructions; the controller is the actual receptionist. Apply an Ingress on a cluster with no controller and you get eerie silence: the object exists, kubectl get ingress shows it, and not one byte reaches your Pods. Wire the two together with a matching ingressClassName and it springs to life.
A LoadBalancer Service gives one external IP per service — expensive and unwieldy. An Ingress is a single HTTP(S) entry point that routes by host and path to many backend Services, and terminates TLS. Crucially, an Ingress object is inert without an ingress controller (NGINX, Traefik, or the AWS Load Balancer Controller) actually running to fulfill it.
app.example.com/api vs / out to different Services.
Read left-to-right: traffic enters once, then splits by URL path to the right backend.
- The blue Internet box is every external client; one solid arrow means one entry point, not one-per-service.
- The purple box is the ingress controller — the running program that terminates TLS once and inspects each request's host and path.
- The two branching arrows route by path:
/apito the green backend Service, everything else (/) to the green frontend Service — Layer-7 routing that a plain IP load balancer can't do. - The amber strip below, joined by a dashed arrow, is the payoff-and-trap: the Ingress object is only rules; without a controller whose
ingressClassNamematches, nothing reads it and no traffic flows.
In short: The Ingress writes the rules; the controller enforces them — no controller, eerie silence.
ingress.yamlapiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
annotations:
# controller-specific annotations go here; verify against your controller's docs
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx # which controller fulfills this
tls:
- hosts: [app.example.com]
secretName: web-tls # a TLS Secret (e.g. from cert-manager)
rules:
- host: app.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service: { name: backend, port: { number: 80 } }
- path: /
pathType: Prefix
backend:
service: { name: frontend, port: { number: 80 } }
helm install NGINX, or the AWS Load Balancer Controller in K5) and set ingressClassName to match it. Annotations are controller-specific; verify each against your controller's current docs rather than copying blindly.4 · NetworkPolicies
Here's the uncomfortable consequence of §1's flat network. If any Pod can reach any Pod, then the day an attacker slips into your least important Pod — a public-facing frontend, a scrappy little metrics sidecar — they can open a connection straight to your database and start reading rows. Nothing in the default cluster stops them. Most breaches aren't a straight line to the crown jewels; they're a foothold in something trivial followed by a quiet walk across an open network. NetworkPolicies are how you close those hallways.
Think of it as the difference between an open-plan office where anyone can wander into any room, and a building with badge readers on every door. A NetworkPolicy is a Pod-level firewall: you select a group of Pods by their labels and declare who is allowed to connect to them and on which ports. "Only Pods labelled app: backend may reach Pods labelled app: db, and only on 5432" is a sentence you can write directly as policy.
The subtlety that catches everyone is how you get to least-privilege. The intuitive move is to add "allow" rules for the traffic you want — but on a flat network, allow-rules alone change nothing, because everything is already allowed. The correct pattern is inverted: first apply a default-deny that shuts all inbound traffic to a namespace, which flips the network from open to closed, and only then punch specific holes for the connections you actually need. Deny first, allow narrowly — the same instinct as a firewall's default-drop rule. There's also a quieter trap the callout below dwells on: a policy is only real if your CNI plugin enforces it. On a cluster whose CNI ignores NetworkPolicy, you can apply perfect-looking rules and feel secure while nothing is actually blocked — the worst kind of failure, because it's invisible.
By default, every Pod can talk to every other Pod — a flat network. In production you want least-privilege: a database should only accept connections from its app. A NetworkPolicy is a Pod-level firewall selected by labels. The standard pattern is default-deny, then explicitly allow what's needed.
Compare the two panels split by the dashed line: same three Pods, opposite security posture.
- The left panel is the default flat network. Both the backend (blue, legitimate) and the red frontend can reach the amber
db— the red arrow is the attack path a compromised frontend walks. - The right panel applies default-deny first, which is why the
dbbox turns green (protected). The one blueallowarrow re-opens exactly backend→db on 5432. - The red crossed-out arrow on the right is the payoff: the frontend's path is now blocked, because deny-first closed everything and only the named connection was reopened.
In short: Allow-rules do nothing on an open network — you must shut everything with default-deny, then punch one hole.
netpol.yamlapiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
spec:
podSelector: {} # all Pods in this namespace
policyTypes: [Ingress] # deny all inbound by default
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-allow-app
spec:
podSelector: { matchLabels: { app: db } } # applies to db Pods
policyTypes: [Ingress]
ingress:
- from:
- podSelector: { matchLabels: { app: backend } } # only backend may connect
ports:
- { protocol: TCP, port: 5432 }
5 · Persistent storage — PV, PVC, StorageClass
The first time this bites is unforgettable. You run a database in a Pod, load it with data, everything works — then the Pod restarts (a node reboot, a rollout, an OOM kill) and your data is gone. Not corrupted: gone, as if you'd never written it. That's not a bug; it's the design. A Pod's container filesystem is scratch space, wiped clean on every restart, because Kubernetes assumes Pods are cattle you can kill and recreate at will. Anything you want to keep has to live somewhere the Pod's death can't touch.
The mental model is a hotel room versus a storage unit. The Pod is a hotel room — you get a fresh, clean one each stay, and whatever you leave on the desk when you check out is thrown away. Persistent storage is the rented storage unit down the street: it stays yours across check-ins, and you mount it into today's room. When the Pod restarts and a new one takes its place, that new Pod can mount the same storage unit and pick up exactly where the last one left off.
Now the three-word vocabulary people constantly muddle — PV, PVC, StorageClass — because they sound interchangeable and aren't. The clean way to hold them apart is by who owns each. A PersistentVolume (PV) is the actual disk — a real EBS volume, a real NFS export — the physical storage unit itself. A PersistentVolumeClaim (PVC) is your app's request for storage ("I need 20Gi I can write to"); the Pod only ever names a PVC, never a raw PV, so the app stays ignorant of the underlying disk. A StorageClass is the menu and the vending machine: it says "class gp3 means an AWS gp3 SSD" and, when a claim comes in, automatically provisions a matching PV to satisfy it. The claim is the order slip, the StorageClass is the fulfillment engine, the PV is the goods. Keep that ownership split straight and the YAML below reads itself; blur it and every storage bug looks like magic.
A Pod's container filesystem is ephemeral: restart the Pod and it's wiped. For data that must survive — a database, uploaded files, a model cache — you need a PersistentVolume (PV), a piece of durable storage. Pods don't reference PVs directly; they make a PersistentVolumeClaim (PVC) — a request for storage — and a StorageClass dynamically provisions a matching PV (e.g. an EBS volume on AWS).
Follow the arrows left-to-right: this is how a Pod ends up with a durable disk without ever naming the disk itself.
- The leftmost blue box is the Pod — your workload. It only ever points at a claim, never at real storage.
- The middle boxes are the request chain: the Pod mounts a PVC (purple, "a request"), which a StorageClass (amber, "provisioner") reads and fulfills.
- The rightmost green box is the payoff — the PV ("real disk"), the actual durable volume (e.g. an EBS disk) that the StorageClass provisions to satisfy the claim.
In short: The Pod asks via a PVC, the StorageClass provisions a matching PV — so the app stays ignorant of the underlying disk.
pvc.yamlapiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: model-cache
spec:
accessModes: [ReadWriteOnce] # one node mounts it read-write
storageClassName: gp3 # a StorageClass in the cluster (e.g. EBS gp3)
resources:
requests:
storage: 20Gi
---
# ...in a Pod/Deployment container spec:
# volumeMounts: [ { name: cache, mountPath: /models } ]
# spec.volumes:
# - name: cache
# persistentVolumeClaim: { claimName: model-cache }
6 · StatefulSets — when identity matters
Try to run a 3-replica database as an ordinary Deployment and you'll produce a catastrophe that looks fine for a week. A Deployment says "give me three copies of this Pod, I don't care which is which" — the Pods get random names, come and go interchangeably, and can share or lose storage freely. That's perfect for stateless web servers and exactly wrong for a database, where three processes suddenly writing to one disk (or disagreeing about who's the primary) quietly corrupt each other's data. The knowledge check earlier in this page is precisely that story. StatefulSets exist because some workloads simply cannot be anonymous and interchangeable.
Think of the difference between temp workers and named employees. A Deployment's Pods are temps from an agency: any one will do, they're numbered by whoever's on shift, and you don't hand a temp the keys to a specific locked cabinet. A StatefulSet's Pods are named employees: db-0 is always db-0, it always comes back to the same desk with the same locked drawer (its own PVC), and it has a stable phone extension others can rely on. When db-0 restarts, it returns as db-0 attached to its original disk — identity and storage preserved — which is the whole point.
The mistake goes in both directions, so hold two rules at once. Reaching for a StatefulSet by default "to be safe" is over-engineering — most of your workloads are genuinely stateless and belong in a simpler, faster-scaling Deployment; a StatefulSet's ordered, one-at-a-time startup is a real cost you shouldn't pay without reason. But forcing a stateful workload into a Deployment is the dangerous error, the data-corrupting one. The honest test is a single question: does each replica need a stable name and its own durable disk? Databases, Kafka brokers, and clustered stores answer yes; almost everything else answers no. And the most senior answer of all, which the tip below and Exercise 5 both push, is often to dodge the question entirely — run the database as a managed service and keep your cluster blissfully stateless.
A Deployment's Pods are interchangeable and get random names. Some workloads — databases, message brokers, anything where each replica has a stable identity and its own persistent disk — need more. A StatefulSet gives each Pod a stable ordinal name (db-0, db-1), a stable network identity, and its own PVC, created in order.
Compare the two panels either side of the dashed line: same idea of "three replicas", two very different guarantees.
- The left panel is a Deployment: three blue Pods with random names (
web-7d9f…) whose dashed lines all point at one shared or throwaway disk — any Pod is as good as any other. - The right panel is a StatefulSet: three green Pods with stable ordinal names (
db-0,db-1,db-2), each with a solid arrow to its own purple PVC (pvc-0…). - The payoff is in the captions: the left starts all at once and forgets which is which; the right preserves identity and per-Pod disk and starts in order (0→1→2) — the difference between safe stateless web servers and a database that won't corrupt itself.
In short: Need a stable name and its own durable disk per replica? StatefulSet. Otherwise, a Deployment.
| Deployment | StatefulSet | |
|---|---|---|
| Pod names | Random (web-7d9f-abc) | Stable ordinals (db-0, db-1) |
| Storage | Usually shared/none | One PVC per Pod, kept across restarts |
| Startup/scaling | All at once | Ordered (0, then 1, …) |
| Use for | Stateless apps, web, APIs | Databases, brokers, clustered stateful apps |
✓ Checkpoint — you can move on when you can…
- State the cluster networking rule and the difference between north-south and east-west traffic.
- Resolve a Service in another namespace by its fully-qualified DNS name.
- Route two paths on one host to two Services via an Ingress, and explain why a controller is required.
- Write a default-deny NetworkPolicy plus one allow rule, and name the CNI caveat.
- Explain PV vs PVC vs StorageClass, and when a StatefulSet beats a Deployment.
You apply a perfectly valid Ingress manifest routing app.example.com to your Services, but nothing is reachable and no external address appears. The Services work fine via kubectl port-forward. What's missing?
Show answer
ingressClassName. Install a controller, set the class, and it will provision the external load balancer and program the routes. (Also verify DNS for the host points at that load balancer.)A team runs PostgreSQL as a plain Deployment with a shared PVC and 3 replicas, and data keeps getting corrupted. Why is this the wrong shape, and what should they use?
Show answer
db-0, db-1) gets its own PVC and stable name, started in order. Better still for most teams: run the database as a managed service (e.g. RDS) and keep the cluster stateless, avoiding the operational burden of running a database on Kubernetes at all.🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: Cluster DNS is how services find each other without hardcoding IPs; using it correctly across namespaces is a daily skill.
Your task: Given a backend Service in the payments namespace, give the URLs a client would use from the same namespace and from a different one.
Requirements:
- Show the short-name form for same-namespace access
- Show the fully-qualified form for cross-namespace access
- State why you'd never hardcode the Pod IP instead
💡 Hint: The pattern is <svc>.<ns>.svc.cluster.local.
Show solution
Same namespace (payments): the short name resolves — http://backend/healthz.
Different namespace: use the fully-qualified name — http://backend.payments.svc.cluster.local/healthz.
Why not the Pod IP: Pod IPs change every time a Pod is rescheduled, and there are N of them behind the Service. The Service's DNS name is stable and load-balances across whatever Pods currently match its selector — so code stays correct while Pods churn.
Context: An Ingress consolidates many services behind one HTTPS front door — the standard way to expose an app.
Your task: Write an Ingress that routes /api to a backend Service and / to a frontend Service on one host, with TLS, and name the prerequisite.
Requirements:
- Host-based rule with two path prefixes
- TLS via a secret
- Set ingressClassName and name the required controller
💡 Hint: The Ingress is inert without a controller that matches its class.
Show solution
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: { name: web }
spec:
ingressClassName: nginx
tls: [ { hosts: [app.example.com], secretName: web-tls } ]
rules:
- host: app.example.com
http:
paths:
- { path: /api, pathType: Prefix, backend: { service: { name: backend, port: { number: 80 } } } }
- { path: /, pathType: Prefix, backend: { service: { name: frontend, port: { number: 80 } } } }Prerequisite: an ingress controller (here NGINX) must be installed and its class must match ingressClassName: nginx; otherwise the object exists but no traffic flows. The TLS secret is typically issued automatically by cert-manager.
Context: A flat cluster network means any compromised Pod can reach your database; a default-deny policy is the fix.
Your task: Write NetworkPolicies so that the db Pods accept connections only from backend Pods on port 5432, and nothing else.
Requirements:
- A namespace default-deny for ingress
- An allow rule scoped to the db Pods and the backend source
- Name the CNI prerequisite
💡 Hint: Default-deny first, then add the single allow — order and scope both matter.
Show solution
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny-ingress }
spec: { podSelector: {}, policyTypes: [Ingress] }
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: db-allow-backend }
spec:
podSelector: { matchLabels: { app: db } }
policyTypes: [Ingress]
ingress:
- from: [ { podSelector: { matchLabels: { app: backend } } } ]
ports: [ { protocol: TCP, port: 5432 } ]How it composes: the default-deny drops all inbound to every Pod in the namespace; the second policy re-opens exactly one path — backend → db on 5432. Anything else (a compromised frontend, another namespace) is denied.
Prerequisite: the cluster's CNI must enforce NetworkPolicy (Calico/Cilium do). On a non-enforcing CNI these rules are decorative — a dangerous false sense of security.
Context: Ephemeral Pod filesystems lose data on restart; persistent storage is required for anything stateful.
Your task: Attach a 20Gi durable volume to a container at /models, and explain the PVC → StorageClass → PV chain and the access-mode choice.
Requirements:
- Write the PVC and the volume mount
- Explain what the StorageClass does
- Justify ReadWriteOnce vs ReadWriteMany for this case
💡 Hint: The PVC is a request; the StorageClass provisions the real PV to satisfy it.
Show solution
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: model-cache }
spec:
accessModes: [ReadWriteOnce]
storageClassName: gp3
resources: { requests: { storage: 20Gi } }Mount it in the container:
volumeMounts: [ { name: cache, mountPath: /models } ]
volumes:
- name: cache
persistentVolumeClaim: { claimName: model-cache }The chain: the Pod mounts the PVC (a request for 20Gi); the StorageClass gp3 dynamically provisions a real PV (e.g. an EBS volume) to satisfy it, and binds the two. Access mode: ReadWriteOnce is right here — a per-Pod model cache mounted by one node. You'd only need ReadWriteMany (and EFS/NFS-class storage) if many Pods on different nodes had to write the same volume simultaneously.
Context: Picking the wrong workload primitive for stateful data causes corruption and pain; the professional call is often 'don't run it in-cluster at all'.
Your task: For three workloads, choose Deployment, StatefulSet, or a managed external service, and justify each.
Requirements:
- Workload A: a stateless REST API
- Workload B: a self-managed Kafka cluster
- Workload C: the primary relational database
- Justify each choice in one or two lines
💡 Hint: Ask: is it stateless? does each replica need identity + its own disk? is a managed service available?
Show solution
A — stateless REST API → Deployment. Pods are interchangeable, no per-replica identity or disk needed. This is the default and simplest case.
B — self-managed Kafka → StatefulSet. Each broker needs a stable identity (kafka-0…), its own persistent log volume, and ordered startup — exactly what a StatefulSet provides. (Even so, a managed Kafka/MSK is worth considering to avoid the operational load.)
C — primary relational database → managed service (RDS/Cloud SQL). Running a primary database on Kubernetes is possible via a StatefulSet + operator, but backups, failover, and upgrades are hard to get right. The professional default is to keep the cluster stateless and let a managed database own durability — the app connects out to it. Choose in-cluster only with a strong reason and a database operator you trust.
Context: Representative scenario: you're the platform engineer standing up a customer-facing web app with a public frontend, an internal API, an internal cache, and a database.
Your task: Produce the end-to-end topology: what's exposed via Ingress, what stays ClusterIP, where NetworkPolicies sit, and how each stateful piece gets storage — with justifications.
Requirements:
- State which component the Ingress fronts and which stay internal
- Define the NetworkPolicy posture (default-deny + specific allows)
- Decide storage/primitive for the cache and the database
- Note the DNS names services use to reach each other
💡 Hint: Only the frontend is north-south; everything else is east-west behind ClusterIP + policy.
Show solution
North-south (public) entry: one Ingress (with an ingress controller and TLS) fronts only the frontend Service on app.example.com. Everything else stays ClusterIP, reachable only inside the cluster.
East-west wiring (by DNS): frontend → api.web.svc.cluster.local; api → cache.web.svc.cluster.local and → the database endpoint. No component hardcodes an IP.
NetworkPolicy posture: namespace default-deny ingress, then explicit allows: frontend→api, api→cache (6379), api→db (5432). A compromised frontend still can't reach the database directly. (Requires an enforcing CNI.)
Storage / primitives: the cache (Redis) can be a StatefulSet with a small per-Pod PVC, or — if it's purely a cache — an ephemeral Deployment that can be lost safely. The database is a managed service (RDS): the cluster stays stateless, durability and backups are RDS's job, and the api reaches it over its endpoint. If policy forces it in-cluster, use a StatefulSet with per-Pod PVCs and a database operator.
Result: one public door, least-privilege east-west traffic, and state pushed to durable/managed storage — the standard production shape you'll refine with EKS specifics in K5.