Article

Pods, Deployments & ReplicaSets: The Holy Trinity of Kubernetes Workloads

12 min read
Originally published on Medium. Read, clap or comment on Medium ↗

You’ll create hundreds of these in your career. Understanding what’s actually happening under the hood will save you every time something goes wrong.


Kubernetes Zero to Hero · Episode 03


In Episode 02, we mapped the architecture of a Kubernetes cluster — every component, what it does, and what breaks when it’s gone. If you haven’t read it, go back and start there. This episode builds directly on that foundation.

Now we get our hands dirty.

This is the episode where Kubernetes stops feeling abstract. We’re going to work with the three objects you’ll interact with more than any others: Pods, ReplicaSets, and Deployments. We’ll create them, break them, watch Kubernetes heal them, and roll updates forward and backward.

By the end, you won’t just know the commands — you’ll understand why they’re designed the way they are.


Start Here: What Is a Pod?

A Pod is the smallest deployable unit in Kubernetes. Not a container — a Pod.

This trips people up, so let’s be precise: a Pod is a wrapper around one or more containers that share the same network namespace and storage. Containers inside the same Pod can talk to each other via localhost. They share an IP address. They live and die together.

💡 The mental model: Think of a Pod as a logical machine. Your containers are processes running on that machine. They share the same network interface, the same filesystem mounts, and the same lifecycle.

In practice, most Pods run a single container. The multi-container pattern is used for specific cases — a main app container plus a logging sidecar, or a web server plus a configuration-fetching init container. We’ll cover those patterns in a later episode.

Your First Pod

# pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: my-first-pod
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
$ kubectl apply -f pod.yaml
pod/my-first-pod created
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
my-first-pod 1/1 Running 0 12s# Get inside the pod
$ kubectl exec -it my-first-pod -- /bin/bash# Stream logs from the pod
$ kubectl logs my-first-pod --follow# Describe the pod — full details including events
$ kubectl describe pod my-first-pod

The Pod Lifecycle

A Pod moves through these phases during its life:

Pending Pod accepted by the cluster; containers not yet started. Scheduler is finding a node, or image is being pulled.
Running Pod bound to a node; at least one container is running.
Succeeded All containers exited successfully (exit code 0). Common for batch jobs.
Failed All containers have terminated; at least one exited with a non-zero code.
Unknown Pod state can't be determined — usually a node communication problem.

# Watch pod status in real time
$ kubectl get pods --watch
# See exactly why a pod is in a given state
$ kubectl describe pod my-first-pod
# The "Events" section at the bottom is your best debugging tool

Why You Almost Never Create Pods Directly

Here’s the thing about that Pod we just created: it’s naked. It has no safety net.

Delete it and it’s gone. The node it runs on crashes — it’s gone. It has no self-healing, no auto-restart, no rolling updates. A bare Pod is a single point of failure by design.

$ kubectl delete pod my-first-pod
pod "my-first-pod" deleted
$ kubectl get pods
# Nothing. It's just gone. No replacement.

In production, you almost never create Pods directly. You create Deployments, which create ReplicaSets, which create and manage Pods on your behalf. Let’s understand why that layering exists.


ReplicaSets: Keeping the Lights On

A ReplicaSet has one job: ensure a specified number of identical pod replicas are running at all times.

When a Pod dies, the ReplicaSet Controller (remember him from Episode 02?) notices the actual count has dropped below the desired count and immediately creates a replacement. This is the reconciliation loop in action.

# replicaset.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: web-replicaset
spec:
replicas: 3
selector:
matchLabels:
app: web # manages all pods with this label
template:
metadata:
labels:
app: web # pods created by this RS get this label
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
$ kubectl apply -f replicaset.yaml
replicaset.apps/web-replicaset created
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
web-replicaset-2xkp9 1/1 Running 0 5s
web-replicaset-7hmnr 1/1 Running 0 5s
web-replicaset-kq84t 1/1 Running 0 5s

Watch Self-Healing in Real Time

Open two terminal windows. In the second one, watch pods continuously:

# Terminal 2
$ kubectl get pods --watch

Now in Terminal 1, delete one of the pods:

# Terminal 1
$ kubectl delete pod web-replicaset-2xkp9
pod "web-replicaset-2xkp9" deleted

Watch Terminal 2. Within seconds:

NAME                    READY   STATUS              RESTARTS   AGE
web-replicaset-2xkp9 1/1 Running 0 45s
web-replicaset-2xkp9 1/1 Terminating 0 48s
web-replicaset-9fvzx 0/1 Pending 0 0s
web-replicaset-9fvzx 0/1 ContainerCreating 0 0s
web-replicaset-9fvzx 1/1 Running 0 3s

That is Kubernetes doing its job. You deleted a pod. The ReplicaSet Controller noticed the count dropped to 2, desired is 3, and immediately created a replacement. You didn’t do anything.

How ReplicaSets Select Pods — The Label System

ReplicaSets don’t track which Pods they created by name. They use label selectors — they own any pod matching the selector, regardless of who created it.

This has an important implication: if you manually create a pod with the label app: web, the ReplicaSet will count it toward its desired total and may delete one of its own pods to compensate. Labels are the ownership mechanism — treat them with care.

# See the labels on your pods
$ kubectl get pods --show-labels
# See the ReplicaSet's selector
$ kubectl describe replicaset web-replicaset | grep -A 2 Selector

Why You Still Don’t Create ReplicaSets Directly

ReplicaSets handle replica count beautifully. But they have one critical limitation: they can’t do rolling updates.

If you change the container image in a ReplicaSet spec, nothing happens to existing pods. The ReplicaSet only acts when the pod count drifts from desired. It has no concept of “update this image across all pods gracefully.”

That’s what Deployments are for.


Deployments: The Workload You’ll Use Every Day

A Deployment is a higher-level abstraction that manages ReplicaSets on your behalf. It gives you:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-deployment
labels:
app: web
spec:
replicas: 3
selector:
matchLabels:
app: web
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # how many extra pods can exist during update
maxUnavailable: 0 # how many pods can be unavailable during update
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
$ kubectl apply -f deployment.yaml
deployment.apps/web-deployment created
# A Deployment creates a ReplicaSet, which creates Pods
$ kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
web-deployment 3/3 3 3 10s$ kubectl get replicasets
NAME DESIRED CURRENT READY AGE
web-deployment-7d4b9c8d6f 3 3 3 10s$ kubectl get pods
NAME READY STATUS RESTARTS AGE
web-deployment-7d4b9c8d6f-x2k9f 1/1 Running 0 10s
web-deployment-7d4b9c8d6f-p3mzr 1/1 Running 0 10s
web-deployment-7d4b9c8d6f-hn7wq 1/1 Running 0 10s

Notice the naming convention: <deployment-name>-<replicaset-hash>-<pod-hash>. This tells you exactly which ReplicaSet created which Pod.


Rolling Updates: Zero-Downtime Deployments

This is where Deployments shine. Let’s update our nginx version from 1.25 to 1.26 and watch the rollout happen.

# Method 1: Update the image directly
$ kubectl set image deployment/web-deployment nginx=nginx:1.26
# Method 2: Edit the deployment live
$ kubectl edit deployment/web-deployment
# Change nginx:1.25 to nginx:1.26, save and exit# Method 3: Update your YAML file and re-apply (preferred in production)
# Edit deployment.yaml, then:
$ kubectl apply -f deployment.yaml

Now watch it roll:

$ kubectl rollout status deployment/web-deployment
Waiting for deployment "web-deployment" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "web-deployment" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "web-deployment" rollout to finish: 1 old replicas are pending termination...
deployment "web-deployment" successfully rolled out

What’s Happening Under the Hood

Kubernetes created a new ReplicaSet for nginx:1.26 and scaled it up while scaling down the old ReplicaSet. At no point did all three pods go down simultaneously — that's the maxUnavailable: 0 strategy at work.

# See both ReplicaSets — old scaled to 0, new scaled to 3
$ kubectl get replicasets
NAME DESIRED CURRENT READY AGE
web-deployment-7d4b9c8d6f 0 0 0 5m ← old RS (nginx:1.25)
web-deployment-9c8e4a1b2d 3 3 3 60s ← new RS (nginx:1.26)

Kubernetes keeps the old ReplicaSet around (scaled to 0) for one reason: rollback.


Rollbacks: Your Safety Net

Something went wrong with nginx:1.26. Roll it back instantly:

# Roll back to the previous version
$ kubectl rollout undo deployment/web-deployment
deployment.apps/web-deployment rolled back
# Confirm — old ReplicaSet is now scaled back up
$ kubectl get replicasets
NAME DESIRED CURRENT READY
web-deployment-7d4b9c8d6f 3 3 3 ← old RS (nginx:1.25) back in charge
web-deployment-9c8e4a1b2d 0 0 0 ← new RS scaled to 0

You can also roll back to a specific revision:

# See the rollout history
$ kubectl rollout history deployment/web-deployment
REVISION CHANGE-CAUSE
1 <none>
2 <none>
3 <none>
# Add change causes when you deploy (best practice)
$ kubectl annotate deployment/web-deployment kubernetes.io/change-cause="upgraded nginx to 1.26"# Roll back to a specific revision
$ kubectl rollout undo deployment/web-deployment --to-revision=1

💡 Best practice: Always use --record or annotate your deployments with a change cause. When you're staring at a broken cluster at midnight, knowing why each revision was deployed is invaluable.


Scaling: Horizontal and Manual

# Scale up manually
$ kubectl scale deployment/web-deployment --replicas=10
deployment.apps/web-deployment scaled
$ kubectl get pods
# 10 pods now running# Scale back down
$ kubectl scale deployment/web-deployment --replicas=3# Scaling is also just editing the YAML and re-applying — preferred
# Change replicas: 3 to replicas: 10 in deployment.yaml, then:
$ kubectl apply -f deployment.yaml

Autoscaling — Let Kubernetes Decide

Once you have metrics set up (covered in Episode 09), you can let Kubernetes scale automatically:

# hpa.yaml — Horizontal Pod Autoscaler
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-deployment
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # scale up when avg CPU > 70%
$ kubectl apply -f hpa.yaml
$ kubectl get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS
web-hpa Deployment/web-deployment 12%/70% 3 20 3

Resource Requests and Limits: Don’t Skip These

Look back at our Deployment YAML — we included this block:

resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"

This is not optional in production. Here’s why:

Requests tell the Scheduler how much resource this container needs. The scheduler uses requests to decide if a node has capacity for the pod. If you don’t set requests, the scheduler schedules blindly and you end up with overloaded nodes.

Limits tell the kubelet the maximum a container can use. A container that exceeds its memory limit gets OOMKilled. A container that exceeds its CPU limit gets throttled. Without limits, one runaway container can starve every other pod on the node.

⚠️ A cluster with no resource requests and limits is a cluster waiting to have a bad day. Set them on every container. Every single one.

# See actual resource usage (requires metrics-server)
$ kubectl top pods
NAME CPU(cores) MEMORY(bytes)
web-deployment-9c8e4a1b2d-x2k9f 5m 12Mi
web-deployment-9c8e4a1b2d-p3mzr 4m 11Mi
web-deployment-9c8e4a1b2d-hn7wq 6m 13Mi
$ kubectl top nodes
NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
minikube 245m 6% 1023Mi 27%

Liveness and Readiness Probes

How does Kubernetes know if your container is actually healthy? It doesn’t — unless you tell it. That’s what probes are for.

containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
  # Is the container alive? If this fails repeatedly, restart it.
livenessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 10 # wait 10s before first check
periodSeconds: 10 # check every 10s
failureThreshold: 3 # restart after 3 consecutive failures # Is the container ready to receive traffic? If not, remove from Service.
readinessProbe:
httpGet:
path: /ready
port: 80
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 2

Liveness probe answers: “Is this container stuck?” A web server that’s running but deadlocked will pass a process check but fail an HTTP check. Kubernetes restarts it.

Readiness probe answers: “Is this container ready for traffic?” During startup, or when under heavy load, a container might be alive but not ready. Kubernetes removes unready pods from the Service’s load balancer until they recover.

💡 The difference matters deeply. A failing liveness probe causes a restart. A failing readiness probe causes traffic removal — no restart. Use readiness probes aggressively. Use liveness probes carefully — a misconfigured liveness probe can cause restart loops that cascade across your cluster.


The Full Picture: How the Three Objects Relate

Deployment
│ Owns and manages →
├── ReplicaSet (nginx:1.25) ← scaled to 0 after update, kept for rollback
│
└── ReplicaSet (nginx:1.26) ← current, desired=3
│ Owns and manages →
├── Pod (web-xxx-aaa) Running on Node 1
├── Pod (web-xxx-bbb) Running on Node 2
└── Pod (web-xxx-ccc) Running on Node 3

Common Failure Patterns and How to Debug Them

Pod stuck in Pending

$ kubectl describe pod <pod-name>
# Look for Events — common causes:
# "0/3 nodes are available: 3 Insufficient memory" → need more nodes or smaller requests
# "0/3 nodes are available: 3 node(s) had taint" → pod needs a toleration
# "no nodes available to schedule pods" → cluster is full

Pod stuck in CrashLoopBackOff

# Container keeps crashing and restarting
$ kubectl logs <pod-name> # current container logs
$ kubectl logs <pod-name> --previous # logs from the LAST crashed container
$ kubectl describe pod <pod-name> # check exit code and last state

Pod stuck in ImagePullBackOff

$ kubectl describe pod <pod-name>
# Events will show: "Failed to pull image: ... 404 Not Found"
# Causes: wrong image name, wrong tag, private registry with no pull secret

Deployment not rolling out

$ kubectl rollout status deployment/<name>
$ kubectl describe deployment <name>
# Check: are pods failing readiness probes? Is there a quota issue?

Hands-On Lab: Put It All Together

Work through this sequence on your local cluster:

# 1. Create the deployment
$ kubectl apply -f deployment.yaml
# 2. Watch the rollout
$ kubectl rollout status deployment/web-deployment# 3. Self-healing test — delete a pod, watch it come back
$ kubectl get pods --watch &
$ kubectl delete pod <any-pod-name># 4. Scale up
$ kubectl scale deployment/web-deployment --replicas=6
$ kubectl get pods# 5. Trigger a rolling update
$ kubectl set image deployment/web-deployment nginx=nginx:1.26
$ kubectl rollout status deployment/web-deployment# 6. Check the ReplicaSet history
$ kubectl get replicasets# 7. Roll back
$ kubectl rollout undo deployment/web-deployment
$ kubectl get replicasets# 8. Scale back down
$ kubectl scale deployment/web-deployment --replicas=3# 9. Clean up
$ kubectl delete deployment web-deployment

If you work through all nine steps and understand what Kubernetes is doing at each one — you’ve internalized the holy trinity.


What’s Next

In Episode 04, we tackle Services and Networking — the piece that makes your pods actually reachable. Pods have IPs, but those IPs change every time a pod restarts. Services give you a stable endpoint that routes traffic to healthy pods automatically.

We’ll cover ClusterIP, NodePort, LoadBalancer, and ExternalName services, how DNS resolution works inside a cluster, and how to expose your Deployment to the outside world.

Episode Topic
EP 01 Introduction to Kubernetes
EP 02 Kubernetes Architecture & Components
EP 03 ← You are here Pods, Deployments & ReplicaSets
EP 04 Services & Networking — Making pods talk to the world
EP 05 ConfigMaps, Secrets & Environment Config
EP 06 Storage: PersistentVolumes & StatefulSets
EP 07 Ingress Controllers & Load Balancing
EP 08 RBAC & Security Fundamentals
EP 09 Monitoring with Prometheus & Grafana
EP 10 Production Hardening & Helm Charts


Before the Next Article

Make sure you can do this from memory — no notes:

# Create a deployment, scale it, update the image, roll it back, delete it.
# If you can do all five without Googling, you own this episode.
$ kubectl create deployment test --image=nginx:1.25 --replicas=3
$ kubectl scale deployment test --replicas=6
$ kubectl set image deployment/test nginx=nginx:1.26
$ kubectl rollout undo deployment/test
$ kubectl delete deployment test

One command at a time. Each one should feel natural before you move on.


The engineers who truly master Kubernetes aren’t the ones who memorize commands. They’re the ones who understand what the cluster is doing and why. You’re building that foundation right now. See you in Episode 04.

← All articles Discuss on Medium ↗