Stuck GKE Upgrade: Reading the Operation and Unblocking It
A failed or frozen GKE upgrade is an operation with an error, a set of blocked pods, or a quota wall. Get the exact operation message first — everything else is guessing.
The operation is the source of truth
gcloud container operations list --filter="status!=DONE" --format="table(name,type,status)" gcloud container operations describe OP_NAME --format=yaml | sed -n '1,60p' # cluster-level state: gcloud container clusters describe CLUSTER --format="value(status,currentMasterVersion,statusMessage)"
A cluster stuck in RECONCILING is mid-operation; do not start another operation on top of it. The describe output's statusMessage and nested operationProgress name the blocking step (control plane, a specific node pool, or an addon).
The PDB block: upgrades that wait forever
Node upgrades drain nodes one surge at a time; a PodDisruptionBudget that cannot be satisfied stops the drain, and the upgrade sits at a percentage for hours. Find it:
kubectl get pdb -A kubectl get events -A --field-selector reason=EvictionFailed 2>/dev/null kubectl top nodes 2>/dev/null; kubectl get pods -A -o wide | grep -c NODE_TO_DRAIN
Typical culprits: minAvailable: 100%-style PDBs, a single-replica Deployment with a PDB, or HPA-starved workloads that cannot place a replacement because the pool is at capacity. Fix the budget or add surge capacity, and the drain resumes; forcing a node delete instead converts a controlled upgrade into an outage.
Node-pool repair loops and broken nodes
- A node pool stuck in
REPAIRINGusually means auto-repair keeps failing the same health check (kubelet not ready after reboot). Read the pool'sconditionsand the node's events; a bad DaemonSet or a node-local disk error will loop repair forever. - Node image incompatibility: workloads pinned to kernel features or hostPath state that the new image changes — test in a staging pool first (
--node-poolupgrade order matters). - After a failed node-pool upgrade, check version skew: control plane versus nodes may now sit at the edge of the supported skew window, which blocks the next step until you finish or roll the pool forward.
Quota and IP-range walls
Upgrades with surge nodes need temporary capacity: CPU quota in the region and secondary IP ranges for new pods. The operation error says which (Quota 'CPUS' exceeded, or no free IP addresses in subnet). Fixes: request quota before the window; widen the pod range or reduce --max-pods-per-node planning; lower --surge/--max-surge so the upgrade fits inside what you have.
Retry discipline
- Let the failed operation finish or be aborted cleanly — never parallel operations.
- Fix the named blocker (PDB, quota, repair loop).
- Upgrade node pools explicitly after the control plane, one pool at a time, watching
kubectl get nodesand workload readiness between pools. - Keep
--maintenance-windowand release-channel expectations aligned with the team; surprise auto-upgrades are a process bug, not a cluster bug.
Removed APIs: the upgrade failure that happens before the upgrade
Each minor version retires API versions; manifests still using them apply fine today and fail after the control plane moves. The warnings are already visible if you look: kubectl prints deprecation warnings on apply, and audit/scanning tools (kubent, pluto) list every object on a dead version per cluster. Run that scan before scheduling, fix manifests in CI (where the same deprecation warnings should fail the pipeline), and only then upgrade the control plane. The post-upgrade symptom is distinctive: new deploys error with "no matches for kind ... in version" while existing workloads keep running — a manifest problem, not a cluster problem, but it pages like one.
Operation error → fix, quick reference
| Operation message | Fix |
|---|---|
| PDB blocking eviction | relax budget or add capacity; never delete nodes under it |
| Quota 'CPUS'/'IN_USE_ADDRESSES' exceeded | request quota or lower surge before retry |
| no free IPs in subnet | expand secondary range / reduce max-pods-per-node |
| node pool REPAIRING loop | read pool conditions + node events; fix the failing DaemonSet/disk |
| version skew error | finish control-plane then pools in order; respect the skew window |
Channels, windows and auto-upgrade surprises
Release channels decide when Google can move your version; a cluster on Rapid that "upgraded itself" mid-quarter is the channel doing its documented job. Keep maintenance windows configured so enforced upgrades land in staffed hours, and read the channel's release notes the way you read dependency changelogs. Node auto-upgrade follows the control plane within a window — if a pool shows a version you did not choose, check the channel and the auto-upgrade flag before suspecting a rogue actor; and keep one unenrolled staging cluster to rehearse each minor version before the production window.
Prevention checklist
- Audit PDBs before upgrade season; a PDB that can never allow a drain is a landmine with a schedule.
- Reserve surge quota as part of the upgrade runbook, not during the incident.
- Stage upgrades: one test pool, then the rest; read node events between.
- Subscribe to GKE release notes for your channel; know which version removes the API your manifests still use (
kubectl explain+ deprecation warnings in CI).