Kubernetes Deployment
Docker runs one container. Production needs many, self-healing, scaled, configured per environment, and routable from outside the cluster — that's the job Kubernetes does.
Learning objectives
- Beginner: Explain what Kubernetes adds beyond running a single Docker container.
- Intermediate: Deploy a service to a local Minikube cluster and expose it via a Service and Ingress.
- Advanced: Configure liveness vs. readiness probes correctly so a struggling pod is restarted or removed from traffic at the right moment, not both interchangeably.
◆ The problem
docker run gives you one container on one machine. It doesn't restart the container if it crashes, doesn't spread replicas across multiple machines, doesn't route traffic only to instances that are actually ready, and doesn't give you a declarative way to say "I want 3 of these running" and have something continuously make that true.
Kubernetes is a container orchestrator: you declare desired state (which image, how many replicas, what config), and its control loop continuously reconciles the cluster's actual state toward it — restarting failed containers, rescheduling them onto healthy nodes, and only routing traffic to instances that report themselves ready.
The control plane continuously schedules and reconciles pods across worker nodes; a crashed pod is automatically rescheduled without operator intervention.
| Component | Role |
|---|---|
| API Server | The single entry point for all cluster state changes (kubectl talks to this). |
| Scheduler | Decides which node a new pod should run on. |
| Controller Manager | Runs the reconciliation loops (e.g. "actual replica count doesn't match desired — create more pods"). |
| etcd | The cluster's own distributed, consistent key-value store of all state — conceptually similar in spirit to how KRaft (Module 02) gives Kafka its own metadata log. |
| kubelet (per node) | The agent on each worker node that actually starts/stops containers as instructed. |
minikube start kubectl get nodes kubectl config current-context
💻 Code example
minikube start kubectl get nodes kubectl config current-context
A Pod is the smallest deployable unit — one or more tightly-coupled containers sharing network/storage. A Deployment declares desired state for a set of identical pods and is what you actually create for a stateless service like either Library Events service.
apiVersion: apps/v1 kind: Deployment metadata: name: library-events-producer spec: replicas: 2 selector: matchLabels: { app: library-events-producer } template: metadata: labels: { app: library-events-producer } spec: containers: - name: library-events-producer image: yourorg/library-events-producer:1.0 ports: [{ containerPort: 8080 }] envFrom: - configMapRef: { name: producer-config } # see §20.7
💻 Code example
apiVersion: apps/v1 kind: Deployment metadata: name: library-events-producer spec: replicas: 2 selector: matchLabels: { app: library-events-producer } template: metadata: labels: { app: library-events-producer } spec: containers: - name: library-events-producer image: yourorg/library-events-producer:1.0 ports: [{ containerPort: 8080 }] envFrom: - configMapRef: { name: producer-config } # see §20.7
| Method | What it's for | Traffic scope |
|---|---|---|
| Service | A stable internal DNS name + virtual IP load-balancing across a Deployment's pods, even as they're rescheduled | Internal cluster traffic (or external, if type LoadBalancer / NodePort) |
| Port-Forward | kubectl port-forward tunnels a local port directly to one pod — for debugging, not production traffic | Your machine only, temporary |
| Ingress | Routes external HTTP(S) by hostname/path to internal Services, typically with TLS termination | Real external production traffic |
apiVersion: v1 kind: Service metadata: { name: library-events-producer-svc } spec: selector: { app: library-events-producer } ports: [{ port: 80, targetPort: 8080 }]
💻 Code example
apiVersion: v1 kind: Service metadata: { name: library-events-producer-svc } spec: selector: { app: library-events-producer } ports: [{ port: 80, targetPort: 8080 }]
kubectl scale deployment library-events-producer --replicas=4
◆ Under the hood — scaling the consumer specifically
Scaling the producer Deployment is straightforward horizontal scaling — more identical, stateless pods sharing incoming HTTP load via the Service. Scaling the consumer Deployment interacts directly with Module 03's consumer-group model: each new consumer pod joins the same group.id and triggers a rebalance, and — exactly as in §03.4's table — scaling consumer replicas past the topic's partition count leaves the extra pods with no partition to consume. Plan consumer replica count against partition count, not against a generic "more replicas = more throughput" assumption.
💻 Code example
kubectl scale deployment library-events-producer --replicas=4
A ConfigMap externalizes non-sensitive config from the image itself — directly extending Module 18/19's environment-variable pattern to Kubernetes.
apiVersion: v1 kind: ConfigMap metadata: { name: producer-config } data: SPRING_KAFKA_PRODUCER_BOOTSTRAP_SERVERS: kafka-broker-svc:9092
💻 Code example
apiVersion: v1 kind: ConfigMap metadata: { name: producer-config } data: SPRING_KAFKA_PRODUCER_BOOTSTRAP_SERVERS: kafka-broker-svc:9092
| Liveness probe | Readiness probe | |
|---|---|---|
| Question it answers | "Is this process stuck and should be restarted?" | "Can this pod currently serve traffic?" |
| On failure | Kubernetes kills and restarts the container | Kubernetes stops routing traffic to it, but doesn't restart it |
| Backed by | A basic liveness endpoint (JVM/process healthy) | Module 17's Kafka readiness health indicator — actual dependency connectivity |
livenessProbe: httpGet: { path: /actuator/health/liveness, port: 8080 } initialDelaySeconds: 30 readinessProbe: httpGet: { path: /actuator/health/readiness, port: 8080 } initialDelaySeconds: 20
Notice the two delays genuinely differ, deliberately: readiness starts checking sooner (20s) since it's safe to briefly withhold traffic from a pod that's still warming up, while liveness waits longer (30s) precisely because a false-positive liveness failure is expensive — it triggers a full container restart, not just a temporary traffic pause.
▲ Pitfall
Wiring the readiness check to liveness's endpoint (or vice versa) is a common misconfiguration: if a broken Kafka connection incorrectly fails the liveness probe, Kubernetes restarts a perfectly healthy process repeatedly instead of simply withholding traffic — restarting the pod does nothing to fix an external Kafka outage and just adds churn.
💻 Code example
livenessProbe: httpGet: { path: /actuator/health/liveness, port: 8080 } initialDelaySeconds: 30 readinessProbe: httpGet: { path: /actuator/health/readiness, port: 8080 } initialDelaySeconds: 20
| Env-var Secret | Volume-mounted Secret | |
|---|---|---|
| How it's consumed | Injected as an environment variable, like ConfigMap | Mounted as a file inside the container's filesystem |
| Good for | Simple credentials (DB password, single token) | Multi-file material (e.g. an SSL keystore/truststore, needed in Module 21) or anything an app expects to read from disk |
| Rotation visibility | Requires a pod restart to pick up a changed env var | A mounted Secret file can be updated live without restarting the pod (kubelet syncs it) |
apiVersion: v1 kind: Secret metadata: { name: db-credentials } type: Opaque data: password: cG9zdGdyZXM= # base64 — not encrypted at rest by default
▲ Pitfall
Base64 is an encoding, not encryption — a Secret is not "secure" purely by virtue of being a Secret object unless the cluster also has encryption at rest and RBAC restricting who can read Secret objects configured. Don't treat "it's a Secret, not a ConfigMap" as a complete security control by itself.
✓ Quick recap
What does a Deployment give you that docker run alone doesn't? Declarative desired state continuously reconciled — automatic restart/rescheduling of failed pods, and coordinated scaling. When would you use Ingress instead of a plain Service? For real external HTTP(S) traffic needing hostname/path routing and typically TLS termination — a Service alone (ClusterIP) isn't externally reachable. Why is it dangerous to point a liveness probe at a dependency-health endpoint? An external dependency outage would repeatedly restart a perfectly healthy pod instead of just withholding traffic from it. Is a Kubernetes Secret encrypted by default? No — its data is base64-encoded, not encrypted, unless the cluster is separately configured for encryption at rest.
💻 Code example
apiVersion: v1 kind: Secret metadata: { name: db-credentials } type: Opaque data: password: cG9zdGdyZXM= # base64 — not encrypted at rest by default
Want a visual for this concept?
Generate a diagram tailored to “Kubernetes Deployment” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →