Skip to content

Scale vCluster workloads in dedicated mode

Run one central Kedify KEDA/KPA pair and let it reconcile ordinary ScaledObjects and ScaledJobs inside automatically discovered vClusters. Each member has its own targets, authentication resources, generated KPAs, and Jobs. Demand is evaluated independently for each member.

This walkthrough selects dedicated mode and uses CPU scaling to demonstrate two independent member workloads. For central DistributedScaledObjects that distribute demand across members, follow the distributed vCluster walkthrough.

  • A central cluster with a compatible Kedify 0.8.1 release bundle or later installed in keda, on a Kubernetes version supported by its KPA chart.
  • A working host metrics.k8s.io API. Check kubectl top pods -A in the host context before creating shared-node members.
  • A default StorageClass that can provision persistent volumes for the vCluster control planes. If you need another class, use the vCluster storage configuration.
  • Network access from central Agent, KEDA, and KPA to each member API. See multi-cluster networking.

Merge these values into your central Agent’s maintained Helm configuration and apply your usual Helm or GitOps upgrade:

global:
features:
multicluster:
enabled: true
type: dedicated
vClusterDiscoveryEnabled: true
keda:
enabled: true
kpa:
enabled: true

The chart enables multicluster reconciliation in KEDA and KPA, selects KPA as the default autoscaler, and disables the Agent’s DistributedScaledObject and DistributedScaledJob controllers. Leave agent.features.multicluster.enabled, deprecated DSO/DSJ switches, namespace watch restrictions, and KEDA label sharding disabled in these controller releases.

Automatic discovery uses vCluster’s native tenant-admin exports. The Agent copies the five live central CRDs into each member: scaledobjects.keda.sh, scaledjobs.keda.sh, triggerauthentications.keda.sh, clustertriggerauthentications.keda.sh, and kedifypodautoscalers.autoscaling.kedify.io. Enabling both chart dependencies supplies the matching central CRDs.

After all five member CRDs are established, the Agent creates a managed kubeconfig Secret and ClusterRegistration in its installation namespace. It periodically reconciles the CRDs, refreshes credentials when the native export changes, and removes the managed registration when its source disappears. The native export remains unchanged.

We recommend integrations.metricsServer.enabled=true by default for shared-node vClusters used with Kedify. It forwards host CPU/memory metrics into the member API, which central KPA uses for resource scaling. This setting requires working host metrics; it does not install a host metrics-server.

The commands use vCluster’s version defaults. Keep the CLI current to use the current stable release. For reproducible deployments, add --version <version> to Helm or --chart-version <version> to the CLI. Review vCluster’s shared-node prerequisites for your host and selected release.

Terminal window
for member in a b; do
helm upgrade --install "vc-$member" vcluster \
--repo https://charts.loft.sh \
--namespace "vcluster-$member" --create-namespace \
--set integrations.metricsServer.enabled=true \
--wait --timeout 5m
done

--connect=false keeps the current central context unchanged. The commands below use vcluster connect command mode for member operations. Central controllers connect through each member’s internal Service using the exported credentials. No member KEDA/KPA deployment is needed.

Run this from the central cluster context:

Terminal window
CENTRAL_CONTEXT=$(kubectl config current-context)
kubectl --context "$CENTRAL_CONTEXT" -n keda get clusterregistrations
for member in a b; do
registration=$(kubectl --context "$CENTRAL_CONTEXT" -n keda get clusterregistrations -o json |
jq -er --arg member "vc-$member.vcluster-$member" \
'.items[] | select(.spec.provider == "vcluster" and .spec.clusterName == $member) | .metadata.name') || break
kubectl --context "$CENTRAL_CONTEXT" -n keda wait \
--for=condition=Ready "clusterregistration/$registration" --timeout=120s || break
vcluster connect "vc-$member" -n "vcluster-$member" -- kubectl get crd \
scaledobjects.keda.sh scaledjobs.keda.sh \
triggerauthentications.keda.sh clustertriggerauthentications.keda.sh \
kedifypodautoscalers.autoscaling.kedify.io
done
kubectl --context "$CENTRAL_CONTEXT" -n keda logs deploy/keda-operator --tail=100
kubectl --context "$CENTRAL_CONTEXT" -n keda logs deploy/kpa --tail=100

Confirm a registration exists for each expected member, both are ready, and both central KEDA and KPA connect. KedifyConfiguration connectivity alone does not verify dedicated consumers.

4. Deploy a CPU-scaled workload in each member

Section titled “4. Deploy a CPU-scaled workload in each member”

Each Deployment declares CPU requests so KPA can calculate utilization. Its container stays idle until the load step. Apply the ordinary ScaledObject in the same member as its target:

Terminal window
for member in a b; do
vcluster connect "vc-$member" -n "vcluster-$member" -- kubectl apply -f - <<EOF_WORKER
apiVersion: v1
kind: Namespace
metadata:
name: scaling-demo
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: worker-$member
namespace: scaling-demo
spec:
replicas: 1
selector:
matchLabels:
app: worker-$member
template:
metadata:
labels:
app: worker-$member
spec:
containers:
- name: worker
image: busybox:1.37.0
command: ["sh", "-c", "sleep 3600"]
resources:
requests:
cpu: 100m
memory: 16Mi
limits:
cpu: 500m
memory: 64Mi
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: worker-$member
namespace: scaling-demo
annotations:
autoscaling.kedify.io/class: kpa
spec:
scaleTargetRef:
name: worker-$member
minReplicaCount: 1
maxReplicaCount: 3
advanced:
horizontalPodAutoscalerConfig:
behavior:
scaleDown:
stabilizationWindowSeconds: 60
triggers:
- type: cpu
metricType: Utilization
metadata:
value: "70"
EOF_WORKER
vcluster connect "vc-$member" -n "vcluster-$member" -- kubectl -n scaling-demo rollout status "deployment/worker-$member" --timeout=120s
vcluster connect "vc-$member" -n "vcluster-$member" -- kubectl top pods -n scaling-demo
vcluster connect "vc-$member" -n "vcluster-$member" -- kubectl -n scaling-demo get scaledobject,kpa
done

Wait until kubectl top pods returns member pod metrics before generating load. KEDA creates each KPA in its member; central KPA evaluates that member’s pod metrics and patches the member Deployment’s scale. CPU-only scaling needs a running pod to supply demand, so this example keeps a minimum of one replica.

For external triggers, the ScaledObject and authentication resources also belong to the member. Central KEDA evaluates them with member-scoped clients, and central KPA requests the corresponding member-scoped external metrics over gRPC. Ordinary ScaledJobs and their generated Jobs likewise belong to their member.

In one terminal, watch member A’s replicas:

Terminal window
vcluster connect vc-a -n vcluster-a -- kubectl -n scaling-demo get deployment worker-a -w

In another terminal, generate CPU load in one of member A’s worker pods for about two minutes. The container’s CPU limit bounds the load:

Terminal window
vcluster connect vc-a -n vcluster-a -- kubectl -n scaling-demo exec deployment/worker-a -- \
sh -c 'end=$(( $(date +%s) + 120 )); while [ "$(date +%s)" -lt "$end" ]; do :; done'

Check both members’ metrics and autoscaler conditions:

Terminal window
for member in a b; do
vcluster connect "vc-$member" -n "vcluster-$member" -- kubectl top pods -n scaling-demo
vcluster connect "vc-$member" -n "vcluster-$member" -- kubectl -n scaling-demo describe kpa "keda-hpa-worker-$member"
vcluster connect "vc-$member" -n "vcluster-$member" -- kubectl -n scaling-demo get deployment "worker-$member"
done

Member A should scale above one replica, up to the configured maximum of three. Member B should remain at one while idle. After the load ends, allow time for metrics collection and the 60-second scale-down stabilization window before expecting A to return to one.

Delete the demo resources while the members are still connected, so central controllers can remove generated KPAs:

Terminal window
for member in a b; do
vcluster connect "vc-$member" -n "vcluster-$member" -- kubectl delete namespace scaling-demo
done

Then delete the demo vClusters through the Helm or vCluster CLI workflow you used to create them. The Agent removes their managed registrations and kubeconfig Secrets when the native exports disappear.

  • No registration: inspect Agent logs and VClusterBootstrapFailed events. All five source CRDs must exist centrally, and the native export must permit member CRD installation.
  • Ready registration, but no KPA: check both central controller logs, the member ScaledObject conditions, and the autoscaler class. Readiness proves API connectivity; workload reconciliation needs both dedicated consumers.
  • CPU metrics unavailable: check host and member kubectl top pods, the metrics-server integration, and container CPU requests. Private-node vClusters need a metrics API for their own nodes; shared-node host forwarding is not a substitute.
  • Unexpected central scaling: keep the central mode dedicated and create ordinary ScaledObjects in members. DistributedScaledObjects belong to the separate distributed walkthrough.

See diagnose multi-cluster controllers and KPA behavior for further checks.