AI EngineeringZero to ProductionHome·About·What’s new·Contact
Kubernetes Orchestration · Part 4

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.

⏱️ ~95 min☸️ Networking + state🎯 Intermediate→Advanced
🌱 The two questions this answers"How does traffic get to my Pods?" and "Where does data that must survive a Pod restart live?" All YAML needs a cluster; Ingress needs an ingress controller installed (NGINX, or the AWS Load Balancer Controller in K5), and dynamic storage needs a StorageClass / CSI driver. Output is illustrative — verify Ingress and storage API fields against the current Kubernetes docs, which change more than most.

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.
📋 Before you startThis page builds on: Pods, Deployments & Services. Jumping in? Skim those first — or open the recap.
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.

Internet Ingresscontroller Service(ClusterIP) Pods
🗺️ How to read this diagram

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.

North-south vs east-westEast-west traffic (Pod ↔ Pod, service ↔ service inside the cluster) rides ClusterIP Services and cluster DNS. North-south traffic (client ↔ cluster) enters through a LoadBalancer or, better, an Ingress. Most of your services stay ClusterIP; only the few that face users get an Ingress route.

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.

client Pod backend.payments ask by name cluster DNS (CoreDNS) stable VIP Service ClusterIP (fixed) Pod .7 Pod .9 Pod .3 Pod IPs churn — the name never does code holds only the name → The name is the contract; the IPs churn beneath it. A Pod asks DNS for a Service name, gets back the Service's fixed virtual IP, and that IP load-balances across whatever Pods happen to be alive — so rescheduling never breaks a caller.
🗺️ How to read this diagram

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.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
shell · resolve Services by DNS (needs a cluster)
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
Config by name, never by IPPod and Service IPs change; DNS names don't. Point your app at 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.

HTTPS client Internet Ingress controller one LB · terminates TLS reads host + path /api / backend Service frontend Service Ingress object = rules onlyinert until a controller's ingressClassName matches One door, many hallways — but only if a controller is listening. The Ingress object is just routing rules; the running ingress controller is what terminates TLS and fans app.example.com/api vs / out to different Services.
🗺️ How to read this diagram

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: /api to 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 ingressClassName matches, nothing reads it and no traffic flows.

In short: The Ingress writes the rules; the controller enforces them — no controller, eerie silence.

YAML · host + path routing with TLS (needs a cluster + ingress controller)
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 } }
No controller = nothing happensApplying an Ingress with no controller installed silently does nothing — the object exists but no traffic flows. Install a controller first (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.

before · flat network (all allowed) frontend backend db :5432 compromised frontend reaches db ✗ after · default-deny + one allow frontend backend db :5432 allow deny-first blocks it; only backend→db opens Deny first, then open one door. On a flat network an allow rule alone changes nothing (everything is already allowed); a namespace default-deny flips it closed, and a single allow re-opens exactly backend→db:5432.
🗺️ How to read this diagram

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 db box turns green (protected). The one blue allow arrow 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.

YAML · default-deny ingress, then allow only the app (needs a CNI that enforces policy)
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 }
NetworkPolicies need an enforcing CNIA NetworkPolicy is only enforced if your CNI plugin supports it (Calico, Cilium do; some don't). On a cluster whose CNI ignores policies, applying them gives a false sense of security — the rules exist but nothing enforces them. Confirm your CNI enforces NetworkPolicy before relying on it, and verify current fields against the docs.

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).

Pod PVC(a request) StorageClass(provisioner) PV(real disk)
🗺️ How to read this diagram

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.

YAML · a PVC and a Pod that mounts it (needs a cluster + StorageClass)
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 }
Access modes decide how it can be sharedReadWriteOnce (RWO) = mounted read-write by one node — the common case (EBS is RWO). ReadOnlyMany / ReadWriteMany allow multiple nodes but need storage that supports it (e.g. EFS/NFS). Pick the mode your storage backend actually supports — verify against the CSI driver's current docs.

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.

Deployment · anonymous, interchangeable web-7d9f web-a1c3 web-b2e8 shared / no disk random names · start all at once StatefulSet · named, own disk db-0 db-1 db-2 pvc-0 pvc-1 pvc-2 stable names · one disk each · ordered 0→1→2 Interchangeable temps vs named employees. A Deployment's Pods get random names and share (or lack) storage; a StatefulSet pins each Pod to a stable ordinal name and its own PVC, started in order — exactly what a database needs.
🗺️ How to read this diagram

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.

DeploymentStatefulSet
Pod namesRandom (web-7d9f-abc)Stable ordinals (db-0, db-1)
StorageUsually shared/noneOne PVC per Pod, kept across restarts
Startup/scalingAll at onceOrdered (0, then 1, …)
Use forStateless apps, web, APIsDatabases, brokers, clustered stateful apps
Default to Deployment; reach for StatefulSet only when identity mattersMost workloads are stateless and belong in a Deployment. Use a StatefulSet only when each replica genuinely needs a stable name and its own durable disk. And often the best answer is to not run the database in-cluster at all — use a managed service (RDS) and keep the cluster stateless (see K5).

✓ 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.
✓ Knowledge check

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
There's no ingress controller running to fulfill the Ingress object. An Ingress is just a declarative routing spec; it does nothing until a controller (NGINX, Traefik, or the AWS Load Balancer Controller) is installed and watching for Ingress objects with a matching 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.)
✓ Knowledge check

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
A Deployment treats Pods as interchangeable and would have three Postgres processes writing to the same ReadWriteOnce volume (or fighting over identity) — a recipe for corruption; databases need each instance to have its own durable disk and stable identity. The correct primitive is a StatefulSet, where each replica (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.

Exercise 1 · Reach a Service across namespaces by DNSBeginner

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.

Exercise 2 · Put two Services behind one IngressIntermediate

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.

Exercise 3 · Lock down a database with NetworkPolicyAdvanced

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.

Exercise 4 · Give a workload durable storageExpert

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.

Exercise 5 · Choose Deployment vs StatefulSet vs managed for three workloadsProfessional

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.

Exercise 6 · Design the full traffic + storage topology for an appIndustry scenario

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.

© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in