How to: Install, Use and Debug the Kubernetes Cluster Autoscaler
This documentation covers how to install, use and debug the Xelon Kubernetes Autoscaler.
What is the Kubernetes Cluster Autoscaler?
The Kubernetes Cluster Autoscaler extends the regular capabilities of autoscaling. By default, Kubernetes only supports the scaling of workloads, which translates to adding or removing pods. This allows your current cluster resources to be redistributed, but it does not allow you to change the amount of resources in a given cluster. If you would like to increase or decrease resources based on current usage, you would need to do this manually by adding or removing nodes.
This is where the Cluster Autoscaler comes in: if you have unschedulable pods due to resource constraints, it can add another node, and if the average node utilisation is too low and your workload allows it, it can also remove nodes. This can improve your financial efficiency by only adding compute during peak hours.
Source repository: https://github.com/Xelon-AG/autoscaler
How to install & configure the Xelon Kubernetes Cluster Autoscaler?
Before you can install the Xelon Kubernetes Cluster Autoscaler, you have to upgrade your cluster to at least Kubernetes version 1.35.
How to get the pool IDs
You need the ID of the node pool you would like to scale. To get it, you can create a temporary debug pod like this:
kubectl -n kube-system run xelon-probe --rm -it --restart=Never \
--image=alpine:latest \
--overrides='{
"spec": {
"containers": [{
"name": "xelon-probe",
"image": "alpine:latest",
"stdin": true, "tty": true,
"env": [
{"name": "XELON_API_URL", "value": "https://hq.xelon.ch/api/v2"},
{"name": "XELON_CLIENT_ID", "valueFrom": {"secretKeyRef": {"name": "xelon-api-credentials", "key": "clientId"}}},
{"name": "XELON_CLUSTER_ID","valueFrom": {"secretKeyRef": {"name": "xelon-api-credentials", "key": "kubernetesClusterId"}}},
{"name": "XELON_TOKEN", "valueFrom": {"secretKeyRef": {"name": "xelon-api-credentials", "key": "token"}}}
]
}]
}
}' -- ash
And then get the list of worker pools in your cluster:
apk add curl jq
curl -fsS "${XELON_API_URL}/kubernetes/${XELON_CLUSTER_ID}/pools" \
-H "Authorization: Bearer ${XELON_TOKEN}" \
-H "X-User-Id: ${XELON_CLIENT_ID}" |
jq -r '.[] | [.identifier, .name, (.nodes | length)] | @tsv'
How to install with Kustomize
We recommend installing with Kustomize, so you can easily upgrade in the future by simply switching the tag. Be sure to use the autoscaler release closest to your Kubernetes version.
kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- https://raw.githubusercontent.com/Xelon-AG/autoscaler/v1.35.2-xelon.2/cluster-autoscaler/cloudprovider/xelon/examples/cluster-autoscaler.yaml
patches:
- target:
group: apps
version: v1
kind: Deployment
name: xelon-cluster-autoscaler
namespace: kube-system
patch: |-
- op: replace
path: /spec/template/spec/containers/0/args/1
value: --nodes=3:5:pt3s8gen7g
- op: replace
path: /spec/template/spec/containers/0/env/0
value:
name: XELON_BASE_URL
value: https://hq.xelon.ch/api/v2/
Be sure to adapt the first patch to your needs: --nodes=<min nodes>:<max nodes>:<pool ID> The example scales the node pool "pt3s8gen7g" between 3 and 5 nodes.
What are scale conditions?
The scale conditions can be tweaked to your needs. The following two sections explain the current defaults and behaviour.
Scale up
A new node gets added if a pod is unschedulable — because the kube-scheduler has set PodScheduled=False with Reason=Unschedulable — and the pod does not fall into any of the filters listed below:
| Filter | Drops |
|---|---|
| Expendable | priority below --expendable-pods-priority-cutoff (default -10) |
| Already schedulable | pods that bin-pack onto existing or upcoming capacity |
| DaemonSet | DS pods — a new node cannot help them |
The "already schedulable" filter is why a pending pod sometimes produces no scale-up: CA has counted capacity on a node that is still being provisioned.
Scale down
A node is removed only after passing six sequential gates. Failing any one of them resets the clock in gate 5.
Gate 1: Loop enabled and not in cooldown
| Condition | Flag | Default |
|---|---|---|
| Scale-down enabled | --scale-down-enabled |
true |
| No recent scale-up | --scale-down-delay-after-add |
10m |
| No recent failed scale-down | --scale-down-delay-after-failure |
3m |
| No recent deletion | --scale-down-delay-after-delete |
0s |
Gate 2: Candidate pre-filter
- Node belongs to an autoscaled node group.
- The group is above its
MinSize(). With--nodes=1:4, the last node is never a candidate.
Gate 3: Eligibility
- Not already being deleted (no
ToBeDeletedByClusterAutoscalertaint). - No
cluster-autoscaler.kubernetes.io/scale-down-disabled: "true"annotation. - If unready:
--scale-down-unready-enabled(defaulttrue) must be on. - Utilization at or below
--scale-down-utilization-threshold(default0.5).
Utilization here means resource requests divided by allocatable — not actual CPU/memory usage. A node sitting at 5% real memory usage but with pods requesting 90% of allocatable will never be scaled down. This is the single most common surprise. The corresponding -v=4 log line is:
Node prod-w-1-1 unremovable: memory requested (93.2809% of allocatable) is above the scale-down utilization threshold
DaemonSet and mirror pods can be excluded from the calculation via --ignore-daemonsets-utilization and --ignore-mirror-pods-utilization.
Gate 4: Simulation
Every pod on the node must:
- Be drainable (no blocking PodDisruptionBudget, not a kube-system pod without a PDB, not a pod without a controller, no local storage unless annotated
safe-to-evict) — otherwiseNode X cannot be removed: … - Have somewhere else to go — otherwise
Node X is not suitable for removal(reasonNoPlaceToMovePods)
Simulation is capped by --scale-down-simulation-timeout (30s).
Gate 5: Unneeded long enough
| Node state | Flag | Default |
|---|---|---|
| Ready | --scale-down-unneeded-time |
10m |
| Unready | --scale-down-unready-time |
20m |
The clock resets whenever a node fails any gate in a loop. A node that flickers above the utilization threshold every few minutes will never accumulate 10 continuous minutes. Track it with:
kubectl logs -n kube-system deploy/xelon-cluster-autoscaler | grep "was unneeded for"
Gate 6: Min size and resource floor
- Group size minus deletions already in flight must stay above
MinSize(). - Removing the node must not drop the cluster below the minimums in
--cores-total/--memory-total.
How to debug the autoscaler?
Debugging the autoscaler is simple: just follow the logs and check what happens between the "Starting main loop" messages.
kubectl logs -f -n kube-system deploy/xelon-cluster-autoscaler
Tip: if you have a pod that is safe to evict even though it does not belong to a redundant ReplicaSet, you can use the following annotation:
cluster-autoscaler.kubernetes.io/safe-to-evict: "true"