You describe the end state, not the steps. That idea underlies replicas, updates, and rollback: the cluster keeps working to match your description.
Spec beats imperative steps
A Deployment is a statement of the end state, not a list of steps: how many replicas, which image, what update strategy, which labels. The operator publishes the what, and the controller, a reconcile loop, is responsible for the how. The distinction is resource ownership, not whether a shell command was used. kubectl run normally creates a standalone Pod; its kubelet can restart containers, but no ReplicaSet maintains a replacement population after that Pod disappears. A Deployment can itself be created with a command, yet its stored specification continues guiding controllers after the client exits. Interrupting the client does not reliably indicate whether the API accepted the request: inspect the stored object before retrying or diagnosing convergence.
A controller converges the gap
The Deployment controller manages ReplicaSets, and ReplicaSet controllers maintain their Pod populations. Kubelet can restart a crashed container within the same Pod; replacing a deleted or lost Pod is a different reconciliation path. These mechanisms cooperate, but should not be collapsed into one controller that creates a Pod every few seconds.
Reconciliation is asynchronous and depends on working control-plane components and sufficient capacity. Image pull failures, quotas, admission denial or unavailable storage can prevent convergence indefinitely. Observe conditions and events. A desired replica count is a target to pursue, not proof of ready capacity or a bound on recovery time.
Updates happen through new Revision
Changing the Pod template triggers a rollout and revision; scaling alone does not. The controller adjusts old and new ReplicaSet sizes according to the rollout strategy, readiness and availability constraints. Services and their datapaths handle traffic separately, so the Deployment controller itself does not gradually switch individual connections.
A rollback restores a previous Pod template, not every field of the Deployment or external state such as a database schema. Retained ReplicaSets supply rollback history, with a default revisionHistoryLimit of ten; cleanup starts after completion. Reapplying an unchanged template does not generate a fresh revision merely because a pipeline ran.
When it breaks
In apps/v1, a Deployment selector is immutable and the Pod template must match it. A template that violates the selector is rejected by API validation, rather than creating an accepted Deployment that endlessly produces unowned Pods. Plan selector changes as an explicit migration. Overlapping controllers deserve scrutiny, but matching labels alone do not erase controlling owner references.
Deleting a managed Pod or directly scaling an owned ReplicaSet can be corrected by its controllers. In contrast, kubectl scale deployment updates the Deployment’s desired replicas and persists until another actor changes that field. HPA, a later apply or a GitOps reconciler can overwrite it; the Deployment does not secretly remember an earlier Git value on its own.
A stalled rollout can retain more ReplicaSets than the intended historical limit because cleanup has completion conditions. Inspect rollout status before declaring a retention leak. Keep external change history and a tested rollback plan; zero retained history removes normal rollback targets, while a large number alone does not prove control-plane performance degradation.
Check it quickly
Save the following as desired_state_loop.py and run python3 desired_state_loop.py. It uses only the standard library and illustrates a simplified count-reconciliation loop: compare desired against observed, emit one corrective action per tick, and repeat until the gap closes.
"""Simulate a Deployment-style desired-state reconcile loop (stdlib only)."""
def reconcile_step(desired, observed):
if observed < desired:
return "create-one-pod"
if observed > desired:
return "delete-one-pod"
return "in-sync"
def converge(desired, observed, log):
while observed != desired:
action = reconcile_step(desired, observed)
log.append(action)
if action == "create-one-pod":
observed += 1
else:
observed -= 1
log.append("in-sync")
return observed
def main():
desired = 3
observed = 1 # one survivor after a node loss
actions = []
observed = converge(desired, observed, actions)
assert observed == desired
assert actions == ["create-one-pod", "create-one-pod", "in-sync"], actions
# Manual drift: someone adds two pods without changing desired.
observed = 5
drift = []
observed = converge(desired, observed, drift)
assert observed == desired
assert drift == ["delete-one-pod", "delete-one-pod", "in-sync"], drift
# Model a selector/template validation check before accepting desired state.
selector = {"app": "api"}
template_labels = {"app": "api-v2"} # deliberate mismatch, a negative control
matched = all(template_labels.get(k) == v for k, v in selector.items())
assert not matched, "mismatched template must fail this validation model"
print(f"desired={desired} final-observed={observed}")
print(f"recovery-actions={actions}")
print(f"observed-count-drift-corrected={drift}")
print("PASS: controller closes the desired-vs-observed gap; mismatched template is rejected by the local validation check")
if __name__ == "__main__":
main()
The two integer-count scenarios end in sync. The separate label check rejects the mismatched template as a negative control, illustrating the contract checked before API acceptance. It does not submit a real Deployment or model adoption, watches, scheduling and readiness.
Both language fixtures passed this review with Python 3.12.14 on Linux. The model checks convergence arithmetic, not scheduling or storage. Before release day, confirm the live selector, replicas, and revision history.
Further reading
Sources
- Kubernetes: Deployments, the Deployment resource explained, checked 2026-10-15.
- Kubernetes: Controllers, the platform and its model, checked 2026-10-15.