Deployments
Deployments are the standard way to run stateless applications in Kubernetes. By wrapping your Pods in a Deployment, you gain self-healing, on-demand scaling, zero-downtime rolling updates, and versioned rollbacks — all managed automatically by the control plane.
The Three-Tier Hierarchy
Section titled “The Three-Tier Hierarchy”Kubernetes separates concerns across three layers of objects:
| Layer | Object | Responsibility |
|---|---|---|
| Top | Deployment | Governs rollouts, rollbacks, and update strategy |
| Middle | ReplicaSet | Ensures the correct number of Pods are running (self-healing + scaling) |
| Bottom | Pod | Runs the actual containerised application |
When you kubectl apply a Deployment manifest, a cascading creation occurs: the Deployment creates a ReplicaSet, which creates and manages the individual Pods.
ReplicaSets
Section titled “ReplicaSets”A ReplicaSet is the object that provides self-healing and scaling. It replaces the older, now-deprecated ReplicationController. Even for a single Pod, deploying via a ReplicaSet (or Deployment) is strongly preferred over creating a standalone Pod — if the node fails, the ReplicaSet reschedules the Pod on a healthy node automatically; a standalone Pod simply disappears.
The Reconciliation Loop
Section titled “The Reconciliation Loop”The ReplicaSet controller runs a continuous observe → compare → act loop:
- Observe — watch the ReplicaSet and its dependent Pods
- Compare — count Pods matching the selector against
spec.replicas - Act — create missing Pods or delete excess ones to restore balance
This loop handles failures automatically with no human intervention.
| Trigger | Controller reaction |
|---|---|
| Pod crashes or is deleted | Immediately creates a replacement |
| Extra Pod matching the selector appears | Terminates the excess to restore the desired count |
Node goes NotReady | Marks Pods for deletion, spins up replacements on healthy nodes |
ReplicaSet Spec
Section titled “ReplicaSet Spec”ReplicaSets live in the apps/v1 API group and require three fields in spec:
apiVersion: apps/v1kind: ReplicaSetmetadata: name: my-appspec: replicas: 3 # desired Pod count (default: 1) selector: matchLabels: app: my-app # which Pods this RS manages template: # blueprint for new Pods metadata: labels: app: my-app # must be a superset of selector labels spec: containers: - name: app image: my-app:1.0Critical rule: the labels in template.metadata.labels must be a superset of the labels in selector.matchLabels. If they don’t match, the API server rejects the object — the ReplicaSet would create Pods it cannot see.
Immutability Constraints
Section titled “Immutability Constraints”| Field | Mutable? | Notes |
|---|---|---|
spec.selector | ❌ No | Delete and recreate to change |
spec.replicas | ✅ Yes | Immediate effect |
spec.template | ✅ Yes | Only affects new Pods — existing Pods are not updated |
Pod Naming
Section titled “Pod Naming”Pods in a ReplicaSet are fungible — there is no concept of order. Kubernetes uses the generateName field to produce names like my-app-x7k2p (ReplicaSet name + 5 random characters).
Scale-Down Priority
Section titled “Scale-Down Priority”When you reduce replicas, Kubernetes doesn’t terminate Pods randomly. It follows this ordered priority:
- Pods not yet assigned to a node
- Pods with an unknown phase
- Pods that are not ready
- Pods with a lower
controller.kubernetes.io/pod-deletion-costannotation - Pods on nodes with more replicas of this ReplicaSet (promotes even distribution)
- Pods ready for a shorter time
- Pods with more container restarts
- Most recently created Pods
Pod Ownership and Garbage Collection
Section titled “Pod Ownership and Garbage Collection”When a ReplicaSet creates Pods, it becomes their owner, recorded in each Pod’s metadata.ownerReferences field. Deleting the ReplicaSet triggers cascading deletion of all its Pods via the garbage collector.
To delete the ReplicaSet while keeping Pods running (e.g., to recreate it with a changed selector):
kubectl delete rs <name> --cascade=orphanThe surviving Pods become independent orphans. If you later create a new ReplicaSet whose selector matches those orphaned Pods, it will adopt them automatically.
Debugging: Isolating a Failing Pod
Section titled “Debugging: Isolating a Failing Pod”If a Pod is continuously failing but you need to keep your service up and debug without destroying evidence:
-
Change one of the failing Pod’s labels so it no longer matches the selector:
Terminal window kubectl label pod <failing-pod> rel=debug --overwrite -
The ReplicaSet detects a missing replica and immediately spins up a healthy replacement.
-
The isolated Pod continues running independently for inspection.
-
After debugging, delete it manually — the garbage collector won’t touch it since it’s now an orphan.
Deployments
Section titled “Deployments”A Deployment sits above the ReplicaSet and adds the machinery for rollouts and rollbacks. In practice, you should always use Deployments rather than bare ReplicaSets — they handle version management automatically.
The Declarative Model
Section titled “The Declarative Model”The declarative model is the foundation of Deployment management:
| Approach | How | Kubernetes can self-heal? |
|---|---|---|
| Declarative | Describe the desired end state in YAML; Kubernetes figures out how to get there | ✅ Yes |
| Imperative | Issue step-by-step commands | ❌ No — no concept of desired state |
Deployment Spec
Section titled “Deployment Spec”apiVersion: apps/v1kind: Deploymentmetadata: name: my-app # valid DNS name (alphanumerics, dots, dashes)spec: replicas: 3 selector: matchLabels: app: my-app # must match template labels exactly template: metadata: labels: app: my-app spec: containers: - name: app image: my-app:1.0 ports: - containerPort: 8080 strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 1 # max Pods offline during update maxSurge: 1 # max extra Pods during update revisionHistoryLimit: 5 # how many old ReplicaSets to retain minReadySeconds: 10 # wait before marking a replica available progressDeadlineSeconds: 300 # timeout before marking rollout as stalledGolden rule: spec.selector.matchLabels and spec.template.metadata.labels must match exactly. If they don’t, Kubernetes rejects the object. The top-level metadata.labels (the Deployment’s own labels) are irrelevant to this mapping.
Labels in Three Places
Section titled “Labels in Three Places”When inspecting a Deployment YAML, you’ll see labels appear three times:
| Location | Purpose |
|---|---|
metadata.labels | Labels on the Deployment object itself |
spec.selector.matchLabels | How the Deployment finds its Pods |
spec.template.metadata.labels | Labels stamped onto each new Pod |
The pod-template-hash Safeguard
Section titled “The pod-template-hash Safeguard”When a Deployment creates its ReplicaSet, Kubernetes automatically injects a pod-template-hash label (a cryptographic hash of the Pod template) into the ReplicaSet’s selector and into every Pod it creates. This prevents a ReplicaSet from accidentally adopting unrelated Pods that happen to share a primary label.
Creating Deployments
Section titled “Creating Deployments”Imperatively
Section titled “Imperatively”# Minimum required: name + imagekubectl create deployment my-app --image=my-app:1.0
# With replicas and portkubectl create deployment my-app --image=my-app:1.0 --replicas=3 --port=8080
# Generate YAML without applyingkubectl create deployment my-app --image=my-app:1.0 --dry-run=client -o yaml > deployment.yamlDeclaratively
Section titled “Declaratively”kubectl apply -f deployment.yamlOnce applied, the Deployment and ReplicaSet controllers start their reconciliation loops immediately, scheduling Pods onto healthy worker nodes.
Inspecting Deployments
Section titled “Inspecting Deployments”# High-level status (READY, UP-TO-DATE, AVAILABLE columns)kubectl get deploy <name>
# Full configuration + events (use when troubleshooting label mismatches)kubectl describe deploy <name>
# List ReplicaSets — names are prefixed with the Deployment name + a hashkubectl get rs
# List Pods with their labelskubectl get pods --show-labels
# List Pods belonging to a ReplicaSet by selectorkubectl get pods -l app=my-appReplicaSet naming: the Deployment name + a crypto-hash of the Pod template, e.g., my-app-54f5d46964. If you update the Pod template, a brand-new ReplicaSet with a new hash is created.
# View logs from all Pods in a ReplicaSet at oncekubectl logs rs/<name>kubectl logs rs/<name> --all-pods --all-containersScaling in Practice
Section titled “Scaling in Practice”Delegation Chain
Section titled “Delegation Chain”When you scale a Deployment, the Deployment controller does not create or delete Pods directly — it only updates the replicas count on the underlying ReplicaSet. The ReplicaSet controller then executes the actual Pod additions or removals. Any manual changes to a Deployment-owned ReplicaSet’s replica count are immediately overwritten.
The Replicas Field Trap
Section titled “The Replicas Field Trap”A common production mishap when mixing imperative scaling with declarative manifests:
- You imperatively scale to handle traffic:
kubectl scale deployment my-app --replicas=50 - Later, you apply an updated manifest that still has
replicas: 3hardcoded - The manifest overwrites the live scale — 47 Pods terminate immediately
The fix: omit replicas from your manifest entirely. Kubernetes defaults to 1 on creation; scale via kubectl scale or HPA afterwards. Future kubectl apply runs will never overwrite the live count.
# Repair an existing Deployment whose last-applied annotation already contains replicaskubectl apply edit-last-applied deploy <name># Delete the replicas field from the annotation and save — future applies will skip itTroubleshooting Deployments
Section titled “Troubleshooting Deployments”When Pods fail to appear after creating or updating a Deployment, the cause is almost always in the underlying ReplicaSet, not the Deployment itself.
| Where to look | What you find |
|---|---|
kubectl describe deploy <name> → Conditions | ReplicaFailure: True with reason FailedCreate — tells you something is wrong |
kubectl describe rs <name> → Events | The exact error: “forbidden”, “service account not found”, “insufficient quota”, etc. |
# Step 1: check Deployment conditionskubectl get deploy <name> -o yaml | grep -A 10 conditions
# Step 2: find and inspect the failing ReplicaSetkubectl get rskubectl describe rs <rs-name>Rolling Updates
Section titled “Rolling Updates”A rolling update replaces Pods incrementally — old Pods are terminated and new ones created until all replicas are running the new version. In Kubernetes, all updates are replacement operations — Pods are immutable, so “updating” a Pod means deleting it and creating a new one.
Prerequisites for Zero Downtime
Section titled “Prerequisites for Zero Downtime”For rolling updates to be truly zero-downtime, your application should be:
- Loosely coupled — services communicate via well-defined APIs
- Backward and forward compatible — during a rollout, old and new versions run simultaneously; clients hitting either version must get a valid response
Mechanism
Section titled “Mechanism”- You update the Pod template (e.g., change the image tag) and
kubectl apply - The Deployment controller creates a new ReplicaSet for the new version
- The controller incrementally scales up the new RS while scaling down the old one
- The rollout completes when the old RS reaches 0 Pods and the new RS reaches the full replica count
Recreate Strategy
Section titled “Recreate Strategy”All existing Pods are deleted simultaneously, and only after they are fully terminated does Kubernetes start creating the new Pods — guaranteeing a period of zero availability between versions.
spec: strategy: type: Recreate # no rollingUpdate block — no configuration options| Aspect | Detail |
|---|---|
| Configuration options | None |
| Client experience during update | 503 Service Temporarily Unavailable via Ingress; connection rejected via ClusterIP |
| When to use | Applications that cannot run two versions simultaneously (e.g., database schema migrations that are not backward compatible) |
RollingUpdate Strategy
Section titled “RollingUpdate Strategy”The default strategy. Gradually replaces old Pods with new ones, keeping the application continuously accessible throughout the update.
spec: strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 # absolute number or percentage (default: 25%) maxUnavailable: 1 # absolute number or percentage (default: 25%)| Parameter | What it controls |
|---|---|
maxSurge | Maximum extra Pods allowed above the desired count during the update |
maxUnavailable | Maximum Pods allowed below the desired count (unavailable) during the update |
Constraint: both cannot be 0 simultaneously — the controller would be unable to add new Pods or remove old ones.
Concrete parameter combinations (assuming 3 desired replicas):
maxSurge | maxUnavailable | Behaviour |
|---|---|---|
0 | 1 | Delete first, then create. Never exceeds 3 Pods; at least 2 available at any time. |
1 | 0 | Create first, then delete. Always ≥ 3 Pods available; temporary 4-Pod state. |
1 | 1 | Create and delete in parallel. Fastest; 2–4 Pods simultaneously. |
minReadySeconds — The Airbag
Section titled “minReadySeconds — The Airbag”By default a new Pod is marked available the instant its readiness probe passes, and the rollout immediately continues to the next Pod. minReadySeconds enforces a mandatory wait between “ready” and “available”:
spec: minReadySeconds: 60 # pod must stay ready for 60 s before it counts as availableIf the Pod’s containers crash or fail their readiness probe at any point during the wait window, the timer resets and the Pod never becomes available — the rollout halts and no further old Pods are replaced.
progressDeadlineSeconds — Stall Detection
Section titled “progressDeadlineSeconds — Stall Detection”If a rolling update makes no progress for longer than progressDeadlineSeconds (default: 600 s), the Progressing condition changes to False with reason ProgressDeadlineExceeded. Kubernetes takes no automated action — the rollout simply stops.
kubectl rollout status deployment <name> # shows "error: deployment exceeded its progress deadline"Monitoring, Pausing, and Resuming
Section titled “Monitoring, Pausing, and Resuming”# Watch rollout progress in real timekubectl rollout status deployment <name>
# Wait until the Deployment is fully available (useful in CI/CD scripts)kubectl wait --for condition=Available deployment/<name>
# Pause mid-rollout (e.g., to observe the first new Pod before continuing)kubectl rollout pause deployment <name>
# Resumekubectl rollout resume deployment <name>Use cases for pausing:
| Use case | How |
|---|---|
| Manual canary check | Trigger update → pause → verify the first new Pod → resume |
| Batch multiple changes | Pause before editing → apply several changes → resume once (single rollout) |
Caveats when paused:
- Split traffic — client requests are served by both old and new versions simultaneously. A user’s browser may receive HTML from one version and CSS from another, causing rendering issues unless backward compatibility is strict.
- Rollback is blocked —
kubectl rollout undodoes nothing while a Deployment is paused. Resume first, then undo. - Autoscaler distribution — if the HPA requests more Pods while paused, they are split across both ReplicaSets proportionally until the rollout resumes.
Updating Methods
Section titled “Updating Methods”# Declarative (preferred for production)kubectl apply -f deployment.yaml
# Interactive editkubectl edit deployment <name>
# Quick image-only updatekubectl set image deployment <name> <container-name>=<new-image>
# Force replace (deletes and recreates)kubectl replace -f deployment.yaml --forceRollbacks
Section titled “Rollbacks”Revision History
Section titled “Revision History”Every change to the Pod template creates a new revision. Old ReplicaSets are kept (at zero replicas) as a documented history. The number of retained revisions is controlled by spec.revisionHistoryLimit (default: 10).
# View revision historykubectl rollout history deployment <name>
# Inspect a specific revisionkubectl rollout history deployment <name> --revision=2Documenting change causes — the CHANGE-CAUSE column in rollout history is blank unless you annotate manually:
kubectl annotate deployment <name> kubernetes.io/change-cause="Image updated to 1.2.0"Executing a Rollback
Section titled “Executing a Rollback”# Roll back to the immediately previous revisionkubectl rollout undo deployment <name>
# Roll back to a specific revisionkubectl rollout undo deployment <name> --to-revision=2
# Monitor the rollback (it follows the same pace as a rollout)kubectl rollout status deployment <name>What a Rollback Does and Does Not Change
Section titled “What a Rollback Does and Does Not Change”| Reverted | Not reverted |
|---|---|
spec.template (container image, env, mounts) | spec.replicas |
| — | Persistent data |
History reordering: when you roll back to revision 1 while on revision 2, Kubernetes promotes the revision 1 configuration to become the newest revision (revision 3) and removes the original revision 1 entry.
rollout undo vs. Reapplying an Old Manifest
Section titled “rollout undo vs. Reapplying an Old Manifest”These two rollback approaches are not equivalent:
| Method | What it reverts | What it preserves |
|---|---|---|
kubectl rollout undo | Pod template only (spec.template) | Replica count, strategy, all other settings |
kubectl apply with old manifest | Everything in the file | Nothing — blindly overwrites all live settings |
Use rollout undo for surgical version rollbacks. Avoid applying old manifests as a rollback mechanism — you risk overwriting live operational changes (e.g., a scaled-up replica count) with outdated values.
Deployment Strategies
Section titled “Deployment Strategies”Beyond the built-in RollingUpdate and Recreate modes, more advanced release patterns can be implemented using combinations of standard Kubernetes objects.
Canary
Section titled “Canary”Route a small slice of traffic to a new version before committing to a full rollout.
Partial canary (single Deployment):
- Set a high
minReadySecondsto slow the rollout — gives time to observe stability at each step - Or: trigger an update → immediately
kubectl rollout pauseafter the first new Pod starts → inspect → resume
True canary (dual Deployments):
- Maintain two separate Deployments: stable (e.g., 9 replicas) and canary (e.g., 1 replica)
- Configure a single Service whose selector matches Pods from both — traffic is split ~90/10
- Once confident, perform a rolling update on the stable Deployment and delete the canary Deployment
A/B Testing
Section titled “A/B Testing”Route specific user segments to a different version based on request attributes (headers, cookies, location, user-agent).
| Component | Role |
|---|---|
| Two Deployments | Version A (stable) + Version B (new) |
| Two Services | One targeting each Deployment |
| Ingress with conditional routing | Evaluates request attributes and directs traffic to Service-A or Service-B |
- A single user is always routed to the same version (session consistency required)
- Requires a Layer-7 Ingress controller that supports header/cookie-based routing — standard Kubernetes Services route randomly across all matching Pods
Blue/Green
Section titled “Blue/Green”Deploy a complete parallel environment, test it, then switch all traffic at once with zero incremental rollout.
# Switch traffic from Blue to Green by patching the Service selectorkubectl patch service my-app -p '{"spec":{"selector":{"col":"green"}}}'| Aspect | Detail |
|---|---|
| Setup | Two Deployments with distinct labels (e.g., col: blue, col: green) + one Service |
| Switch | Update the Service selector — atomic, instant cutover |
| Rollback | Revert the Service selector to Blue |
| Native support | ✅ No third-party tools required — works with standard Deployments and Services |
Traffic Shadowing (Dark Launch)
Section titled “Traffic Shadowing (Dark Launch)”Test a new version under real production load without affecting any users.
- Deploy the new version as a separate Deployment with labels that do not match the primary Service selector
- A proxy/Ingress mirrors each incoming request to both the stable and shadow Pods simultaneously
- Only the stable response is returned to the user — the shadow response is silently discarded
- Requires an Ingress controller or service mesh that supports traffic mirroring (not available natively in Kubernetes)
Network Access
Section titled “Network Access”A Deployment manages Pods but does not expose them to network traffic. To route requests to your Pods, create a separate Service object:
apiVersion: v1kind: Servicemetadata: name: my-appspec: type: LoadBalancer selector: app: my-app # must match Deployment's Pod template labels ports: - protocol: TCP port: 80 targetPort: 8080kubectl apply -f service.yaml
# Find the assigned external IPkubectl get svc my-appThe Service discovers Pods via label selector — it has no direct reference to the Deployment itself. Changing the Service’s selector is how blue/green cutover works.
Deleting Deployments
Section titled “Deleting Deployments”kubectl delete deployment <name>Deletion cascades: the Deployment, its ReplicaSets, and all managed Pods are removed automatically by the garbage collector.
# Verify full cleanupkubectl get deployments,replicasets,pods