Hedronite · Dev Lesson · Polyglot-Dev / Python · Fri 2026-08-28

Python and the Kubernetes Deployment Rollout Census — the surge that is not a second cluster

The client lists Deployments and the ReplicaSets they own. CURRENT above DESIRED is the surge. Your loop is not a patch.

Lesson Class: Dev (Python + Kubernetes client AppsV1Api)
Cloud Referent: EKS leftover is a second cluster; this client cannot see AWS
Paired Ops: EKS rollingUpdate / maxSurge / two ReplicaSets on one API
Paired Cert: CKA Q18 template patch the checker must not issue
Grounding: K8sUR3 Ch.13 p.216 · Ch.9 referenced · CKA Q18
List
Deployments and owned ReplicaSets. Continue-token the year-old cluster.
Tags
surge_over / stale / stalled / floor_breach.
No patch
Q18 writes one template. A loop starts a fleet rollout.
The client lists Deployments and the ReplicaSets they own. CURRENT above DESIRED is the surge. Your loop is not a patch.

<!-- hal:authoritative:yaml -->

The client lists Deployments and the ReplicaSets they own. CURRENT above DESIRED is the surge. Your loop is not a patch.

§I — Frame

The day's Ops lesson names the leftover a second EKS cluster next to a Deployment that already had maxSurge. Two ReplicaSets on one API. An ALB target group that grows by one IP. A cloned control plane that should not have been the release step.

This slot is Python touching K8s. Cert is CKA App Lifecycle. Dev stays on the client. 08-25 already listed ClusterRoleBindings and refused to delete. 08-22 already listed PersistentVolumes and refused to patch claimRef. 08-19 already read encryptionConfig and could not see etcd. 08-16 already predicted a scheduler no and refused to bind. 08-13 already mutated a RuntimeClass with kopf. 08-10 already watched events. None of those tools answer the question Ops just asked: which Deployments are mid-surge, which ReplicaSets do they own, and is anyone about to patch the image from a loop?

Call the failure the census that must not roll. A first draft will read updated_replicas < spec.replicas and call patch_namespaced_deployment so the next image lands. That is Cert's apply. It is not a census. Q18 LabSetUp plants a Deployment so a candidate can practice a strategic merge patch. A cluster-wide loop that does the same thing starts a rollout a human is still paging about, starts a rollout a canary is mid-flight on, and starts a rollout you needed as evidence. MariaDB on the wrong disk was 08-22. Anonymous delete was 08-25. Today the wrong write is a new ReplicaSet you did not mean to mint.

Today's tool does one read. AppsV1Api.list_deployment_for_all_namespaces for spec and status. AppsV1Api.list_replica_set_for_all_namespaces for owner references. It prints rows whose CURRENT is above DESIRED, whose UPDATED is below DESIRED, or whose Progressing condition is False. It does not patch. It does not create. It does not call kubectl rollout restart. It does not call aws eks create-cluster. The second cluster is a different client.

§II — Language idiom: the generated apps object

K8sUR3 Ch. 13 is blunt about how you talk to this API. The OpenAPI spec is the source. The generated client libraries are how a language consumes it (p. 216). Python's kubernetes package is that generated client. 08-25 used RbacAuthorizationV1Api because ClusterRoleBinding lives on rbac.authorization.k8s.io/v1. 08-22 used CoreV1Api because PersistentVolume is core v1. Deployment is not core. It lives on apps/v1. The client is AppsV1Api. There is no CoreV1Api.list_namespaced_deployment. Do not mix the two clients and then wonder why the method is missing.

Poulton already told you the workloads API is the apps sub-group (Ch. 6, printed p. 59). Deployments, DaemonSets, StatefulSets. Today's object is the first of those. A DaemonSet rolling update is a later tool. A StatefulSet partition is a later tool. Scope is a feature.

Load config the way 08-04 already taught. In-cluster uses the projected token. Out-of-cluster uses kubeconfig. Today's tool does not care which, so long as the ServiceAccount can get and list deployments and replicasets. A patch verb is a bug. An update verb is a bug. A create on clusters is a bug in a different SDK. The Role that owns this tool lists two resources and two verbs.

from kubernetes import client, config

def apps() -> client.AppsV1Api:
    try:
        config.load_incluster_config()
    except config.ConfigException:
        config.load_kube_config()
    return client.AppsV1Api()

The list call returns V1DeploymentList. Each item is a V1Deployment. spec.replicas is an int or None. spec.strategy is a V1DeploymentStrategy or None. status is a V1DeploymentStatus or None. Prefer the attributes. They fail loud when the generated class changes. A dict silently returns None for a typo.

status.replicas is CURRENT. status.updated_replicas is UP-TO-DATE. status.available_replicas is AVAILABLE. status.ready_replicas is the Ready count. status.unavailable_replicas is the hole. Poulton's mid-rollout row was DESIRED 10, CURRENT 11, UP-TO-DATE 5, AVAILABLE 11 (Ch. 6, printed p. 73). Those four numbers are four attributes. They are not a parsed kubectl line.

ReplicaSets are the other list. K8sUR3 Ch. 9, p. 143: the Deployment manages a ReplicaSet by label selector, and you can query that set. The honest join for a census is owner_references, not a hope that the labels are unique. A free ReplicaSet with the same app label is a finding of a different kind. Do not hide it inside the Deployment row. Print it as orphan_rs.

None on spec.replicas is not zero. A Deployment that omitted the field defaults to 1 on the API after apply, and the generated object may still show None if you built it in memory. Read the live object. status None on a brand new Deployment is a race, not a quiet cluster. Retry once. A second None is exit 2 against yourself: you listed an object the API has not populated. Do not coerce it to zeros and call the fleet healthy.

IntOrString is how maxSurge arrives as 1 or 25%. Compare after normalizing. 1 and "1" are the same floor. 25% of desired 10 is 3 after Kubernetes rounding, not 2.5. The census does not re-implement the rounding. It prints the field the API stored and tags floor_breach only when available is below desired minus the implied hole you can compute without guessing. If you cannot compute the hole (percent of a moving HPA desired), print the raw field and skip the tag. A wrong floor is worse than no floor.

from dataclasses import dataclass
from typing import Optional

@dataclass(frozen=True)
class RolloutRow:
    namespace: str
    name: str
    desired: int
    current: int
    updated: int
    available: int
    max_surge: Optional[str]
    max_unavailable: Optional[str]
    progressing: Optional[str]
    progressing_reason: Optional[str]
    new_rs: Optional[str]
    old_rs: Optional[str]
    tag: str

tag is the coined name, not a color. surge_over when current > desired. stale when updated < desired. stalled when Progressing is False and the reason is ProgressDeadlineExceeded. floor_breach when available is below the floor implied by maxUnavailable. quiet when the four numbers agree. A quiet row with a second EKS cluster in the same account is Ops's leftover. This client will not see that cluster. Absence from this list is not absence from AWS.

§III — The census: page, join, do not patch

Year-old clusters have more than 500 Deployments. list_deployment_for_all_namespaces pages. 08-25 already paid this tax on ClusterRoleBindings. Pay it again. A tool that reads page one and exits 0 is a lie about page two.

def list_deployments(api: client.AppsV1Api):
    _continue = None
    while True:
        resp = api.list_deployment_for_all_namespaces(
            limit=200,
            _continue=_continue,
        )
        yield from resp.items
        _continue = resp.metadata._continue
        if not _continue:
            return

Same shape for ReplicaSets. Build an index keyed by (namespace, owner_name) from owner_references where kind == "Deployment" and controller is true. Two sets under one owner is the live surge Poulton drew (Ch. 6, printed p. 64). Sort them by status.replicas descending. The high one is the new set if a walk is live. The zero one is the rollback handle. If both are non-zero, the walk is mid-air. Print both names.

Strategy fields arrive as IntOrString. Read .spec.strategy.rolling_update.max_surge and max_unavailable as the object's str() or the raw value. A missing rolling_update with type == "Recreate" is not a surge. Tag it recreate and move on. Recreate is downtime K8sUR3 already named (Ch. 9, p. 155). It is not this fire's leftover.

Conditions are a list. Find type == "Progressing". Status True and reason NewReplicaSetAvailable is a finished walk. Status False and reason ProgressDeadlineExceeded is a stalled walk. Status True and reason ReplicaSetUpdated is still walking. Do not invent a fourth reason. Print the one the API sent.

def progressing(dep: client.V1Deployment):
    conds = dep.status.conditions or []
    for c in conds:
        if c.type == "Progressing":
            return c.status, c.reason
    return None, None

Exit codes are part of the tool. 08-25 used 1 for a leftover row and 2 for 403. Keep that. 0 means every Deployment is quiet. 1 means at least one surge_over, stale, stalled, or floor_breach row. 2 means the ServiceAccount cannot list. A 403 is not a quiet cluster. It is a Role you have not minted.

Forbidden writes, named so a later Friday cannot "improve" them in.

  1. patch_namespaced_deployment. That is Q18. That is a rollout.
  2. replace_namespaced_deployment. Same rollout, heavier body.
  3. create_namespaced_replica_set. The Deployment owns that object.
  4. Any boto3 create_cluster or create_nodegroup. Wrong SDK, wrong leftover.

Read-only is the product. A green census on a cloned EKS API is not a release. A stale row on one API is the surge that is not a second cluster.

Namespace filters belong on the Role, not in a hardcoded skip list inside the tool. Skipping kube-system because "those are not ours" hides the Recreate add-on that just dropped AVAILABLE to 0. Print every Deployment the token can see. A later flag can narrow. The default is the fleet.

Owner references can be empty on a ReplicaSet a human applied by hand. That set will not join a Deployment row. It will appear in the orphan walk. If its labels match a Deployment selector, Poulton's "do not manage ReplicaSets directly" sentence is the finding (Ch. 6, printed p. 60). Print the name. Do not adopt it by writing an owner reference. Adoption is a write.

§IV — Worked example: print the two sets, leave the image

A fleet has storefront in prod. Desired 10. Someone applied 1.4.3. maxSurge is 1. The API currently reports current 11, updated 5, available 10. Two ReplicaSets: storefront-6f8677b5b at 5 and storefront-65cbc9474c at 6. Progressing is True, reason ReplicaSetUpdated.

The census prints one row.

prod/storefront desired=10 current=11 updated=5 available=10 surge=1 unavail=1 progressing=True reason=ReplicaSetUpdated new=storefront-6f8677b5b old=storefront-65cbc9474c tag=surge_over,stale

Exit 1. Do not patch. Do not undo. Undo is Cert. The human who owns the image decides. The tool owns the row.

A second Deployment, checkout in prod, desired 4, current 4, updated 4, available 2, maxUnavailable 0, Progressing False, ProgressDeadlineExceeded. Tag stalled,floor_breach. The new set never became Ready. minReadySeconds never elapsed. This is the bad-image case Ops named. Printing it is the job. Recreating the cluster is the failure.

A third Deployment, admin in kube-system, type Recreate. Tag recreate. No surge columns. A Recreate walk will drop AVAILABLE to 0 on purpose. Do not page it as floor_breach unless you also print the strategy type. The strategy is why.

Pagination test: a cluster with 650 Deployments. _continue is set after 200 and after 400. If you drop the token, you report 200 quiet rows and miss the stalled one on page three. 08-25 already said this about bindings. Say it again about Deployments. The year-old cluster is the real fixture.

Q18's stem is the write this tool must not perform. The candidate patches CPU and memory with --type=strategic. The Deployment remains available at 2 replicas because maxUnavailable allows the walk. If your census sees checkout after that patch, you will see current maybe 3, updated climbing. That is expected. A helper that then "fixes" the image to the old tag is a rollback the candidate did not ask for.

§V — Prior-lesson reach

08-25 listed ClusterRoleBindings, printed anonymous subjects, and refused to delete. Today's shape is the same shape on a different group. AppsV1Api instead of RbacAuthorizationV1Api. A ReplicaSet instead of a subject. Patch instead of delete as the forbidden write. The reason is the same: LabSetUp is a one-object edit. A loop is a fleet event.

08-22 listed PersistentVolumes, printed claimRef, and refused to patch. Today's refusal is the same verb on a different object. A leftover checker that patches a Deployment mints Pods. A leftover checker that patched a PV reused the wrong disk. Both writes look like cleanup. Both are incidents.

08-19 read encryptionConfig and could not see etcd. Today's analog: the client can see ReplicaSets and cannot see whether someone opened a second EKS cluster in the account. Two blinds. Two honest tools. Do not pretend a quiet Deployment list proves aws eks list-clusters is still length one.

08-16 predicted a scheduler no and did not bind. Today predicts nothing. It reports objects that already exist. A fit-checker that binds is a scheduler. A census that patches is a Deployment controller. Neither is this process.

08-10 watched events with resourceVersion. Today's list is not a watch. Rollouts last minutes, not weeks. A watch that never reconnects looks like a finished walk. A periodic list that exits is enough.

One more collision to refuse: wrapping kubectl rollout status in subprocess. That is a shell with a Python file around it. K8sUR3 p. 216 pointed you at the generated client. The status columns are attributes. If you shell out, you parse text, you lose pagination, and you inherit whatever context the sidecar kubeconfig happened to use. 08-18 already spent subprocess on a different track. Do not bring it here as a shortcut.

§VI — Connection to today's Ops and Cert

Ops coined the surge that is not a second cluster. Two ReplicaSets, then a leftover API someone stood up. This file is those ReplicaSets as a typed list.

The Python client cannot flip maxSurge. It cannot create an EKS cluster. Poulton already told you the walk lives on one Deployment (Ch. 6, printed p. 64). The client sees the sets Q18's patch would move. It does not see the second cluster. If the task is the strategy block, you are in the Cert file. Stay here for the object.

Ops printed a one-shot kubectl get deploy with custom-columns. That snippet is a desk check. This lesson is the same read with pagination, exit codes, and a frozen dataclass. The desk check misses page two. The tool should not.

§VII — Close

List Deployments. Join owned ReplicaSets. Print surge_over, stale, stalled, floor_breach. Exit 1 when a row exists. Exit 2 on 403. Do not patch.

The EKS doors Ops closed are "do not clone the API." This client is the Deployment. A green census next to a second cluster is not a release. A stale row on one API is still the surge that is not a second cluster.

Examine well. The object is the finding. The patch is a later Friday. The second cluster is a later year.

Related