Kubernetes Course
First Kubernetes Deployment
This is where the last 14 lessons snap together — you're going to deploy a real multi-replica application, expose it to traffic, verify it's healthy, and understand exactly what Kubernetes did behind the scenes to make it happen.
Why You Almost Never Create Raw Pods
Back in Lesson 8, you learned what a Pod is. And in Lesson 14, you created one directly with a YAML manifest. But here's the thing — in real production environments, you almost never create Pods directly. Raw Pods have one fatal flaw: if the node they're running on dies, the Pod dies with it and nothing brings it back. There's no supervisor. No resurrection. Just gone.
A Deployment fixes this. A Deployment wraps your Pods in a management layer that continuously watches them. If a Pod crashes, the Deployment creates a new one. If you need 5 replicas, the Deployment ensures exactly 5 are always running. If you push a new image version, the Deployment handles the rollout without downtime. This is why Deployments are the default way to run workloads in Kubernetes — and why this lesson is the most important one in the fundamentals section.
🎯 The mental model: A Deployment is a manager. It creates a ReplicaSet (from Lesson 9), which in turn creates and owns the Pods. You talk to the Deployment — the Deployment talks to the ReplicaSet — the ReplicaSet talks to the Pods. You never need to touch ReplicaSets or Pods directly. The Deployment handles everything.
The Deployment Ownership Chain
Understanding the three-tier ownership structure is the key to understanding everything that happens when you run kubectl apply on a Deployment manifest.
Writing Your First Deployment Manifest
The scenario: You're a DevOps engineer at a fast-growing fintech startup. The backend team just finished version 1.0 of a new payment API and it needs to go live. The engineering lead wants three replicas minimum for high availability — if one Pod goes down, the other two keep handling payments. She also needs it accessible from within the cluster so the frontend service can reach it. You need to write the Deployment manifest, the Service, and get it live today.
apiVersion: apps/v1 # Deployments live in the apps API group, version 1
kind: Deployment # We're creating a Deployment — the production-standard way to run Pods
metadata:
name: payment-api # Name of this Deployment object — unique within the namespace
namespace: default # Which namespace to deploy into
labels:
app: payment-api # Label on the Deployment itself — useful for kubectl filtering
team: backend # Team label — great for multi-team clusters
spec:
replicas: 3 # Run exactly 3 Pods at all times — Kubernetes enforces this continuously
selector: # selector: how this Deployment finds and owns its Pods
matchLabels:
app: payment-api # Own any Pod that has the label app=payment-api
template: # template: the Pod blueprint — everything below is applied to each Pod
metadata:
labels:
app: payment-api # CRITICAL: these labels must match the selector above — if they don't, kubectl apply fails
version: "1.0" # Extra label on the Pod — useful for canary deployments later
spec:
containers:
- name: payment-api # Name of the container within the Pod
image: nginx:1.25 # Using nginx as a stand-in for the payment API image
ports:
- containerPort: 8080 # The port the app listens on inside the container
resources:
requests:
cpu: "100m" # 0.1 CPU guaranteed — used by the scheduler for placement
memory: "128Mi" # 128MB RAM guaranteed
limits:
cpu: "300m" # 0.3 CPU ceiling — container throttled if it exceeds this
memory: "256Mi" # 256MB RAM ceiling — container OOM-killed if exceeded
env:
- name: APP_ENV # Inject an environment variable into the container
value: "production" # Value accessible as $APP_ENV inside the container
- name: PORT
value: "8080" # Tell the app which port to bind to
$ kubectl apply -f payment-deployment.yaml deployment.apps/payment-api created $ kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE payment-api 3/3 3 3 18s $ kubectl get pods NAME READY STATUS RESTARTS AGE payment-api-7d6b9c8f4d-2xkpj 1/1 Running 0 18s payment-api-7d6b9c8f4d-8rvnq 1/1 Running 0 18s payment-api-7d6b9c8f4d-m4czl 1/1 Running 0 18s
What just happened?
spec.replicas: 3 — You declared 3 replicas. The Deployment controller received this, created a ReplicaSet, and the ReplicaSet launched 3 Pods. This number is now a contract — if you manually delete one of those Pods, the ReplicaSet immediately creates a replacement. Try it: kubectl delete pod payment-api-7d6b9c8f4d-2xkpj and then instantly run kubectl get pods — you'll see a brand new Pod spinning up.
spec.selector.matchLabels — This is the ownership declaration. The Deployment says "I own any Pod with label app=payment-api." The template labels must match this selector or Kubernetes will reject the manifest. This is a hard validation error — not a warning.
Pod names — Notice the pattern: payment-api-7d6b9c8f4d-2xkpj. That's [deployment-name]-[replicaset-hash]-[pod-hash]. The middle hash (7d6b9c8f4d) identifies which ReplicaSet owns this Pod. When you roll out a new image, the ReplicaSet hash changes — that's how you can tell which generation of Pods are from which rollout.
Deployment READY 3/3 — UP-TO-DATE: 3 means 3 Pods are running the desired spec. AVAILABLE: 3 means 3 are passing readiness checks and can receive traffic. READY: 3/3 combines both. Any number lower than your replica count here means something is wrong.
Exposing the Deployment with a Service
The Pods are running, but nothing can reach them yet. Each Pod has its own IP address — but those IPs are temporary. Every time a Pod restarts, it gets a new IP. You need a stable address that other services can depend on. That's what a Service provides.
The scenario: Your payment API Pods are live. Now the frontend team's checkout service needs to call it. They can't be hardcoding Pod IPs — those change. They need a stable DNS name and a load-balanced endpoint that automatically distributes their requests across all three healthy Pods. Here's the Service that makes that happen.
apiVersion: v1 # Services are in the core v1 API group
kind: Service # Creating a Service — stable network endpoint for our Pods
metadata:
name: payment-api-svc # Service name — this becomes the DNS hostname inside the cluster
namespace: default # Must be in the same namespace as the Pods it targets
spec:
selector:
app: payment-api # Match all Pods with label app=payment-api — our 3 Deployment Pods
ports:
- protocol: TCP # TCP is default but explicit here for clarity
port: 80 # Port the Service exposes to other services in the cluster
targetPort: 8080 # Port to forward traffic to on each matched Pod (our containerPort)
type: ClusterIP # ClusterIP = internal-only — reachable only within the cluster
# Other types: NodePort (external via node IP), LoadBalancer (cloud LB)
$ kubectl apply -f payment-service.yaml service/payment-api-svc created $ kubectl get services NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 5d payment-api-svc ClusterIP 10.96.214.88 <none> 80/TCP 6s $ kubectl describe service payment-api-svc Name: payment-api-svc Namespace: default Selector: app=payment-api Type: ClusterIP IP: 10.96.214.88 Port: <unset> 80/TCP TargetPort: 8080/TCP Endpoints: 10.244.0.5:8080,10.244.1.3:8080,10.244.2.7:8080 Session Affinity: None
What just happened?
CLUSTER-IP: 10.96.214.88 — Kubernetes assigned the Service a stable virtual IP. This IP never changes for the lifetime of the Service. Any Pod in the cluster can now call http://10.96.214.88 or the DNS name http://payment-api-svc to reach the payment API.
Endpoints: 10.244.0.5:8080, 10.244.1.3:8080, 10.244.2.7:8080 — These are the actual Pod IPs. The Service's job is to keep this list up-to-date. When a Pod crashes and a new one appears with a different IP, kube-proxy updates the endpoints list automatically. Callers keep hitting the same Service address — they never notice the Pod churn underneath.
port: 80 vs targetPort: 8080 — The Service listens on port 80 (easy to remember, no port in the URL). But your app container actually listens on 8080. The Service does the translation. This decoupling means you can change the internal port your app uses without forcing every caller to update their URL.
Inspecting a Running Deployment
Deploying is one thing. Knowing how to inspect what you deployed is equally important — especially when something isn't working at 3am.
The scenario: It's your first week as an SRE and your on-call shift just started. An alert fires — the payment API is showing elevated error rates. You need to quickly assess the state of the Deployment, check which Pods are healthy, and look at the logs from the failing Pod. Here are the commands you reach for first.
kubectl describe deployment payment-api
# describe: the most detailed view of any Kubernetes object
# Shows: replica counts, selector, pod template, conditions, and recent events
# The Events section at the bottom is often the fastest path to root cause
kubectl get pods -l app=payment-api
# -l app=payment-api: filter Pods by label — shows only pods belonging to this deployment
# Faster than scanning all pods when you have dozens of services running
kubectl get pods -l app=payment-api -o wide
# -o wide: adds NODE column — tells you which physical node each Pod landed on
# Critical when a node is degraded and all Pods on it are failing
kubectl logs payment-api-7d6b9c8f4d-2xkpj
# logs: stream the stdout/stderr from a specific Pod
# Replace the pod name with one from your kubectl get pods output
kubectl logs -l app=payment-api --all-containers
# -l: stream logs from ALL pods matching the label selector at once
# --all-containers: if a Pod has multiple containers, include all of them
kubectl logs payment-api-7d6b9c8f4d-2xkpj --previous
# --previous: get logs from the PREVIOUS container instance (the one that crashed)
# Invaluable for debugging crash-loop issues — you need the dying container's logs, not the fresh one
$ kubectl describe deployment payment-api
Name: payment-api
Namespace: default
CreationTimestamp: Mon, 10 Mar 2025 09:14:22 +0000
Labels: app=payment-api
Replicas: 3 desired | 3 updated | 3 total | 3 available | 0 unavailable
StrategyType: RollingUpdate
RollingUpdateStrategy: 25% max unavailable, 25% max surge
Pod Template:
Labels: app=payment-api
version=1.0
Containers:
payment-api:
Image: nginx:1.25
Port: 8080/TCP
Limits: cpu: 300m, memory: 256Mi
Requests: cpu: 100m, memory: 128Mi
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing True NewReplicaSetAvailable
Events:
Normal ScalingReplicaSet 2m deployment-controller Scaled up replica set payment-api-7d6b9c8f4d to 3What just happened?
StrategyType: RollingUpdate — This is the default rollout strategy. When you update the Deployment image, Kubernetes won't kill all Pods and redeploy from scratch. It rolls — bringing up new Pods while old ones are still serving traffic, then draining the old ones. The 25% max unavailable means at most 25% of Pods can be down at any point during the rollout. This is how you get zero-downtime deploys for free.
Conditions — These are truth statements about the Deployment's health. Available: True means the minimum number of Pods are ready to serve traffic. Progressing: True means a rollout is underway or just completed. If you ever see Available: False, that's your incident right there.
Events — The Events section at the bottom of any kubectl describe is often the first place to look during debugging. It shows a chronological log of what Kubernetes did to this object — scale events, image pull failures, scheduling problems. Burned into every SRE's muscle memory: when in doubt, describe and scroll to events.
Scaling and Updating the Deployment
The scenario: It's Black Friday. Traffic to the payment API is 4x normal volume and the three Pods are struggling. Your SLO is 99.9% success rate and you're watching it slip in Grafana. Your team lead pings you: "Scale it. Now." Then the load dies down and marketing wants a new feature shipped — which means a new container image needs to be rolled out without any downtime.
kubectl scale deployment payment-api --replicas=8
# scale: imperatively change the replica count — faster than editing YAML during an incident
# Kubernetes immediately creates 5 new Pods to reach the target of 8
# WARNING: imperative commands like this drift from your YAML — always update the YAML afterward
kubectl set image deployment/payment-api payment-api=nginx:1.26
# set image: update the container image — triggers a rolling update automatically
# Format: deployment/[name] [container-name]=[new-image]
# container-name must match the name field under spec.template.spec.containers
kubectl rollout status deployment/payment-api
# rollout status: watch the rolling update in real-time — blocks until complete or failed
# Shows which replica set is scaling up/down and the percentage progress
# Exit code 0 = success, 1 = failed (useful in CI/CD pipelines)
kubectl rollout history deployment/payment-api
# history: shows every revision of this Deployment — each image update creates a new revision
# You can roll back to any numbered revision if the new one breaks something
$ kubectl scale deployment payment-api --replicas=8 deployment.apps/payment-api scaled $ kubectl get pods -l app=payment-api NAME READY STATUS RESTARTS AGE payment-api-7d6b9c8f4d-2xkpj 1/1 Running 0 12m payment-api-7d6b9c8f4d-8rvnq 1/1 Running 0 12m payment-api-7d6b9c8f4d-m4czl 1/1 Running 0 12m payment-api-7d6b9c8f4d-p9wxt 0/1 ContainerCreating 0 2s payment-api-7d6b9c8f4d-r2skl 0/1 ContainerCreating 0 2s payment-api-7d6b9c8f4d-v8njq 0/1 ContainerCreating 0 2s payment-api-7d6b9c8f4d-xb4fm 0/1 ContainerCreating 0 2s payment-api-7d6b9c8f4d-zq7cd 0/1 ContainerCreating 0 2s $ kubectl rollout status deployment/payment-api Waiting for deployment "payment-api" rollout to finish: 2 out of 8 new replicas have been updated... Waiting for deployment "payment-api" rollout to finish: 5 out of 8 new replicas have been updated... deployment "payment-api" successfully rolled out $ kubectl rollout history deployment/payment-api deployment.apps/payment-api REVISION CHANGE-CAUSE 1 <none> 2 <none>
What just happened?
kubectl scale — This is an imperative command, meaning you're telling Kubernetes what to do rather than declaring the desired state in YAML. It works great for emergency scaling, but because it bypasses your YAML files, your Git repo is now out of sync with the cluster. The right practice is to scale imperatively during the incident, then update replicas: in your YAML and commit the change afterward.
ContainerCreating status — The five new Pods appeared immediately in kubectl get pods but in ContainerCreating state. This means Kubernetes has scheduled them to nodes and the kubelet is pulling the container image and creating the container — it hasn't started yet. Within seconds they'll transition to Running.
rollout history — Every time you update the Deployment's Pod template (image, env vars, resources), Kubernetes saves the previous state as a revision. Revision 1 is your original nginx:1.25 deploy. Revision 2 is the nginx:1.26 update. If 1.26 turns out to be broken, you can roll back to revision 1 instantly with kubectl rollout undo deployment/payment-api --to-revision=1. We cover this in depth in Lesson 26.
The Full End-to-End Traffic Flow
Let's make the whole picture concrete — from a request entering the cluster to the response coming back:
Service Pod
ClusterIP
payment-api-svc resolves to 10.96.214.88 via CoreDNS inside the cluster. No hardcoded IPs needed.
kube-proxy uses iptables rules on every node to distribute traffic round-robin across all healthy Pod endpoints.
If Pod 2 crashes, kube-proxy removes it from endpoints within seconds. Traffic flows to Pod 1 and 3 only — no manual intervention.
Teacher's Note: Declarative beats imperative every time
In this lesson you saw both styles: declarative (kubectl apply -f) and imperative (kubectl scale --replicas=8). Declarative wins in the long run because the YAML file in Git becomes the authoritative truth about what's deployed. Imperative commands are for emergencies and quick exploration. Whenever you run an imperative command in production, make it a habit to immediately update the corresponding YAML and commit it. Your future self — and your teammates — will thank you.
You've now completed the Kubernetes Fundamentals section. You understand the architecture, the objects, the YAML, and you've run a real deployment end to end. Section II starts getting into the details that separate Kubernetes beginners from engineers who can actually run production clusters.
Practice Questions
1. What is the correct apiVersion for a Kubernetes Deployment manifest?
2. What is the default Deployment update strategy that replaces Pods gradually to avoid downtime?
3. What kubectl command would you run to immediately roll back the payment-api Deployment to its previous revision?
Quiz
1. You have a Deployment with replicas: 3. You manually delete one Pod. What happens next?
2. What is a critical requirement regarding labels in a Deployment manifest that, if violated, will cause kubectl apply to fail with a validation error?
3. A Pod is crash-looping. You need to see the logs from the container instance that just crashed, not the new one that restarted. Which command do you run?
Up Next · Lesson 16
Pod Lifecycle
Pending, Running, Succeeded, Failed, Unknown — learn what every Pod phase really means and how to debug each one.