Skip to content

For the complete documentation index and AI-optimized content, see /llms.txt. All pages support markdown format via .md extension or Accept: text/markdown header.

Scale Adapter

For the complete documentation index and AI-optimized content, see /llms.txt. All pages support markdown format via .md extension or Accept: text/markdown header.

ScaleAdapter (autoscaling.kedify.io/v1alpha1, short name sca) lets KEDA and the Kubernetes HPA scale resources whose /scale subresource is incomplete, or that have no /scale subresource at all. It is available starting with Kedify Agent v0.6.8.

KEDA can target any custom resource that exposes the Kubernetes /scale subresource. Some operators, however, expose a partially functional implementation: desired replicas can be read and written, but the scale selector is missing or empty.

Agones Fleet is the motivating example. Its /scale endpoint reads and writes replicas just fine, but the returned Scale object has an empty status.selector (the CRD points at a status field that Agones never populates and that gets pruned by the structural schema, see agones#2051). The HPA controller rejects any scale target with an empty selector before it evaluates a single metric, so a KEDA-generated HPA for a Fleet stays in ScalingActive=False with reason InvalidSelector forever.

The adapter sits between the HPA and the target and normalizes the scale contract:

KEDA scaler ──metric──> HPA (or KEDA for 0<->1 activation)
│ writes /scale.spec.replicas
ScaleAdapter exposes a complete /scale: replicas + selector
│ forwards desired replicas
target /scale e.g. Agones Fleet

KEDA and the HPA keep all their responsibilities: metric evaluation, min/max bounds, stabilization, and scaling behavior. The adapter only exposes an HPA-compatible /scale subresource with an explicit Pod selector, forwards desired replica changes to the target’s /scale endpoint, and mirrors the target’s current replicas back. Any KEDA or Kedify trigger works, including scale to zero.

Enable the controller on the Kedify Agent helm chart and grant RBAC for the target kind’s /scale subresource, which is not part of the default role:

agent:
features:
scaleAdaptersEnabled: true
extraRbacRules:
- apiGroups: ["agones.dev"]
resources: ["fleets/scale"]
verbs: ["get", "update"]

The env var equivalent of the feature toggle is SCALE_ADAPTER_ENABLED (default false). In this default mode the controller only uses the target’s /scale endpoint; it never needs update permission on the whole target resource. The exception is targets without a /scale subresource, which need a wider grant.

First, label the Fleet’s Pod template so the adapter has something to select. Agones does not put a per-fleet label on Pods by default, and the label has to go on the inner Pod template (spec.template.spec.template); labels on the GameServer template end up on GameServer objects, not on Pods:

apiVersion: agones.dev/v1
kind: Fleet
metadata:
name: game-fleet
spec:
template:
spec:
template:
metadata:
labels:
kedify.io/scale-target: game-fleet
# ...

Create the adapter. Leave spec.replicas unset; the controller initializes it from the Fleet, so creating an adapter never changes the Fleet’s replica count:

apiVersion: autoscaling.kedify.io/v1alpha1
kind: ScaleAdapter
metadata:
name: game-fleet
namespace: default
spec:
targetRef:
apiVersion: agones.dev/v1
kind: Fleet
name: game-fleet
selector:
matchLabels:
kedify.io/scale-target: game-fleet

Then point a regular ScaledObject at the adapter instead of the Fleet:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: game-fleet
namespace: default
spec:
scaleTargetRef:
apiVersion: autoscaling.kedify.io/v1alpha1
kind: ScaleAdapter
name: game-fleet
minReplicaCount: 0
maxReplicaCount: 100
triggers:
- type: metrics-api
metadata:
# any KEDA or Kedify trigger works

Scaling to and from zero is handled by KEDA writing to the adapter’s /scale directly (activation bypasses the HPA), which the adapter forwards like any other replica change.

For targets that do not implement the scale contract at all, spec.desiredReplicasPath switches the adapter to explicit replica field paths instead of the target’s /scale subresource. This makes anything with a numeric “how many” field scalable by KEDA, including resources that represent systems outside the cluster, such as Crossplane managed resources for VM pools or autoscaling groups:

apiVersion: autoscaling.kedify.io/v1alpha1
kind: ScaleAdapter
metadata:
name: worker-pool
namespace: default
spec:
targetRef:
apiVersion: example.org/v1
kind: Pool
name: worker-pool
selector:
matchLabels:
kedify.io/scale-target: worker-pool
desiredReplicasPath: ".spec.size"
currentReplicasPath: ".status.size" # optional
  • Paths are simple dot-separated field paths; wildcards and array indices are not supported.
  • currentReplicasPath is optional. When unset, status.replicas mirrors the desired replica count. Pods are deliberately not counted, since field-path targets may have none.
  • In this mode the adapter reads and updates the whole target resource instead of its /scale subresource, with reads served from an informer cache, so the RBAC grant widens accordingly:
agent:
extraRbacRules:
- apiGroups: ["example.org"]
resources: ["pools"]
verbs: ["get", "list", "watch", "update"]
  • The HPA still requires a non-empty status.selector, so spec.selector remains required. For targets without Pods a nominal selector works, and only external metrics with an AverageValue target (KEDA’s default) make sense, since pods and resource metrics and Value targets count the selected Pods. The SelectorMatchesPods condition is not evaluated in this mode.
  • Initialization, forwarding, conflict detection, and scaling to and from zero behave exactly as with /scale targets.
  • Initialization. On the first reconciliation, the controller reads the target’s /scale, copies its desired replicas into spec.replicas and current replicas into status.replicas, and serializes spec.selector into status.selector.
  • Forwarding. Whenever spec.replicas differs from the target’s desired replicas, the controller updates the target through its /scale endpoint, or through desiredReplicasPath when configured, never by touching any other field.
  • Mirroring. The target’s current replicas are continuously mirrored into status.replicas, which the HPA uses for per-pod metric math.
  • Single writer. Exactly one component may manage the target’s desired replicas. Do not combine the adapter with a native autoscaler (for Agones, a FleetAutoscaler) on the same target. The adapter is authoritative: if something else changes the target’s desired replicas, the adapter overwrites them, raises the ConflictingReplicaWriter condition, and emits a Warning event with reason ExternalReplicaChange.
TypeMeaning
ReadyTarget resolved, selector usable, replicas synchronized
TargetResolvedspec.targetRef maps to a resource with a readable /scale (or readable replica field paths)
SelectorValidspec.selector is a valid, non-empty selector
SelectorMatchesPodsWarning only: False when the target reports replicas but the selector matches no Pods (labels missing from the Pod template)
ConflictingReplicaWriterWarning only: True when something else wrote the target’s desired replicas

The HPA validates that status.selector is non-empty and parseable before evaluating metrics. For external metrics with an AverageValue target (KEDA’s default), that is all the selector is used for. For external metrics with a Value target, and for pods and resource metrics, the HPA counts the Pods matched by the selector, so spec.selector must actually match the workload’s Pods. The SelectorMatchesPods condition flags a mismatch.

  • The target must expose a readable and writable /scale subresource, or explicit replica field paths must be configured through spec.desiredReplicasPath.
  • The target must live in the same namespace as the adapter; cluster-scoped and cross-namespace targets are not supported.
  • The adapter never computes desired replicas from metrics; that stays with KEDA and the HPA.