AI EngineeringZero to ProductionHome·About·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.

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.

Internet Ingress controller Service (ClusterIP) Pods
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 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.

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

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.

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

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.

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

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

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.

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