Integrate KEDA into milner #85
Labels
No labels
blocked
bug
duplicate
effort
high
effort
low
effort
medium
effort
minimal
enhancement
help wanted
invalid
pr
feedback
pr
reviewing
question
status
backlog
status
document
status
done
status
implement
status
pending
status
planning
status
reviewing
waiting
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
tepichord/milner#85
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Here's the KEDA change spelled out against this tree's actual conventions. All paths are relative to gitops/clusters/milner/.
Files to add
(pods runAsNonRoot: true; containers drop ALL capabilities, allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, seccompProfile: RuntimeDefault), so
take the tightest level that works rather than baseline — postgres already sets the precedent. If you'd rather not bet on chart defaults for the namespace's
admission level, that's the one place to decide it.
existing HelmRepositories). Not OCI: ghcr.io/kedacore/charts/keda answers 403 anonymously, so HelmRepository is the route — and it's the pattern metrics-server,
cloudnative-pg, vault-secrets-operator and vault already use.
HelmRepository, interval: 10m, valuesFrom → the ConfigMap below.
default, and that default is nowhere in this repository (tests/test_chart_sources.py)`. So:
stay Pending.
satisfied either way; pin them if you want the numbers reviewable in git.
from values.yaml, resources helmrepository.yaml then helmrelease.yaml.
Files to modify
among components is stylistic, the layer is applied atomically).
tests/test_gitops.py::test_crd_providers_are_not_in_the_layer_that_uses_their_crds asserts once a keda.sh object exists.
assert users, f"nothing applies {group} resources; drop this case" — so the row cannot land in this change alone. Adding it with no keda.sh resource anywhere
makes the suite red with "nothing applies keda.sh resources". Two honest options:
Whichever you pick, say so in the HelmRelease comment, because the next reader will wonder why the row is missing.
ScaledJob's state, and how to pause/unpause scaling for maintenance (autoscaling.keda.sh/paused — the browser-reachable lever the annia remediation design wants).
docs index table for docs/keda.md.
Ordering and CRD notes to write into the comments
first install) and upgrade.crds: Skip (later chart bumps do not update them). Since a KEDA minor bump can change its CRD schemas, state the policy deliberately in
helmrelease.yaml rather than inheriting it — either upgrade.crds: CreateReplace or an explicit "CRDs are updated by hand, here is where" note.
thing to check when scaling silently does nothing.
Verification
(network-backed: index served, keda 2.20.2 listed) cover the rest.
--kustomization-file=gitops/clusters/milner/infrastructure.yaml --dry-run is what the L0 gate runs.
v1beta1.external.metrics.k8s.io → True; kubectl -n keda get deploy -o wide → all on talos-qpo-xhw.
Naina:
Updated on 2026-09-22 based on maintainer feedback.
Overview
Install KEDA
2.20.2as aninfrastructure-layer Flux component in a newkedanamespace, so a future ScaledJob/ScaledObject inappscan scale workload pods from RabbitMQ queue depth (ARCHITECTURE.md §2). The change adds namespace + HelmRepository + HelmRelease + pinned values +docs/keda.md §17, and deliberately defers theCRD_PROVIDERSrow until annia's ScaledJob (#25, step 10) lands.Technical Approach
Add the namespace —
restricted, same precedent aspostgres.yaml1:Add the HelmRepository — non-OCI (anonymous OCI pull from
ghcr.io/kedacore/charts/kedareturns 401/4032), 24h like every other source:Add the HelmRelease — v2, quoted version, values from the ConfigMap:
Add
values.yaml— pin all three components for reviewability (the chart's ownvalues.yamlalready sets identical blocks, and Helm deep-merges, so a partial pin is safe34):Add
kustomization.yaml— mirrormetrics-server/5:Wire into
infrastructure/kustomization.yaml6:State the real CRD lifecycle in the HelmRelease comment — do not inherit it silently, and do not repeat the stale "upgrade.crds defaults to Skip" claim78:
Defer the
CRD_PROVIDERSrow. Adding("keda.sh", "keda", "KEDA")now failstest_crd_providers_are_not_in_the_layer_that_uses_their_crdswithnothing applies keda.sh resources; drop this case9, and a placeholder ScaledObject needs a real scale target to be honest. The consumer is annia's ScaledJob (#25, step 10); add the row there and say so in the HelmRelease comment.Document it — new
docs/keda.md§17 (§16 is RabbitMQ): what KEDA is for here, the namespace PSA level and why, reading ScaledJob state, and the pause leverautoscaling.keda.sh/pausedused by the oz console (#90). Add the managed-state row and the docs-index row inOPERATIONS.md.Implementation Details
Create
gitops/clusters/milner/infrastructure/namespaces/keda.yamlrestrictedPSAgitops/clusters/milner/infrastructure/keda/helmrepository.yamlgitops/clusters/milner/infrastructure/keda/helmrelease.yaml2.20.2, CRD-lifecycle + CRD_PROVIDERS-deferral commentsgitops/clusters/milner/infrastructure/keda/values.yamlgitops/clusters/milner/infrastructure/keda/kustomization.yamldocs/keda.mdModify
gitops/clusters/milner/infrastructure/kustomization.yamlnamespaces/keda.yaml+kedaOPERATIONS.mdConstraints
kedaorg-certs(certificates.autoGenerated: true,certificates.certManager.enabled: false), so no cert-manager is needed.keda,keda-metrics-apiserver,keda-admission-webhooks) are multi-arch includinglinux/arm6410; no:latest.seccompProfile—restrictedallows container-level-only seccomp, and the chart sets it there.securityContext/podSecurityContextmap: the chart template'selsebranch renders the three-component map under a single component key, producing a malformed manifest. unverifieddocs/keda.md pause guidance (corrected)
kubectl -n annia annotate scaledjob <name> autoscaling.keda.sh/paused=truekubectl -n annia annotate scaledjob <name> autoscaling.keda.sh/paused-"false"also resumes on 2.20.2 — the bug where it did not (#5215) was fixed in v2.13.011. Any value outside Go'sParseBoolset stays paused12.Pausedcondition: the condition lags and was historically unreliable (#5251 closed stale; the pause reconciler was reworked in v2.20.0 by #7663/#766413).Out-of-scope notes
TriggerAuthentication/RabbitMQ trigger — that lands with the first ScaledJob and needs Vault.rbac.controlPlaneServiceAccountsNamespaceonly moves thekeda-operator-auth-readerRoleBinding; pinningkube-systemmatches the chart default and is documentation-only.watchNamespace(which only scopes the cache and RBAC);failurePolicystaysIgnore.apiGroups: *forscaleTargetRefby default (permissions.operator.restrict.secret,rbac.scaledRefKinds); narrowing both is a separate security decision.Acceptance Criteria
kustomize build gitops/clusters/milner/infrastructurerenders thekedaNamespace, HelmRepository, HelmRelease, andkeda-valuesConfigMap.python -m pytest tests/ -vpasses withCRD_PROVIDERSunchanged.values.yamlpinsnodeSelector,watchNamespace,crds.install,rbac.controlPlaneServiceAccountsNamespace, and all three components'resources+securityContext/podSecurityContext, with no empty maps.helmrelease.yamlcarries the CRD-lifecycle comment and the CRD_PROVIDERS-deferral note.docs/keda.mdexists as §17 andOPERATIONS.mdlinks it and lists the KEDA release in the managed-state table.Testing Plan
python -m pytest tests/ -vkustomize build gitops/clusters/milner/infrastructure | kubeconform -strictflux build kustomization infrastructure --path=gitops/clusters/milner/infrastructure --kustomization-file=gitops/clusters/milner/infrastructure.yaml --dry-runPost-reconcile, manual:
kubectl -n keda get pods→ three Ready pods on a worker;kubectl get crd -o name \| grep -c keda.sh→ six;kubectl get apiservice v1beta1.external.metrics.k8s.io→True.Related Issues/PRs
CRD_PROVIDERSrow.autoscaling.keda.sh/pausedon annia's ScaledJob.Research Findings
securityContext/podSecurityContextpin does not delete sibling keys, and the chart's defaults already satisfyrestrictedfor all three components. The only real template footgun is an empty map, whoseelsebranch renders the three-component map under one key.#5215(settingpaused: "false"sticking) was fixed in v2.13.0 and is included in 2.20.2; only values outside Go'sParseBoolset stay paused, so both removing the annotation and settingfalseresume.#5251(ScaledJobPausedcondition not flipping back) was closed stale, but the pause reconciler was reworked in v2.20.0 (#7663/#7664); on 2.20.2 the condition does flip back. Read the annotation because it is the source of truth, not because the condition is broken.rbac.controlPlaneServiceAccountsNamespaceonly moves thekeda-operator-auth-readerRoleBinding; pinningkube-systemis a documentation no-op (same as the chart default).watchNamespace, which only scopes the cache and RBAC;failurePolicyisIgnoreby default.templates/crds/gated bycrds.install, so Flux'sinstall.crds/upgrade.crds(both defaultCreate) never apply and every bump replaces them; Helm never deletes CRDs.ghcr.io/kedacore/charts/kedareturns 401/403, confirming the non-OCIHelmRepository;https://kedacore.github.io/charts/index.yamllists2.20.2.v1beta1.external.metrics.k8s.iocoexists with metrics-server'sv1beta1.metrics.k8s.io; KEDA uses the standardmetrics.k8s.ioHPA path forcpu/memorytriggers.Pre-Mortem
Assume this plan shipped and then failed. Ranked by likelihood:
kustomize build,flux build --dry-run, and pytest all stop at the generated ConfigMap, so a mis-nestedvalues.yamlkey or akeda-values/valuesFromname mismatch only surfaces when helm-controller reconciles on the cluster. Keep thevaluesFromname byte-identical to theconfigMapGeneratorname and treat the first post-reconcilekubectl -n keda get podsas the real gate.securityContext/podSecurityContextmap malforms the release. WritingsecurityContext: {}orsecurityContext.operator: {}trips the chart template'selsebranch and renders all three components' blocks under one key, producing a manifest that offline validation accepts. Never emit an empty map; pin the full block for each of the three components.CRD_PROVIDERSrow leaves the layer-ordering invariant unenforced forkeda.sh. Since the row lands only with annia's ScaledJob (#25, step 10), nothing fails if that ScaledJob is placed ininfrastructureinstead ofapps, which is exactly the mistake the test exists to catch. Add the row in that follow-up change and keep the consumer inapps.One linkage and two coordination facts, all small.
The consumer that resolves the open sequencing question. This plan records that the
("keda.sh", "keda", "KEDA")row intests/test_gitops.py'sCRD_PROVIDERScannot landwith no consumer, so KEDA lands with the first ScaledJob. That consumer is annia's
ScaledJob, owned by the annia pod issue as its step 10, now explicitly after this one.
Linking the two issues is enough; nothing in this plan needs rewriting.
The browser lever has an owner now. This plan names
autoscaling.keda.sh/pausedas thebrowser-reachable lever for annia's crash loop. That is now a console change — the issue
Add the annia pause lever to the oz consoleadds a control inozthat writes thatannotation on annia's ScaledJob, backed by a Role in annia's namespace for
get/patchon
scaledjobsand nothing else. Two details worth carrying intodocs/keda.mdwhen it iswritten, because both are easy to get wrong:
"false"had a documented historyof leaving the object paused (kedacore/keda#5215, fixed by kedacore/keda#5257).
Pausedcondition, which has been reportednot to flip back to
Falsewhen the annotation is removed (kedacore/keda#5251).Documentation numbering. This plan takes §17 for
docs/keda.md, and that is yours: theegress/relay work adds a section to
docs/tailscale.mdand deliberately took "the next freenumber at the time it is written" instead of pinning one, so there is no collision to
resolve.