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.
1 · The cluster networking model
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.
2 · Cluster DNS
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.
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
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.
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
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.
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
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).
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
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 | 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.