Cluster cost estimate + budget guardrail¶
Motivation¶
Operations-readiness checklist section 5: "cost/quota guardrails + budget alerts". The dashboard had no cost surface at all. This adds the light, honest version: a coarse compute-cost estimate and a per-cluster monthly budget threshold with a dashboard warning — deliberately not a full Azure Cost Management integration.
User-facing change¶
- A new Cost estimate card in the dashboard Resource plane shows the managed cluster's approximate hourly and projected-monthly compute cost (node SKU price × node count), plus an accrued "this session" figure when the cluster uptime is known.
- A monthly budget input lets an operator set a USD threshold; the card shows an over-budget warning when the projected monthly cost exceeds it.
- Everything is labelled APPROX with a disclaimer: workload node pool only, assumes 24/7 running, excludes Spot/reserved discounts, system pool, storage, and egress. Authoritative spend stays in Azure Cost Management.
Design — honest approximation, no billing API¶
- No Azure Cost Management / Consumption / Pricing SDK dependency. The hourly
price comes from a small hardcoded SKU price map in
api/services/cost/estimate.pycarrying aPRICED_AS_OFdate that is surfaced to the UI. An unknown SKU returnspriced=Falseso the card shows "estimate unavailable" rather than a fabricated number. - Cluster SKU / node count / power state are read from the existing
get_aks_cluster_snapshotmonitoring wrapper; uptime is derived from the auto-stoplast_started_atanchor (when present). - The estimate is
is_estimate=Trueand intentionally coarse: it covers the workload node pool only and assumes 24/7 running for the monthly projection. - The per-cluster budget is one Azure Table row (
budgetpref,PartitionKey = "budget:" + sub:rg:cluster), mirroring the performance/auto-stop preference pattern. Budget is clamped to[0, 10M]; 0 means "no threshold".
Hardening (post-critique)¶
- Disclaimer spells out the approximation boundaries (workload pool only / 24/7 / exclusions) so a low estimate is never mistaken for a bill.
GET /api/costdegrades to adegradedpayload (never 500) when the cluster is unreadable.- Budget is validated both at the HTTP boundary (
Field(ge=0)) and innormalise_budget(negative/NaN → 0, capped). Query params are length-bounded.
API / IaC diff summary¶
- New backend:
api/services/cost/estimate.py(pure price map + math),api/services/cost/budget_pref.py(Table-backed budget),api/routes/cost.py(GET /api/cost,GET/PUT /api/cost/budget, allrequire_caller), registered inapi/main.py. - New frontend:
web/src/api/cost.ts(+ barrel),web/src/components/cards/CostCard.tsx, wired intoweb/src/pages/Dashboard/DashboardGrid.tsx. - No IaC change: the
budgetpreftable is created on first use. No new env var, no new Azure resource, no billing-API dependency.
Validation evidence¶
uv run pytest -q api/tests/test_cost_estimate.py api/tests/test_cost_budget_pref.py api/tests/test_cost_routes.py— 15 passed.uv run ruff check api— all checks passed.cd web && npm run build— built successfully (resolved aCostEstimate/costApiexport clash with@/api/blastToolsby renaming toClusterCostEstimate/clusterCostApi).uv run pytest -q api/tests— 4621 passed, 3 skipped, 1 failed. The single failure (test_control_plane_env.py::test_bicep_references_every_guard_key,STORAGE_DATE_LAYOUT_ENABLED) is pre-existing and unrelated — this change touches noinfra/file.