01 / ONE CENTRAL SCALING CONTROL PLANE KEDA and Kedify are already installed centrally. Tenant clusters will join automatically. $ kubectl -n keda get deployments NAME READY UP-TO-DATE AVAILABLE AGE keda-admission-webhooks 1/1 1 1 13m keda-operator 1/1 1 1 13m keda-operator-metrics-apiserver 1/1 1 1 13m kedify-agent 1/1 1 1 12m $ cat manifests/discovery-values.yaml global: features: multicluster: enabled: true vClusterDiscoveryEnabled: true Configure discovery once. New vClusters supply their native kubeconfig Secret and Service. 02 / CREATE TWO VCLUSTERS Standard vCluster installations. No KEDA installation or registration command inside either tenant. $ vcluster create vc-a --driver helm -n vcluster-a \ --chart-version 0.37.1 -f manifests/vcluster.yaml --connect=false 15:12:54 info Creating namespace vcluster-a 15:12:54 info Create vcluster vc-a... 15:12:58 done Successfully created virtual cluster vc-a in namespace vcluster-a. - Use 'vcluster connect vc-a --namespace vcluster-a' to access the virtual cluster - Use `vcluster connect vc-a --namespace vcluster-a -- kubectl get ns` to run a command directly within the vcluster $ vcluster create vc-b --driver helm -n vcluster-b \ --chart-version 0.37.1 -f manifests/vcluster.yaml --connect=false 15:13:02 info Creating namespace vcluster-b 15:13:02 info Create vcluster vc-b... 15:13:05 done Successfully created virtual cluster vc-b in namespace vcluster-b. - Use 'vcluster connect vc-b --namespace vcluster-b' to access the virtual cluster - Use `vcluster connect vc-b --namespace vcluster-b -- kubectl get ns` to run a command directly within the vcluster The virtual APIs start first. Kedify connects as soon as each API is ready. ELAPSED VCLUSTER A API KEDIFY A VCLUSTER B API KEDIFY B 0s Starting Waiting Starting Waiting 6s Starting Waiting Starting Waiting 15s Starting Waiting Starting Waiting 18s Ready Waiting Starting Waiting 24s Ready Waiting Starting Waiting 27s Ready Connected Starting Waiting 30s Ready Connected Ready Waiting 33s Ready Connected Ready Waiting 36s Ready Connected Ready Connected $ kubectl -n keda get kedifyconfiguration -o json | jq -r '["MEMBER", "PROVIDER", "STATE"], (.items[].status.multiClusterStatus.clusters | to_entries[] | [.key, .value.provider, .value.state]) | @tsv' | column -t MEMBER PROVIDER STATE vc-a.vcluster-a vcluster Connected vc-b.vcluster-b vcluster Connected $ kubectl -n vcluster-a get secret vc-vc-a NAME TYPE DATA AGE vc-vc-a Opaque 5 24s $ kubectl -n vcluster-b get secret vc-vc-b NAME TYPE DATA AGE vc-vc-b Opaque 5 14s These native kubeconfig exports stay in their original namespaces. Kedify follows credential updates. 03 / WORKLOADS IN TENANTS, POLICIES CENTRAL Each DistributedScaledObject selects one member and one workload. $ vcluster -s connect vc-a -n vcluster-a -- kubectl apply -f manifests/workload-a.yaml namespace/scaling-demo created deployment.apps/worker-a created $ vcluster -s connect vc-b -n vcluster-b -- kubectl apply -f manifests/workload-b.yaml namespace/scaling-demo created deployment.apps/worker-b created This demo uses two controlled demand values. Production metrics can come from queues or monitoring. $ kubectl apply -f manifests/metric-source.yaml namespace/scaling-demo created configmap/demo-metrics created $ cat manifests/dso-a.yaml apiVersion: keda.kedify.io/v1alpha1 kind: DistributedScaledObject metadata: name: worker-a namespace: scaling-demo spec: memberClusters: - name: vc-a.vcluster-a scaledObjectSpec: scaleTargetRef: kind: Deployment name: worker-a minReplicaCount: 0 maxReplicaCount: 5 pollingInterval: 5 cooldownPeriod: 15 advanced: horizontalPodAutoscalerConfig: behavior: scaleDown: stabilizationWindowSeconds: 0 triggers: - type: kubernetes-resource metadata: resourceKind: ConfigMap resourceName: demo-metrics key: a targetValue: '1' $ kubectl apply -f manifests/dso-a.yaml -f manifests/dso-b.yaml distributedscaledobject.keda.kedify.io/worker-a created distributedscaledobject.keda.kedify.io/worker-b created $ kubectl -n scaling-demo wait dso --all --for=condition=Ready --timeout=180s distributedscaledobject.keda.kedify.io/worker-a condition met distributedscaledobject.keda.kedify.io/worker-b condition met $ kubectl -n scaling-demo get dso,scaledobject,hpa \ -o custom-columns=KIND:.kind,NAME:.metadata.name KIND NAME DistributedScaledObject worker-a DistributedScaledObject worker-b ScaledObject worker-a ScaledObject worker-b HorizontalPodAutoscaler keda-hpa-worker-a HorizontalPodAutoscaler keda-hpa-worker-b 04 / SCALE EACH TENANT INDEPENDENTLY One central installation. Separate demand and replica counts for each tenant. $ kubectl -n scaling-demo patch configmap demo-metrics --type merge \ -p '{"data":{"a":"3","b":"1"}}' configmap/demo-metrics patched $ vcluster -s connect vc-a -n vcluster-a -- kubectl -n scaling-demo wait deployment/worker-a \ --for=jsonpath='{.status.availableReplicas}'=3 --timeout=180s deployment.apps/worker-a condition met $ vcluster -s connect vc-b -n vcluster-b -- kubectl -n scaling-demo wait deployment/worker-b \ --for=jsonpath='{.status.availableReplicas}'=1 --timeout=180s deployment.apps/worker-b condition met $ vcluster -s connect vc-a -n vcluster-a -- kubectl -n scaling-demo get deployments NAME READY UP-TO-DATE AVAILABLE AGE worker-a 3/3 3 3 91s $ vcluster -s connect vc-b -n vcluster-b -- kubectl -n scaling-demo get deployments NAME READY UP-TO-DATE AVAILABLE AGE worker-b 1/1 1 1 90s Tenant A: 3 replicas. Tenant B: 1 replica. Both decisions are managed centrally. 05 / ONE TENANT IDLES WHILE THE OTHER GROWS Scale A to zero while B scales from one replica to two. $ kubectl -n scaling-demo patch configmap demo-metrics --type merge \ -p '{"data":{"a":"0","b":"2"}}' configmap/demo-metrics patched $ vcluster -s connect vc-a -n vcluster-a -- kubectl -n scaling-demo wait deployment/worker-a \ --for=jsonpath='{.spec.replicas}'=0 --timeout=180s deployment.apps/worker-a condition met $ vcluster -s connect vc-a -n vcluster-a -- kubectl -n scaling-demo wait pod \ -l app=worker-a --for=delete --timeout=120s $ vcluster -s connect vc-b -n vcluster-b -- kubectl -n scaling-demo wait deployment/worker-b \ --for=jsonpath='{.status.availableReplicas}'=2 --timeout=180s deployment.apps/worker-b condition met $ vcluster -s connect vc-a -n vcluster-a -- kubectl -n scaling-demo get deployments,pods NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/worker-a 0/0 0 0 2m25s $ vcluster -s connect vc-b -n vcluster-b -- kubectl -n scaling-demo get deployments,pods NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/worker-b 2/2 2 2 2m24s NAME READY STATUS RESTARTS AGE pod/worker-b-58b458544f-c2swg 1/1 Running 0 116s pod/worker-b-58b458544f-nfr76 1/1 Running 0 11s 06 / BOTH TENANTS RETURN TO ZERO Idle workload pods disappear. The virtual APIs remain available. $ kubectl -n scaling-demo patch configmap demo-metrics --type merge \ -p '{"data":{"a":"0","b":"0"}}' configmap/demo-metrics patched $ vcluster -s connect vc-b -n vcluster-b -- kubectl -n scaling-demo wait deployment/worker-b \ --for=jsonpath='{.spec.replicas}'=0 --timeout=180s deployment.apps/worker-b condition met $ vcluster -s connect vc-b -n vcluster-b -- kubectl -n scaling-demo wait pod \ -l app=worker-b --for=delete --timeout=120s $ vcluster -s connect vc-a -n vcluster-a -- kubectl -n scaling-demo get deployments,pods NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/worker-a 0/0 0 0 2m59s $ vcluster -s connect vc-b -n vcluster-b -- kubectl -n scaling-demo get deployments,pods NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/worker-b 0/0 0 0 2m58s $ kubectl -n keda get kedifyconfiguration -o json | jq -r '["MEMBER", "PROVIDER", "STATE"], (.items[].status.multiClusterStatus.clusters | to_entries[] | [.key, .value.provider, .value.state]) | @tsv' | column -t MEMBER PROVIDER STATE vc-a.vcluster-a vcluster Connected vc-b.vcluster-b vcluster Connected Two tenant APIs. Independent scaling. One central Kedify installation. Hands-on tutorial: https://docs.kedify.io/how-to/vcluster-scaling/