Symptoms
kubectl get nodesshows several nodesReady,SchedulingDisabledfor a long time.- Pods
Pendingwith0/N nodes are available. - The node pool work request in the console is
IN_PROGRESSfor much longer than usual, orFAILED.
Step 1: see the state (3 minutes)
kubectl get nodes -o wide
kubectl get pods -A --field-selector=status.phase=Pending | head -20
oci ce work-request list --compartment-id $COMP --cluster-id $CLUSTER \
--query 'data[0:5].{id:id, op:"operation-type", status:status, started:"time-started"}' --output table
oci ce work-request-error list --work-request-id $WR --compartment-id $COMP
oci ce work-request-log-entry list --work-request-id $WR --compartment-id $COMP --query 'data[-10:].message'
| Finding | Go to |
|---|---|
| Old nodes cordoned, drain never finishes | Step 2 |
New nodes created but NotReady or missing | Step 3 |
| Work request failed on capacity or limits | Step 4 |
Step 2: unblock drains
Drains stop on PodDisruptionBudgets that cannot be satisfied, and on pods with local storage or no controller.
kubectl get pdb -A
kubectl get pdb -A -o json | jq -r '.items[] | select(.status.disruptionsAllowed==0) | "\(.metadata.namespace)/\(.metadata.name) minAvailable=\(.spec.minAvailable) healthy=\(.status.currentHealthy)"'
A PDB with minAvailable equal to the replica count (or a single-replica Deployment with minAvailable: 1) blocks forever. Options, safest first:
- Scale the workload up by one so the PDB allows a disruption:
kubectl -n shop scale deploy/cart --replicas=3. - Temporarily relax the PDB:
kubectl -n shop patch pdb cart-pdb --type merge -p '{"spec":{"minAvailable":1}}'. - Drain manually with a timeout, accepting disruption for that one workload.
kubectl drain 10.0.10.15 --ignore-daemonsets --delete-emptydir-data --timeout=10m
Caution
--disable-evictionor deleting pods by hand bypasses PDBs. Use it only for workloads you have confirmed can take the outage, and tell their owners.
Step 3: new nodes do not become Ready
kubectl describe node <new-node> | sed -n '/Conditions/,/Addresses/p'
oci ce node-pool get --node-pool-id $NP --query 'data.nodes[].{name:name, state:"lifecycle-state", err:"node-error"}'
Common causes:
| Cause | Check | Fix |
|---|---|---|
| Image or Kubernetes version mismatch with a custom image | Node pool image vs cluster version | Use an OKE image matching the target version |
| Worker subnet full | Subnet free IPs (and pod subnet for VCN-native networking) | Add a node pool in a bigger subnet; free IPs |
| Security rules blocking nodes from the API endpoint (6443/12250) | Worker NSG/security list | Restore the OKE-required rules |
| cloud-init customisation failing | Serial console / console history of the node | Fix the init script |
HIST=$(oci compute console-history capture --instance-id $NODE_INSTANCE --query 'data.id' --raw-output)
sleep 20
oci compute console-history get-content --instance-console-history-id $HIST --file - | tail -50
Step 4: restore capacity now
If the workload is short of capacity, add capacity first, then finish the upgrade:
oci ce node-pool update --node-pool-id $NP --size 8 # scale the pool up
# or create a temporary pool at the target version
oci ce node-pool create --cluster-id $CLUSTER --compartment-id $COMP --name np-surge \
--kubernetes-version v1.31.1 --node-shape VM.Standard.E5.Flex \
--node-shape-config '{"ocpus":4,"memoryInGBs":32}' \
--placement-configs '[{"availabilityDomain":"'"$AD"'","subnetId":"'"$WORKER_SUBNET"'"}]' \
--size 3 --node-image-id $OKE_IMAGE
Out-of-capacity errors for a shape: try another AD or fault domain, or another shape. Service limit errors: check oci limits resource-availability get for the compute limit and request an increase.
Uncordon old nodes if you need their capacity back while you fix the new ones:
kubectl uncordon 10.0.10.15
Step 5: finish the upgrade
Once drains are unblocked and new nodes are healthy, retry the cycle. Node pool cycling settings control surge and unavailability:
oci ce node-pool update --node-pool-id $NP --node-pool-cycling-details \
'{"isNodeCyclingEnabled":true,"maximumSurge":"1","maximumUnavailable":"0"}'
maximumUnavailable: 0 with a surge of 1 adds a new node before removing an old one, which keeps capacity steady.
Step 6: confirm
- All nodes
Readyon the target version:kubectl get nodes -o custom-columns=NAME:.metadata.name,VER:.status.nodeInfo.kubeletVersion. - No
Pendingpods; PDBs back to their normal values. - Temporary node pool removed if you created one.
Prevent it next time
- Policy: no PDB may have
minAvailableequal to replicas; single-replica apps must not have a PDB that blocks drains. - Upgrade non-production first and time the cycle.
- Keep 20% free IPs in worker and pod subnets.
- Use surge-based cycling (
maximumUnavailable: 0).