Symptom
One of two things:
- No scale-up: pods are
PendingwithFailedScheduling(Insufficient cpu,Insufficient memory), and the node pool size doesn't change. - No scale-down: nodes run at low utilization for hours and the pool stays at its peak size.
The Cluster Autoscaler (CA) only works on managed node pools. It doesn't manage virtual node pools, which scale per pod anyway. Work through the checklist in order; most failures are in the first four sections.
export CLUSTER=ocid1.cluster.oc1..aaaa
kubectl get pods -A --field-selector=status.phase=Pending
kubectl -n kube-system get deploy,pods -l app=cluster-autoscaler -o wide
Label selectors and deployment names vary between the add-on and self-managed installs; kubectl -n kube-system get deploy | grep -i autoscaler finds it either way.
1. Is the autoscaler running, and only once?
- The add-on is installed and
ACTIVE, or a self-managed deployment exists, but not both. Two autoscalers fight over pool sizes.
oci ce cluster list-addons --cluster-id $CLUSTER \
--query 'data[].{name:name,state:"lifecycle-state",version:"current-installed-version"}' --output table
oci ce cluster get-addon --cluster-id $CLUSTER --addon-name ClusterAutoscaler
kubectl -n kube-system get deploy | grep -i autoscaler
- The CA version matches the cluster's Kubernetes minor version. A mismatched self-managed image is a common cause after a control plane upgrade.
- Pods aren't crash-looping. Check the first 50 log lines for auth and config errors:
kubectl -n kube-system logs deploy/cluster-autoscaler --tail=200
- With multiple replicas, find the leader (only it acts). Read the logs from that pod.
2. Does the CA know about the node pool?
- The
nodesconfiguration lists every pool you expect to scale, inmin:max:nodepool-ocidformat, comma-separated:
nodes: "2:10:ocid1.nodepool.oc1..aaaa,0:20:ocid1.nodepool.oc1..bbbb"
- Or
nodeGroupAutoDiscoveryis set instead (compartment and node pool tags), on a CA version that supports it. Never set both. - The pool's current size is below
max. A pool already at max produces "max node group size reached" in the logs. - The node pool OCID is current. Re-created pools get new OCIDs, and stale ones fail silently in some versions.
oci ce node-pool get --node-pool-id $NP \
--query 'data.{size:"node-config-details".size,state:"lifecycle-state",k8s:"kubernetes-version"}'
Update add-on settings with oci ce cluster update-addon --cluster-id $CLUSTER --addon-name ClusterAutoscaler --configurations file://ca-config.json, where the file is a list of {"key": ..., "value": ...} pairs.
3. Can it call the OCI API?
The CA resizes node pools through the Container Engine API. The authType setting decides which identity it uses:
authType | Identity | Requirements |
|---|---|---|
instance (default) | Instance principal of the worker node running the CA | Dynamic group containing the worker nodes, plus policies |
workload | Workload identity for the cluster-autoscaler service account | Enhanced cluster, plus policies scoped to that service account |
- Logs show no
401,404 NotAuthorizedOrNotFoundor "failed to get node pool" errors. - For
instance: the dynamic group matches the worker nodes (for example by compartment), and policies grant the dynamic group access in the node pool compartment. - For
workload: policies use the request principal conditions:
Allow any-user to manage cluster-node-pools in compartment <compartment> where ALL {request.principal.type='workload', request.principal.namespace='kube-system', request.principal.service_account='cluster-autoscaler', request.principal.cluster_id='<cluster-ocid>'}
Allow any-user to manage instance-family in compartment <compartment> where ALL {request.principal.type='workload', request.principal.namespace='kube-system', request.principal.service_account='cluster-autoscaler', request.principal.cluster_id='<cluster-ocid>'}
Allow any-user to use subnets in compartment <compartment> where ALL {request.principal.type='workload', request.principal.namespace='kube-system', request.principal.service_account='cluster-autoscaler', request.principal.cluster_id='<cluster-ocid>'}
Allow any-user to read virtual-network-family in compartment <compartment> where ALL {request.principal.type='workload', request.principal.namespace='kube-system', request.principal.service_account='cluster-autoscaler', request.principal.cluster_id='<cluster-ocid>'}
Allow any-user to use vnics in compartment <compartment> where ALL {request.principal.type='workload', request.principal.namespace='kube-system', request.principal.service_account='cluster-autoscaler', request.principal.cluster_id='<cluster-ocid>'}
Allow any-user to inspect compartments in compartment <compartment> where ALL {request.principal.type='workload', request.principal.namespace='kube-system', request.principal.service_account='cluster-autoscaler', request.principal.cluster_id='<cluster-ocid>'}
- Network and node subnets in another compartment are covered by policies in that compartment too.
- Worker nodes can reach OCI APIs (service gateway or NAT route from the node subnet). A private cluster with no route to the Container Engine endpoint has every API call time out.
4. Would a new node actually fit the pod?
The CA simulates scheduling on a template node from the pool. If the simulated node can't run the pod, it doesn't scale, and the pod events say so.
kubectl describe pod <pending-pod> | sed -n '/Events/,$p'
# look for: "pod didn't trigger scale-up" and the reason per node group
kubectl -n kube-system get configmap cluster-autoscaler-status -o yaml
- The pod's requests fit the pool's shape after system reservations and DaemonSets. A pod requesting 14 GiB on a 16 GB node often doesn't fit.
-
nodeSelector, node affinity and tolerations match the pool's labels and taints (node poolinitial-node-labelsand taint settings), not just the labels of existing nodes. - Architecture matches: arm64-only images on an Ampere pool, amd64 on x86.
- PVC availability domain: a pod bound to a block volume in AD-1 can only use a pool with placement in AD-1.
- Topology spread constraints and pod anti-affinity can be met by adding a node in a placement the pool actually has.
5. Did the scale-up start but the node never joined?
- Check node pool work requests for launch failures:
oci ce work-request list -c $C --resource-id $NP --all \
--query 'data[].{op:"operation-type",status:status,started:"time-started"}' --output table
oci ce work-request-error list --work-request-id $WR -c $C
- Look for out-of-host-capacity,
LimitExceededor compartment quota errors. Add placement in more ADs, use a capacity reservation or add a second pool with a different shape. - Subnet IP exhaustion: node subnet, and with VCN-native pod networking, the pod subnet too.
- Nodes that launch but stay
NotReadybeyondmaxNodeProvisionTime(default 15 minutes) are treated as failed, and the CA backs off that pool. Fix the boot or join problem first (cloud-init, image, NSG to the API endpoint).
6. Why won't it scale down?
kubectl -n kube-system logs deploy/cluster-autoscaler --tail=500 | grep -iE "scale.?down|unremovable|not be removed"
kubectl get pdb -A
| Blocker | Fix |
|---|---|
PDB with ALLOWED DISRUPTIONS = 0 | Add replicas or relax the PDB |
| Pods without a controller (bare pods) | Run them through a Deployment or Job |
Pods with local storage (emptyDir) | skipNodesWithLocalStorage: false if that data is disposable |
kube-system pods without PDBs | Add PDBs, or adjust skipNodesWithSystemPods |
cluster-autoscaler.kubernetes.io/safe-to-evict: "false" | Remove the annotation where it isn't needed |
Utilization above scaleDownUtilizationThreshold | Requests, not actual usage, drive this; right-size requests |
Pool already at min | Lower min in the nodes setting |
| Recent scale-up | scaleDownDelayAfterAdd hasn't expired yet |
Warning
Lowering
scaleDownUnneededTimeand utilization thresholds aggressively on clusters with stateful or slow-starting workloads causes eviction churn. Change one parameter at a time and watch restarts and p99 latency.
7. Don't let OKE operations fight the CA
- Don't change node pool size manually (Console, Terraform
size) on CA-managed pools. In Terraform, ignore changes to the size attribute. - During node pool cycling or upgrades, expect temporary over-provisioning. Leave headroom in
maxfor surge nodes. - Use
balanceSimilarNodeGroupswhen you split one logical pool across several pools (for example one per AD).
Prevent it next time
- Manage the add-on configuration in code, with the
nodeslist generated from the pool OCIDs. - Alert on pods
Pendingfor more than 10 minutes and on CA log errors, not just on node count. - After every Kubernetes upgrade, confirm the CA version and run a scale-up test (a deployment with large requests) in a non-production cluster.
- Review PDBs and
safe-to-evictannotations quarterly; they are the usual reason clusters stop shrinking.