Schedule icon
Webhook icon
WorkingDirectory icon
Commands icon
Docker icon
Script icon
If icon
Create icon
SlackIncomingWebhook icon
Loop icon
Log icon

Right-Size Kubernetes HPA Replica Bounds from Live CPU Utilization

Compare HPA CPU utilization against a target, recommend min and max replica changes, alert Slack, open a GitHub issue, then patch with kubectl.

Categories
Infrastructure

How it works

  1. daily_hpa_review (io.kestra.plugin.core.trigger.Schedule, cron 0 6 * * *) is the primary trigger and ships disabled: true, so nothing runs until an operator switches it on. on_demand_review (io.kestra.plugin.core.trigger.Webhook, keyed) accepts an on-demand review of a namespace from a CI job or a laptop.
  2. usage_workspace (io.kestra.plugin.core.flow.WorkingDirectory) opens one shared filesystem so the scrape, the analyzer, and the patcher all work from the same hpa-usage.json.
  3. collect_hpa_usage (io.kestra.plugin.scripts.shell.Commands on the io.kestra.plugin.scripts.runner.docker.Docker runner, image bitnami/kubectl:latest) writes the fixture with a heredoc: web-frontend steady at 40 percent, checkout-api pinned at 95 percent with desired replicas equal to maxReplicas, and report-generator idle at 2 percent on a minReplicas floor of 3. It then runs kubectl get --raw /apis/metrics.k8s.io/v1beta1/nodes || true, so a missing cluster or missing RBAC is absorbed and the flow keeps its shape.
  4. analyze_utilization (io.kestra.plugin.scripts.python.Script on python:3.12-slim) reads the file and applies three rules against the CPU target. Idle at or below 10 percent normalizes the floor to 2 replicas, lowering a padded floor or raising a single-replica floor to a warm pair. Pinned at or above 90 percent with desired replicas already at maxReplicas raises the ceiling by 50 percent, at least 2 replicas. Anything else, including the steady 40 percent HPA, is left alone, and the task emits verdict, recommendations, and hpas through the outputs protocol.
  5. tuning_gate (io.kestra.plugin.core.flow.If) branches on whether the recommendation list is empty. open_tuning_issue (io.kestra.plugin.github.issues.Create) files a labeled issue with one bullet per change, alert_slack posts the counts to Slack, and apply_recommendations (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) walks the list.
  6. Inside the loop, patch_hpa echoes a --dry-run=server preview of the exact kubectl patch horizontalpodautoscaler command before merging minReplicas and maxReplicas, and ends the command with || true so an unreachable API server or missing RBAC degrades to a warning while the issue and the Slack message still carry the tuning work.
  7. When the list is empty, log_optimal (io.kestra.plugin.core.log.Log) records the all-clear instead of paging anyone. The errors block alerts Slack whenever the scrape, the analysis, or the patch stage breaks, because a silent tuner looks exactly like a healthy fleet.

What you get

  • Gateable outputs: verdict, recommendations, hpas, and cpu_target are flow outputs any downstream flow or dashboard can branch on.
  • Tracked work instead of a forgotten report: every suggested bound change lands as a labeled GitHub issue with the current and suggested values side by side.
  • A channel signal: one Slack message per run that needed tuning, carrying the counts, the repository, and the execution id.
  • Applied changes with an audit trail: each patch is echoed with a dry-run preview first, and every execution keeps the inputs, the recommendation list, and the patch results.

Who it's for

  • Platform and SRE teams that own autoscaler defaults and want them evidence-based instead of guessed.
  • Kubernetes operators who suspect padded floors and pinned ceilings are costing them money or headroom.
  • FinOps and capacity engineers who need a repeatable, reviewable tuning loop over minReplicas and maxReplicas.

Why orchestrate this with Kestra

Tuning HPAs is the kind of task everyone agrees is useful and nobody schedules. Kestra supplies the loop around it: a disabled-by-default daily schedule as the safety net, a keyed webhook for on-demand reviews after a deploy, a hermetic kubectl container so the fixture and the tool version live in the flow file, a threshold branch that separates escalation from the all-clear, a sequential patch loop that cannot be knocked over by one bad autoscaler, and routing into GitHub and Slack from one declarative definition. A cron entry on a bastion host gives you none of that, it only patches the cluster someone remembered to look at.

Prerequisites

  • A worker with Docker access, the scrape and the patch run in bitnami/kubectl and the analysis in python:3.12-slim.
  • A Kubernetes context the worker can reach for real patches. Without one the flow still completes on the bundled fixture, which is exactly what makes it safe to run in CI or on a laptop.
  • A GitHub token that can open issues in github_repository (classic PAT scope repo, or fine-grained Issues: write).
  • A Slack incoming webhook for tuning alerts and for failure alerts.
  • RBAC able to get and patch horizontalpodautoscaler resources in the target namespace.

Secrets

  • HPA_TUNER_WEBHOOK_KEY: key segment guarding the on-demand webhook URL, anyone holding it can trigger a review.
  • GITHUB_TOKEN: token used to open the tuning issue.
  • SLACK_WEBHOOK_URL: Slack incoming webhook URL used for tuning alerts and failure alerts.

Quick start

  1. Add the three secrets to your Kestra namespace.
  2. Run the flow once with the default inputs and confirm verdict is TUNE with hpas set to 3 and two recommendations in the outputs.
  3. Point github_repository at a repository where issues are welcome, then re-run and watch the issue, the Slack message, and the patch loop fire in that order.
  4. Trigger on_demand_review at /api/v1/{tenant}/executions/webhook/{namespace}/hpa-utilization-tuner/{HPA_TUNER_WEBHOOK_KEY}, or enable daily_hpa_review once a cluster is reachable from the worker.

How to extend

  • Tune several namespaces by fanning the flow out with a ForEach over a namespace list, one execution per namespace.
  • Add a second threshold for memory by extending the analyzer with a cpuUtilization sibling field and a second rule branch.
  • Dry-run only: add --dry-run=server to the kubectl patch command in patch_hpa when you want recommendations without mutations, the issue and the Slack message still fire.
  • Fail loudly: swap the trailing guard in patch_hpa for io.kestra.plugin.core.execution.Fail under tuning_gate when a rejected patch should page.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.