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

Measure Azure Reserved Instance Coverage Before On-Demand Spend Drifts

Measure Azure Reserved Instance coverage against your target, list unbound on-demand VM hours, and escalate below target with a GitHub issue and Slack alert.

Categories
CloudInfrastructure

How it works

  1. weekly_coverage_check (io.kestra.plugin.core.trigger.Schedule, cron 0 6 * * 1) is the primary trigger and ships disabled: true, so the estate is measured only when an operator switches it on. on_demand_check (io.kestra.plugin.core.trigger.Webhook, keyed) starts an on-demand run after a reservation purchase, a large scale-out, or at month end.
  2. usage_workspace (io.kestra.plugin.core.flow.WorkingDirectory) opens one shared directory so the collector, the parser, and both report files stay together for the whole run.
  3. collect_usage (io.kestra.plugin.scripts.shell.Commands on the io.kestra.plugin.scripts.runner.docker.Docker runner, image mcr.microsoft.com/azure-cli:latest) writes ri-coverage.json, the purchased and utilized reservation hours per reservation, and ondemand-hours.json, the per-VM-size unbound on-demand hours, with heredocs. It then logs in with the service principal and runs az reservation show --output json || true, so a worker without Azure credentials still finishes the run on the bundled report pair instead of failing the gate. entryPoint: [] clears the image entrypoint so the shell script runs instead of az.
  4. compute_coverage (io.kestra.plugin.scripts.python.Script on python:3.12-slim) reads both files and computes coverage_pct as utilized reservation hours over purchased reservation hours times 100, counts purchased and fully utilized reservations, builds unbound as an array of {vm_size, hours, est_monthly_usd} rows sorted by monthly cost, and sets verdict to PASS when coverage reaches coverage_target and FAIL otherwise. Unreadable or missing files degrade to zero coverage and a FAIL, never a silent pass.
  5. coverage_gate (io.kestra.plugin.core.flow.If) compares coverage_pct with coverage_target. Below the target, open_coverage_issue (io.kestra.plugin.github.issues.Create) files a labeled issue with a markdown table of every unbound VM size, alert_slack posts the gap and the monthly unbound total, and log_rightsizing_hints (io.kestra.plugin.core.flow.Loop) writes one right-sizing hint per unbound VM size into the execution log. At or above the target, log_covered records the all-clear with the same numbers.
  6. The errors block alerts Slack whenever the collection or the parse breaks, because a broken collector must never be read as a fully covered estate.

What you get

  • One number that answers the FinOps question, coverage_pct measured against your own coverage_target, computed from reservation hours instead of a portal screenshot.
  • The unbound list: every VM size still paying on-demand rates, with hours and estimated monthly dollars, sorted by spend.
  • Escalation where engineers already work, a labeled GitHub issue carrying the full unbound table plus a Slack ping with the gap and the total.
  • A right-sizing hint per unbound VM size in the execution log, so the fix is stated where the run is reviewed.
  • Gateable outputs, verdict, coverage_pct, unbound and purchased_ris, any downstream flow or dashboard can branch on them.

Who it's for

  • FinOps and cloud cost owners who need to know what share of compute is actually reserved.
  • Platform teams running a mixed estate of reservations and on-demand VMs across subscriptions.
  • Engineering managers who want under-coverage to become tracked work instead of an invoice surprise.

Why orchestrate this with Kestra

Reservation coverage is easy to check once and easy to forget, and the Azure portal answers per reservation rather than per estate. Kestra wraps the check in a disabled-by-default weekly safety net plus a keyed webhook for on-demand runs, runs the CLI and the parser in pinned containers so the toolchain lives in version control, branches the verdict on an explicit threshold, and routes the shortfall into GitHub and Slack from one definition. The report pair ships as bundled fixtures, so the same flow can be exercised in CI before any service principal exists.

Prerequisites

  • A worker with Docker access, the collector runs in mcr.microsoft.com/azure-cli and the parser in python:3.12-slim.
  • A service principal with Reader access on the subscription for the live az reservation show probe. Without one the flow completes on the bundled report pair.
  • A GitHub token that can open issues in github_repository (classic PAT scope repo, or fine-grained Issues: write).
  • A Slack incoming webhook for the under-coverage alert and for failure alerts.

Secrets

  • RI_COVERAGE_WEBHOOK_KEY: key segment guarding the on-demand webhook URL, anyone holding it can start a coverage check.
  • AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET: service principal used for the best-effort az login.
  • GITHUB_TOKEN: token used to open the under-coverage issue.
  • SLACK_WEBHOOK_URL: Slack incoming webhook URL.

Quick start

  1. Add the six secrets above to your Kestra namespace.
  2. Run the flow once with the default inputs and read coverage_pct, unbound and verdict from the outputs. The bundled report pair lands at 87.14 percent against a 90 percent target, so the first run escalates.
  3. Point github_repository at a repository where issues are welcome, then re-run to watch the issue and the Slack message appear.
  4. Raise coverage_target above the measured coverage to watch log_covered record the all-clear, or lower it to keep the run quiet.
  5. Trigger on_demand_check at /api/v1/{tenant}/executions/webhook/{namespace}/azure-reserved-instance-coverage-checker/{RI_COVERAGE_WEBHOOK_KEY}, or enable weekly_coverage_check once the service principal is in place.

How to extend

  • Fan subscription_id out with a Loop over a subscription list and aggregate coverage across the whole tenant.
  • Replace the bundled report pair with az reservation list-associations and consumption exports when live numbers are wanted on every run.
  • Add a io.kestra.plugin.core.execution.Fail under coverage_gate so pipelines waiting on coverage stop instead of only being notified.
  • Publish coverage_pct to a warehouse on every run to chart the trend toward the target.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.