Move your existing KEDA ScaledObjects onto DevZero's KEDA and retire your own KEDA operator, ending with a single KEDA driving all horizontal autoscaling.
DevZero can run KEDA for you. This guide moves your existing KEDA ScaledObjects
onto DevZero's KEDA and retires your own KEDA operator, so you end up with a
single KEDA — DevZero's — driving all your autoscaling.
Every step below has a short, copy‑paste block that does the work for all of
your objects at once (no "repeat for each object" by hand). Read the paragraph,
run the block, check the output it prints, move on.
DevZero does not take over your ScaledObject in place. For each workload you
migrate, DevZero's operator creates its own object — a ScaledObject named
mpa-<kind>-<target>, owned by a DevZero WorkloadRule. Your original object and
DevZero's mpa-* object are two different resources.
That matters because any KEDA operator in the cluster reconciles everyScaledObject, no matter who owns it. If your original object and DevZero's
mpa-* object both exist while a KEDA operator is running, that operator builds
two HPAs for one Deployment and they fight each other. To avoid that, this
guide does the swap while no KEDA operator is running: you remove your
originals and stop your KEDA first, then bring DevZero's KEDA up as the only one.
The one trade‑off — a short, safe pause. Between stopping your KEDA (Step 4)
and DevZero's KEDA serving (Step 8), external‑metric autoscaling does not
update. Your Deployments simply hold their current replica count — nothing
restarts, nothing scales to zero. Plan for a few minutes of "frozen" replicas;
that's the entire blast radius.
Your objects are copied verbatim.keda-import performs a blind copy: the
entire ScaledObject spec — every trigger, advanced block (including
scalingModifiers and a custom HPA name), fallback, and any field a future KEDA
release adds — plus all of your labels and annotations (including
autoscaling.keda.sh/paused) are carried onto DevZero's mpa-* object unchanged.
The only things intentionally dropped are the tool‑ownership keys that would make
Argo CD / Flux / Helm reclaim our object (kubectl last‑applied,
argocd.argoproj.io/*, *.toolkit.fluxcd.io/*, meta.helm.sh/*). A paused
workload stays paused; there is nothing special to do for any field.
Read this before Step 3. If your ScaledObjects are applied by Argo CD or
Flux (i.e. they live in a Git repo, not just in the cluster), a plain
kubectl delete in Step 3 will not stick: the GitOps controller sees the
deletion as drift and re‑creates the object from Git within minutes. You would
then have your original and DevZero's mpa-* object for the same Deployment —
two KEDA‑built HPAs fighting, the exact collision this guide exists to avoid.
Check whether this applies to you:
# Argo CD stamps a tracking id; Flux stamps kustomize/helm labels.kubectl get scaledobjects.keda.sh -A -o json | jq -r ' .items[] | select( (.metadata.annotations["argocd.argoproj.io/tracking-id"] != null) or (.metadata.labels["kustomize.toolkit.fluxcd.io/name"] != null) or (.metadata.labels["helm.toolkit.fluxcd.io/name"] != null) ) | "\(.metadata.namespace)/\(.metadata.name)"'
If that prints nothing, you are not affected — skip to Prerequisites. If it lists
objects, pick one of these before you reach Step 3:
Recommended — migrate the source of truth. Commit the wr-*.yamlWorkloadRules from Step 2 into the same Git path that holds those
ScaledObjects, and remove the ScaledObject manifests from Git in the same
change. Let the GitOps controller apply the WorkloadRules and prune the
ScaledObjects. Because Git no longer contains the originals, there is nothing
to self‑heal, and you skip the kubectl delete in Step 3 entirely (the
prune does it for you). This is the durable outcome — your Git repo now
describes the DevZero rules.
Quicker, temporary — suspend reconciliation for the migration window. Turn
off auto‑sync and self‑heal on the managing Application(s) so your manual
kubectl delete/apply is not reverted, then re‑point Git and re‑enable
afterwards. For Argo CD:
# Disable automated sync (which includes selfHeal) on the app that owns them.argocd app set <app> --sync-policy none# ... run Steps 3–8 by hand ...# Then update Git to the WorkloadRules and re-enable:argocd app set <app> --sync-policy automated --self-heal
Leaving self‑heal on during the window will undo Step 3 — do not skip this.
Either way, keep DevZero's KEDA off (dzkeda.enabled=false) until Step 8, so
nothing reconciles a re‑created original while you work.
DevZero is already installed with dzkeda.enabled=false. In this mode
DevZero ships no KEDA at all — no controller and no CRDs — so it cannot
collide with your running KEDA. (This is the default; you turn DevZero's KEDA on
in Step 8.)
kubectl and helm are configured against the target cluster.
The keda-import tool is on your PATH. It converts a ScaledObject into a
DevZero WorkloadRule. Contact DevZero support for the prebuilt binary for your
OS/architecture — no Go toolchain or source checkout required.
Set these once; every block below reuses them:
export DZ_RELEASE=dakr # your DevZero helm release name (helm list -A)export DZ_NS=devzero-system # namespace DevZero is installed inexport DZ_CHART=oci://registry-1.docker.io/devzeroinc/dakr-operator # or a local ./pathexport KEDA_RELEASE=keda # YOUR existing KEDA helm release nameexport KEDA_NS=keda # namespace YOUR KEDA is installed inmkdir -p keda-migration && cd keda-migration # all backups land here
Before changing anything, snapshot the current world: your ScaledObjects, your
TriggerAuthentications, and the Secrets those auths point at. DevZero's KEDA
reads the very same TriggerAuthentications and Secrets after cutover, so we
preserve them exactly. We also record current replica counts so you can confirm
parity at the end. Everything is written into the keda-migration/ folder.
kubectl get scaledobjects.keda.sh -A -o yaml > backup-scaledobjects.yamlkubectl get triggerauthentications.keda.sh,clustertriggerauthentications.keda.sh -A -o yaml \ > backup-triggerauth.yamlkubectl get deploy -A -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,REPLICAS:.spec.replicas \ > baseline-replicas.txtecho "backed up $(kubectl get scaledobjects.keda.sh -A --no-headers | wc -l) ScaledObject(s)"
The Secrets referenced by your TriggerAuthentications live in your app
namespaces and are not owned by your KEDA release, so uninstalling KEDA in
Step 4 does not touch them. They stay put; DevZero's KEDA reads them as‑is.
keda-import reads a ScaledObject and prints the equivalent DevZero
WorkloadRule, copying the object verbatim (all triggers, spec fields, labels,
and annotations — see the blind‑copy note above). This block runs it for everyScaledObject in the cluster and drops one wr-*.yaml per object into
keda-migration/. It only writes files — nothing is applied to the cluster yet,
so it is completely safe to run and re‑run.
: > imported.txt # records the exact originals we convert, for Step 3kubectl get scaledobjects.keda.sh -A \ -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name --no-headers |while read -r ns name; do if kubectl get scaledobject "$name" -n "$ns" -o yaml | keda-import > "wr-$ns-$name.yaml" 2>/dev/null; then echo "$ns $name" >> imported.txt echo "converted $ns/$name -> wr-$ns-$name.yaml" else echo "SKIPPED $ns/$name (unsupported field, e.g. scalingModifiers or a custom HPA name)" rm -f "wr-$ns-$name.yaml" fidone
Because the conversion is a blind copy, a SKIP is now rare — it means the
object is structurally broken (e.g. no scaleTargetRef.name), not that a field is
unsupported. If anything is SKIPPED, leave its original in place — do not
delete it in Step 3 — and inspect it by hand.
Deleting a ScaledObject requires your KEDA operator to be alive: KEDA runs a
finalizer that also removes the HPA it built. If you stopped KEDA first, the object
would wedge in Terminating forever. So we delete the originals now, while
your KEDA is still up. Deleting a ScaledObject does not touch the Deployment
— it simply stops being autoscaled and holds its current replica count.
This block deletes exactly the originals you recorded in imported.txt in Step 2,
so anything you SKIPPED is left alone automatically.
GitOps users: if you took the recommended path in the GitOps section above
(migrated the source of truth to WorkloadRules in Git), skip this block —
your controller already pruned the originals. Run it only if you chose the
temporary path, and only after auto‑sync and self‑heal are off, or the
objects will be re‑created from Git before you finish.
while read -r ns name; do kubectl delete scaledobject "$name" -n "$ns" --wait=true --timeout=60sdone < imported.txtkubectl get scaledobjects.keda.sh -A # only SKIPPED originals (if any) should remain
Scaling your KEDA operator to zero is not enough: its cluster‑scoped pieces
(the keda-admission webhook, the external.metrics APIService, its ClusterRoles)
would linger and collide with DevZero's KEDA. Uninstall the release so those are
removed cleanly.
helm uninstall "$KEDA_RELEASE" -n "$KEDA_NS"kubectl get deploy -n "$KEDA_NS" | grep -i keda || echo "your KEDA controller is gone"
Don't worry about the KEDA CRDs here. Whether your uninstall keeps them or
removes them, the next two steps make DevZero the sole, clean owner — and your
TriggerAuthentications are restored from backup in Step 6 regardless. This is
the "frozen replicas" window; it ends in Step 8.
If your uninstall left the KEDA CRDs behind, they are still tagged as owned by your
old release, and DevZero's Helm release would refuse to manage them. This one block
re‑stamps them as DevZero‑owned so DevZero adopts them in Step 8. It is safe to run
even if the CRDs are already gone (the commands simply no‑op on what isn't there).
for crd in scaledobjects.keda.sh scaledjobs.keda.sh triggerauthentications.keda.sh \ clustertriggerauthentications.keda.sh cloudeventsources.eventing.keda.sh \ clustercloudeventsources.eventing.keda.sh; do kubectl annotate crd "$crd" \ meta.helm.sh/release-name="$DZ_RELEASE" meta.helm.sh/release-namespace="$DZ_NS" \ --overwrite 2>/dev/null kubectl label crd "$crd" app.kubernetes.io/managed-by=Helm --overwrite 2>/dev/nulldoneecho "CRD ownership handed to $DZ_RELEASE (where the CRDs still exist)"
DevZero's KEDA authenticates to your scalers using your existing
TriggerAuthentications and their Secrets. If Step 4 removed the KEDA CRDs, it
also removed the TriggerAuthentication objects (the Secrets always survive).
Re‑applying your Step 1 backup guarantees they're present no matter what your
uninstall did — and if they were never removed, re‑applying is a harmless no‑op.
Apply the backup only when it actually containsTriggerAuthentications.
Guarding on the content this way keeps a real apply failure (a missing CRD, an
RBAC gap, a corrupt backup) from being hidden — the naive 2>/dev/null || echo
form swallowed every error and reported "nothing to do" even when the restore
failed, which would leave DevZero's KEDA unable to authenticate after cutover.
if grep -qE 'kind: (Cluster)?TriggerAuthentication' backup-triggerauth.yaml; then kubectl apply -f backup-triggerauth.yaml # errors surface — do not hide themelse echo "no TriggerAuthentications to restore (none existed) — nothing to do"fi
Now apply every WorkloadRule you generated in Step 2. No KEDA operator is running
yet, so DevZero simply records each rule and (once its KEDA is on in Step 8) will
render an mpa-*ScaledObject for it. Applying them now means the moment
DevZero's KEDA starts, everything reconciles at once.
if ls wr-*.yaml >/dev/null 2>&1; then kubectl apply -f wr-*.yamlelse echo "no wr-*.yaml to apply (nothing converted in Step 2, or you migrated via Git)"fikubectl get workloadrules.dakr.devzero.io -A # one per converted object
Flip DevZero's KEDA on with a single flag. This deploys DevZero's KEDA controller,
adopts the CRDs you re‑stamped in Step 5 (or creates them fresh if your uninstall
removed them — either way, no conflict), and reconciles every WorkloadRule into
its mpa-*ScaledObject. --reuse-values keeps every other setting from your
original DevZero install exactly as it was.
If you originally installed DevZero from a values file, use -f your-values.yaml
in place of --reuse-values so the flip renders from the same source of truth.
DevZero's KEDA should now be the only KEDA: its adapter registers the external
metrics API, and each mpa-*ScaledObject becomes READY=True with one HPA per
target. This block prints the three things that prove it, plus a replica comparison
against your Step 1 baseline.
kubectl get apiservice v1beta1.external.metrics.k8s.io \ -o custom-columns=NAME:.metadata.name,AVAILABLE:'.status.conditions[?(@.type=="Available")].status'kubectl get scaledobjects.keda.sh -A \ -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,READY:'.status.conditions[?(@.type=="Ready")].status',OWNER:.metadata.ownerReferences[0].kindkubectl get hpa -A | grep keda-hpa-mpa- # one per migrated workloaddiff <(sort baseline-replicas.txt) \ <(kubectl get deploy -A -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,REPLICAS:.spec.replicas | sort) \ && echo "replica counts match the pre-migration baseline"
Expect AVAILABLE=True, every mpa-* object READY=True and OWNER=WorkloadRule,
and one keda-hpa-mpa-* HPA per workload. If AVAILABLE is stuck, check DevZero's
metrics adapter: kubectl logs -n "$DZ_NS" -l app=keda-operator-metrics-apiserver.
You're done. Never delete an mpa-*ScaledObject directly — remove one only
by deleting its WorkloadRule (the owner reference cascades the cleanup).
If anything looks wrong, turn DevZero's KEDA back off and restore your originals.
Because your ScaledObject and TriggerAuthentication backups are complete, this
returns you to exactly where you started.
helm upgrade "$DZ_RELEASE" "$DZ_CHART" -n "$DZ_NS" --reuse-values --set dzkeda.enabled=false --waitkubectl delete workloadrules.dakr.devzero.io --all -A # cascade-removes the mpa-* objectskubectl apply -f backup-triggerauth.yaml # restore your authskubectl apply -f backup-scaledobjects.yaml # restore your originalshelm repo add kedacore https://kedacore.github.io/charts >/dev/null 2>&1 || truehelm repo update kedacore >/dev/null # ensure the chart is fetchablehelm install "$KEDA_RELEASE" kedacore/keda -n "$KEDA_NS" --create-namespace # reinstall your KEDA
Your KEDA comes back up and resumes reconciling your restored ScaledObjects.