Skip to content

sie-cluster: keda-apply hook is OOMKilled at its hardcoded 256Mi limit on every upgrade, and the limit is not configurable #248

Description

@igor-raits

Summary

The post-install,post-upgrade hook Job *-keda-apply-* is consistently OOMKilled (exit 137) on upgrade at the resource limits hardcoded in templates/keda-scaledobject.yaml (lines 481-487):

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 200m
    memory: 256Mi

Because that Job sets backoffLimit: 0, a single OOM fails the whole Helm operation. With chart 0.7.2 this made every upgrade fail — 5 consecutive attempts (hook Jobs …-keda-apply-{12,14,16,18,20}, of which I confirmed -12 and -14 as OOMKilled/exit 137 at the pod level before Helm's hook-delete-policy cleaned them up), each failure rolling the release back. The deployed application itself was healthy and serving traffic throughout; only the upgrade path was blocked.

Notably the post-install run of the same hook (…-keda-apply-1) succeeded on the same cluster at the same limit — see the mechanism note below, which I think explains the asymmetry.

The chart exposes no value to raise those limits (no hook*.resources key exists in values.yaml), which is the core of this request.

Where it dies

The hook's own log shows CRD discovery succeeding and the failure happening in the apply step:

=== KEDA ScaledObject Apply ===
Waiting for KEDA CRD...
KEDA CRD available
Applying ScaledObjects...
  Applying sie-keda-so-0000
<OOMKilled>

So it is past "Step 1: Wait for KEDA CRD" and inside "Step 2".

Why install passes and upgrade fails, at the same limit on the same cluster — the ownership check in Step 2 is data-dependent:

existing="$(kubectl get scaledobject "$name" -n "$NAMESPACE" --ignore-not-found -o json)"
if [ -n "$existing" ] && ! printf '%s' "$existing" | jq -e ' ... ' ; then

On a fresh install nothing matches, existing is empty, and the jq branch is skipped. On every subsequent upgrade each ScaledObject is fetched as full JSON and piped through jq, and Step 3 then runs another namespace-wide kubectl get scaledobjects -l …. That is several extra kubectl/jq processes inside the same 256Mi cgroup that already holds a kubectl discovery cache for ~500 API resources. I have not profiled the individual allocations, so treat the split as the observation and this as the likely explanation — but it does match install-passes / every-upgrade-fails exactly, and it means the headroom needed scales with how many lanes a deployment has.

Why the usual workarounds do not apply

  • --no-hooks / upgrade.disableHooks — the chart README explicitly says not to use --no-hooks, --atomic or --cleanup-on-fail. We are currently forced to run upgrade.disableHooks: true anyway, which means upgrades no longer reconcile the KEDA ScaledObjects or run the health gates. We compensate with a CI check that blocks chart-version and worker-lane changes while that override is in place, but it is a workaround for a chart limitation, not a fix.
  • Post-renderers — we deploy via Flux. Helm does not pass hook manifests through post-renderers, so a Kustomize patch raising the memory has no effect on hook Jobs (verified empirically: patch applied, hook pods still ran at 256Mi). Flux helm-controller v1.3.0 has no postRenderStrategy option to change that.
  • Admission mutation — no mutating webhook (Kyverno/Gatekeeper/VPA) in this cluster to raise it either.
  • autoscaling.enabled: false — not viable: templates/keda-lifecycle.yaml then switches its hooks to pre-upgrade,pre-delete, and that Job is the same kubectl-in-a-Job pattern at an even smaller 128Mi limit (line 120).

That leaves no supported way to run this chart's hooks on a cluster like ours.

Request

Expose the hook Jobs' resources through values, so operators can size them for their cluster. Something as simple as:

hooks:
  resources:
    requests: {cpu: 100m, memory: 128Mi}
    limits:   {cpu: 200m, memory: 1Gi}

applied to the kubectl-based hook Jobs would be enough. The same limitation affects the other kubectl-based hooks — templates/hooks/keda-ready-test.yaml (256Mi, line 385) and templates/keda-lifecycle.yaml (128Mi, line 120) — so a shared key covering all of them seems right.

A complementary improvement, if you would rather not make it configurable: backoffLimit: 0 turns any transient hook failure into a failed release. Even backoffLimit: 1-2 would make these hooks considerably less brittle.

Happy to open a PR for the values-driven version if that shape works for you.

Environment

  • Chart / app version: sie-cluster 0.7.2 / v0.7.2 (latest published at time of writing)
  • Kubernetes: v1.34 (EKS), Bottlerocket nodes
  • Deployed via Flux (helm-controller v1.3.0); external KEDA 2.20.2 and an external Prometheus (autoscaling.enabled: true, keda.install: false)
  • Cluster API surface: 471 CRDs, 539 API resources
  • Hook image: alpine/k8s:1.29.10 (as pinned by sie-cluster.keda.hookImage)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions