A bare Pod dies and stays dead. A ReplicaSet ensures that a specified number of Pod replicas are running at all times. It's the first layer of self-healing — and understanding how it finds its Pods (label selection) is critical for debugging ownership issues.
1. What a ReplicaSet Does
A ReplicaSet has one job: maintain exactly N Pods matching a label selector. Its reconciliation loop:
- Count Pods in the namespace matching its selector
- Compare to desired replica count
- If too few → create Pods from its template
- If too many → delete excess Pods
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: web
spec:
replicas: 3 # ← desired count
selector: # ← how it finds its Pods
matchLabels:
app: web
version: v1
template: # ← Pod template (used to create new Pods)
metadata:
labels:
app: web
version: v1 # ← MUST match selector
spec:
containers:
- name: nginx
image: nginx:1.25
The Three Parts
| Part | Purpose | Mutable? |
|---|---|---|
spec.replicas | How many Pods to maintain | ✅ Yes |
spec.selector | Which Pods belong to this RS | ❌ Immutable after creation |
spec.template | Blueprint for new Pods | ✅ Yes (but won't affect existing Pods) |
2. The Label Selection Contract
This is the most important concept: ReplicaSets don't track Pods by internal state or a list of IDs. They find their Pods purely by label matching.
Rules of the Contract
- Template labels must include selector labels — otherwise the RS creates Pods it immediately disowns (API server rejects this)
- Template labels can be a superset — extra labels beyond the selector are fine
- Any Pod matching the selector can be adopted — even Pods the RS didn't create
- Removing a label from a Pod releases it — the RS no longer counts it
What "Matching" Means
# ReplicaSet selector:
selector:
matchLabels:
app: web
version: v1
# This Pod matches (has both labels):
labels:
app: web
version: v1
team: platform # extra labels are fine
# This Pod does NOT match (missing version):
labels:
app: web
team: platform
3. Ownership & ownerReferences
When a ReplicaSet creates a Pod, it stamps an ownerReference on the Pod's metadata:
kubectl get pod web-abc12 -o yaml | grep -A8 ownerReferences # ownerReferences: # - apiVersion: apps/v1 # kind: ReplicaSet # name: web-5d4f6b7c8 # uid: a1b2c3d4-... # controller: true # ← this is the managing controller # blockOwnerDeletion: true
What ownerReferences Enable
| Feature | How It Works |
|---|---|
| Garbage collection | Delete the RS → its Pods are automatically deleted (cascading) |
| Ownership claim | Prevents two ReplicaSets from fighting over the same Pod |
| Controller identity | controller: true marks the primary manager — only one controller can be the "owner" |
Orphan & Adopt Mechanics
Practical: Isolating a Bad Pod for Debugging
# Pod "web-abc12" is misbehaving. Want to debug it without # it being deleted by the RS, and without losing a replica: # 1. Remove the RS selector label → orphans it kubectl label pod web-abc12 app- # removes "app" label # RS immediately creates a replacement Pod (maintains count). # The old Pod keeps running — you can now debug it at leisure. # When done: kubectl delete pod web-abc12
kubectl exec and kubectl logs.
4. Scaling
Scaling a ReplicaSet just changes the desired count — the controller handles the rest:
# Scale up: kubectl scale rs web --replicas=5 # RS counts 3 Pods → needs 5 → creates 2 more from template # Scale down: kubectl scale rs web --replicas=2 # RS counts 5 Pods → needs 2 → deletes 3 (newest first by default) # Scale to zero (stop all Pods, keep RS for later): kubectl scale rs web --replicas=0
Which Pods Get Deleted on Scale-Down?
The ReplicaSet controller uses a ranking algorithm:
- Unassigned Pods (Pending) deleted first
- Pods on nodes with more replicas (spread Pods across nodes)
- Newer Pods deleted before older ones (by creation timestamp)
- Pods with more container restarts deleted first
5. ReplicaSet vs Deployment — When to Use Which
Almost never use a ReplicaSet directly. In practice, you always use a Deployment, which manages ReplicaSets for you.
| ReplicaSet | Deployment | |
|---|---|---|
| Rolling updates | ❌ No (manual) | ✅ Automatic |
| Rollback | ❌ No | ✅ Built-in revision history |
| Template change | No effect on existing Pods | Creates new RS, scales down old |
| Use case | Understanding internals, rare custom controllers | All production workloads |
The ownership chain:
Deployment
└── ReplicaSet (revision 1) ← scaled to 0 after rollout
└── ReplicaSet (revision 2) ← current, owns the running Pods
└── Pod A
└── Pod B
└── Pod C
kubectl get rs), you're seeing the rollout history.
6. Inspecting ReplicaSets
# List ReplicaSets:
kubectl get rs
# NAME DESIRED CURRENT READY AGE
# web-6f4a8b2 0 0 0 5d ← old revision
# web-8b2c3d4 3 3 3 1d ← current
# See which Pods a RS owns:
kubectl get pods -l app=web --show-labels
# Describe for events (scaling events, failures):
kubectl describe rs web-8b2c3d4
# Check ownerReferences on a Pod:
kubectl get pod web-8b2c3d4-xk2j9 -o jsonpath='{.metadata.ownerReferences[0].name}'
# web-8b2c3d4
Common Issues
| Symptom | Cause | Fix |
|---|---|---|
| RS shows DESIRED=3 but READY=0 | Pods stuck Pending or crashing | Check Pod events: kubectl describe pod ... |
| Two RSs fighting over Pods | Overlapping selectors | Ensure selectors are unique (use pod-template-hash) |
| RS creates Pods but they immediately disappear | Another controller (Deployment) scaling it down | Check if RS is owned by a Deployment — modify the Deployment instead |
| Orphaned Pods (no owner) | RS was deleted with --cascade=orphan | Delete Pods manually or create a new RS with matching selector |
The pod-template-hash Label
Deployments add a pod-template-hash label to both the RS and its Pods. This guarantees selector uniqueness across revisions:
kubectl get rs -o custom-columns=NAME:.metadata.name,HASH:.metadata.labels.pod-template-hash # NAME HASH # web-6f4a8b2 6f4a8b2 # web-8b2c3d4 8b2c3d4 # The RS name IS the deployment name + hash # Pods also carry this label → only the correct RS matches them
pod-template-hash prevents cross-adoption. Without it, a new RS with the same selector as an old RS would adopt its Pods (since labels match). The hash makes each RS's selector unique, even across Deployment revisions.
Summary
| Concept | Key Takeaway |
|---|---|
| ReplicaSet | Maintains N Pods matching a label selector |
| Label contract | RS finds Pods purely by labels — no internal list |
| Selector immutability | Cannot change after creation |
| Template changes | Only affect new Pods (existing Pods unchanged) |
| ownerReferences | Stamp on Pods enabling garbage collection & ownership claim |
| Orphaning | Remove selector label → Pod escapes RS → RS creates replacement |
| Adoption | Matching unowned Pod → RS claims it → may delete excess |
| pod-template-hash | Prevents cross-adoption between Deployment revisions |
📝 Quiz: ReplicaSets & Label Selection
Q1: A ReplicaSet has selector app=web, tier=frontend. A Pod has labels app=web, tier=frontend, version=v2. Does the RS match this Pod?
app=web and tier=frontend. The extra version=v2 label is irrelevant — the selector is a subset of the Pod's labels.Q2: You change a ReplicaSet's Pod template to use nginx:1.26 instead of nginx:1.25. What happens to the 3 running Pods?
nginx:1.25. The RS only uses the template to create new Pods. Only if a Pod is deleted (or you scale up) will new Pods get nginx:1.26. This is exactly why Deployments exist — they automate the old-RS → new-RS transition.Q3: A production Pod is misbehaving. How do you isolate it for debugging without causing downtime?
kubectl label pod web-abc12 app-. This orphans the Pod (RS no longer matches it). The RS creates a replacement immediately (maintaining desired count). You now have the bad Pod isolated for inspection.Q4: You delete a ReplicaSet with kubectl delete rs web. What happens to its Pods?
--cascade=orphan.Q5: You manually create a Pod with labels matching an existing ReplicaSet's selector. The RS has replicas=3 and currently has 3 Pods. What happens?
Q6: Why does a Deployment-managed RS include pod-template-hash in its selector?
app=web). Without the hash, the new RS might adopt old Pods (or vice versa). The hash makes each RS's full selector unique: app=web, pod-template-hash=8b2c3d4.