An open, opinionated, five-engine FinOps toolkit for Azure. Replaces Advisor's average-based recommendations with peak-aware decisions, finds the hidden waste Advisor doesn't price, sizes Compute Reservations / Savings Plans and Storage Reservations against a configurable cancellation-exposure buffer, and turns every finding into an approve-ready, per-owner remediation queue β all from
az login, no agents, no SaaS.
Most FinOps tools fall into one of two camps:
- Reactive dashboards that re-state what Azure already shows you (Cost Management, Advisor) without changing the decision quality.
- Black-box SaaS platforms that ingest your billing data, charge a percentage of savings, and still hand-off to your engineers to actually act on the findings.
The FinOps Engine is neither. It is five small, deterministic Python engines that reproduce β and significantly improve on β the analysis a senior FinOps practitioner would do by hand:
| Engine | Replaces / improves on | Headline output |
|---|---|---|
rightsizing-peak |
Azure Advisor's average-based VM rightsizing | A list of Advisor recommendations that would have been unsafe under P95/P99 peak data |
hidden-waste |
Manual orphan / lifecycle hunts in Cost Management | Twelve categories of waste, priced against actual Β£, plus a starter Azure Policy pack |
ri-coverage |
Portal Reservations β Recommendations (single-SKU view) | A risk-scored VM RI / Compute Savings-Plan shortlist that fits inside your cancellation-exposure buffer |
storage-coverage |
Azure Monitor's 200-account-capped Storage insights view | A tenant-wide Storage Reserved-Capacity shortlist (Blob + Files) grouped by service Γ region Γ redundancy Γ tier, bounded by the same buffer |
context-enricher |
Spreadsheet round-trips between FinOps and domain teams | Per-owner GitHub Issue bodies β auto-routed via CODEOWNERS |
Every engine is read-only. None of them delete or modify resources. They emit Markdown, CSV, and (where appropriate) Azure Policy / Azure Workbook JSON. Remediation is always a human decision.
The absolute numbers vary wildly between tenants β a 50-subscription enterprise looks nothing like a 5-subscription startup. The shape of the findings, however, is remarkably consistent. On a tenant of any size you should expect, in a single afternoon:
- A small handful of Advisor downsize recommendations flagged as unsafe β workloads with bursty or batch profiles that Advisor's average-based logic would have downsized into a peak-hour outage. Even one is worth the run.
- A material chunk of recoverable hidden waste that Advisor doesn't price β typically dominated by old snapshots, empty App Service Plans, unattached premium disks (especially ASR seed disks), idle public IPs, and stopped-but-not-deallocated VMs. Single-digit-percent of monthly spend is a normal first-pass result.
- A risk-scored RI / Savings-Plan shortlist that fits inside whatever cancellation-exposure buffer your procurement function has set β and a quantified view of how much additional commitment would be unlocked by raising that buffer. The binding constraint is almost always procurement policy, not the data.
- One GitHub Issue per domain owner the next morning, with
accept/defer/rejectcheckboxes β replacing whatever recurring spreadsheet walkthrough your FinOps function does today.
The samples under samples/ show the full report set against
a synthetic Contoso tenant so you can see the output shape before you run
anything against your own data.
git clone https://github.com/prbeegala/FinOpsEngine.git
cd FinOpsEngine
# 1. Authenticate to Azure (Reader + Cost Management Reader is enough)
az login
az account set --subscription <default-sub-id>
# 2. Run any engine standalone β pick your scope
# --subs <a,b,c> explicit list
# --all-subs every enabled sub in the current tenant
# (add --exclude-subs / --tenant / --include-disabled as needed)
python tools/rightsizing-peak/rightsizing_peak.py `
--all-subs `
--days 30 `
--out-dir ./out/peak-rightsizing
python tools/hidden-waste/hidden_waste.py `
--all-subs `
--out-dir ./out/hidden-waste
python tools/ri-coverage/ri_coverage.py `
--all-subs `
--months 3 `
--refund-buffer 5000 `
--out-dir ./out/ri-coverage
python tools/storage-coverage/storage_coverage.py `
--all-subs `
--days 60 `
--refund-buffer 5000 `
--out-dir ./out/storage-coverage
# 3. Join everything into per-owner remediation queues
python tools/context-enricher/context_enricher.py `
--hidden-waste-csv ./out/hidden-waste/hidden-waste-<date>.csv `
--rightsizing-csv ./out/peak-rightsizing/tenant-peak-rightsizing-savings-<date>.csv `
--out-dir ./out/enrichedTip. Pin a sandbox or two for your first run rather than going tenant-wide. Use
--subs "<one-id>"and grow the scope once you're happy with the output.--all-subs --exclude-subs "<sandbox-id>"is the safest tenant-wide default.
Linux / macOS users: substitute \ for the PowerShell line-continuation
backtick. The Python is portable; only the shell quoting differs.
- Python 3.10+ (standard library only β no
pip installstep needed). - Azure CLI 2.55+ (
az --versionto check). - Permissions on the in-scope subscriptions:
Reader(Resource Graph, Monitor metrics, Advisor)Cost Management Reader(/queryREST API)- Optional:
Microsoft.Capacity/reservationOrders/readto subtract existing RI cover from the coverage gap (the engine works without it; the gap is then relative to measured PAYG rather than PAYG net of RI).
The engines never write to Azure. They issue GET / POST /query calls only.
βββββββββββββββββββββββββββββββββββββ
β Azure (your tenant) β
β β’ Resource Graph β
β β’ Azure Monitor metrics β
β β’ Cost Management /query β
β β’ Advisor β
βββββββββββββββββ¬ββββββββββββββββββββ
β az CLI (read-only)
ββββββββββββββββββββ¬ββββββββββββββββββΌββββββββββββββββββ¬βββββββββββββββββββ
βΌ βΌ βΌ βΌ βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βrightsizing- ββhidden-waste ββri-coverage ββstorage-coverage β
βpeak ββ ββ ββ β
β P95/P99 vs ββ 12 waste ββ FamilyΓregionββ Blob + Files RI β
β Advisor diff ββ classes + ββ VM RI / SP ββ per svcΓregΓ β
β ββ Policy pack ββ shortlist + ββ redundΓtier + β
β ββ ββ buffer ββ buffer β
ββββββββ¬ββββββββββββββββ¬ββββββββββββββββ¬ββββββββββββββββ¬ββββββββββββ
β CSV + MD β CSV + MD β CSV + MD β CSV + MD
βββββββββββββββββ΄ββββββββ¬ββββββββ΄ββββββββββββββββ
βΌ
ββββββββββββββββββββββββββββββββββββββββββββ
β context-enricher β
β β’ joins findings β tags via Resource β
β Graph β
β β’ HIGH / MED / LOW confidence scoring β
β β’ per-owner GitHub Issue bodies β
ββββββββββββββββββββββ¬ββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββ
β automation/finops- β
β nightly.yml (GitHub β
β Actions: 05:00 UTC) β
β β opens / updates β
β per-owner Issues β
ββββββββββββββββββββββββββββ
FinOpsEngine/
βββ tools/
β βββ rightsizing-peak/ Peak-aware VM rightsizing engine + Workbook
β βββ hidden-waste/ Orphan & lifecycle waste finder + Policy pack
β βββ ri-coverage/ Compute Reservation / Savings Plan shortlist
β βββ storage-coverage/ Storage Reserved-Capacity shortlist (Blob + Files)
β βββ context-enricher/ Tag join + per-owner Issue generator
βββ automation/
β βββ finops-nightly.yml GitHub Actions: nightly refresh + Issues
βββ samples/ Synthetic example outputs (runnable without Azure)
β βββ peak-rightsizing/
β βββ hidden-waste/
β βββ ri-coverage/
β βββ storage-coverage/
β βββ enriched/
βββ docs/ Methodology, FAQ, troubleshooting
βββ LICENSE
βββ requirements.txt
βββ README.md (this file)
The samples in
samples/are synthetic β generated to illustrate the output shape. None of the resource IDs, subscriptions, or numbers represent a real environment.
1. Peak-aware rightsizing β combined report (full file)
# Peak-Aware Rightsizing β Combined Pilot Report
| Subscription | VMs | Downsize | Keep | Upsize warn | Insufficient | Advisor unsafe |
|----------------|----:|---------:|-----:|------------:|-------------:|---------------:|
| ContosoApp.Prod| 7 | 1 | 6 | 0 | 0 | 0 |
| ContosoBatch | 22 | 7 | 5 | 9 | 1 | 1 |
| **Total** |**29**| **8**|**11**| **9** | **1** | **1** |
## The headline number
**1** of Azure Advisor's downsize recommendations across the pilot trio
would have been **unsafe** according to peak (P95/P99) workload data β the
manual-validation overhead this engine eliminates.2. Hidden waste β by category (full file)
# Hidden Waste & Lifecycle
- Subscriptions scanned: 20
- Findings: **1,196**
- Estimated monthly Β£ recoverable: **Β£12,952** (~Β£155,424 / yr)
| Category | Count | Monthly Β£ | Annualised Β£ |
|--------------------------------|------:|----------:|-------------:|
| Unattached managed disks | 592 | Β£4,845 | Β£58,143 |
| Old snapshots (>90d) | 54 | Β£3,053 | Β£36,635 |
| Hot-tier storage accounts (cold workload) | 11 | Β£2,210 | Β£26,520 |
| Empty App Service Plans | 20 | Β£1,717 | Β£20,607 |
| Oversized premium file shares | 6 | Β£820 | Β£9,840 |
| Under-utilised App Service Plans | 1 | Β£178 | Β£2,136 |
| Unused public IPs | 46 | Β£91 | Β£1,098 |
| Idle Standard load balancers | 2 | Β£30 | Β£360 |
| Stopped-not-deallocated VMs | 1 | Β£6.67 | Β£80 |
| Orphan NICs | 420 | Β£0.00 | Β£0.00 |
| Untouched blob containers (>90d) | 38 | Β£0.00 | Β£0.00 |
| Idle Container Apps (warm replicas) | 5 | Β£0.00 | Β£0.00 |3. Compute RI / SP shortlist (full file)
# RI / Savings-Plan Risk-Scored Shortlist
Buffer: **Β£5,000** cancellation exposure (configurable).
| # | FamilyΓRegion | Product | Annual Β£ committed | Annual savings | Exposure |
|--:|------------------|---------------|-------------------:|---------------:|---------:|
| 1 | Dsv5 / uksouth | Compute SP 1Y | Β£18,200| Β£3,094 | Β£2,184 |
| 2 | Esv5 / uksouth | RI 1Y | Β£14,800| Β£4,440 | Β£1,776 |
| 3 | Bs / uksouth | RI 1Y | Β£6,400| Β£1,920 | Β£768 |
| 4 | Fsv2 / westeu | Compute SP 1Y | Β£2,200| Β£374 | Β£264 |
| | **Buffer used** | | | **Β£9,828** | **Β£4,992** |4. Storage Reserved-Capacity shortlist (full file)
# Storage Reserved-Capacity Risk-Scored Shortlist
Buffer: **Β£5,000** cancellation exposure. Storage has **no Savings Plan** β
only RI 1Y / RI 3Y variants per (service Γ region Γ redundancy Γ tier).
| # | Service/Region/Redund./Tier | Product | Yr commit | Yr savings | Cancel exp. |
|--:|----------------------------------|----------------------|----------:|-----------:|------------:|
| 1 | files / uksouth / lrs / premium | Premium Files RI 1Y | Β£38,400 | Β£9,600 | Β£4,608 |
| 2 | blob / westeurope / lrs / hot | Blob Storage RI 3Y | Β£2,160 | Β£605 | Β£259 |
| | **Buffer used** | | | **Β£10,205**| **Β£4,867** |5. Per-owner remediation queue (full file)
# FinOps remediation queue β contoso-app-team β 2026-01-01
**14 findings Β· ~Β£1,230 / month (Β£14,760 / yr) recoverable.**
| Resource | Category | Env | Β£/mo |
|-----------------------|--------------------------------|------|-----:|
| pip-fe-prod-uks-08 | Unused public IPs | Prod | Β£3|
| disk-bkp-2024-q4 | Old snapshots (>90d) | Prod | Β£312|
| asp-legacy-portal-001 | Empty App Service Plans | Prod | Β£180|
| vm-batch-night-04 | Rightsize Standard_E16ds_v5 β | Prod | Β£420|
| | Standard_E8ds_v5 | | |
| ... | ... | ... | ... |
> Reply `accept`, `defer`, or `reject` per row.
> Generated by the FinOps Engine. Each row links back to the source engine.Each engine has its own README with full documentation:
tools/rightsizing-peak/README.mdβ P95/P99 decision tree, Advisor diff, downsize ladders.tools/hidden-waste/README.mdβ the twelve waste classes, pricing fallbacks, Policy pack.tools/ri-coverage/README.mdβ Compute Reservation / Savings Plan risk model, buffer guardrail, limitations.tools/storage-coverage/README.mdβ Storage Reserved-Capacity sizing (Blob + Files), meter-string classifier, non-reservable callout.tools/context-enricher/README.mdβ tag conventions, confidence scoring, GitHub Issue routing.automation/README.mdβ GitHub Actions deployment guide.
For the why behind the engines (rather than the how), see:
docs/methodology.mdβ peak-vs-average, the Β£-buffer mental model, deterministic-first / LLM-later.docs/troubleshooting.mdβ common permission errors, throttle behaviour, Windows quoting.docs/faq.mdβ frequently-asked questions.
Every engine calls az billing account list once at startup and
renders amounts in the tenant's billing currency (USD, EUR, SEK,
etc.). If detection fails (no Billing Reader, offline run,
multi-currency setup), the engine falls back to Β£ and prints a
one-line provenance message. Override with --currency-symbol '<glyph>'
on any engine.
Where the Β£ figures come from. Mostly your actual bill via Cost
Management ActualCost, not public retail price. EA / MCA discounts,
regional pricing, Hybrid Benefit, and dev/test rates are all baked in.
Public list price is only used as a thin fallback for never-attached disks
in hidden-waste (which never produce a billing record) and is explicitly
tagged estimate in the output. rightsizing-peak also uses Cost
Management ActualCost for its current_payg_cost /
recommended_payg_cost columns (issue #55); there is no retail-price
fallback for a recommended SKU absent from the tenant's billing history β
the cell stays blank with pricing_status = "no_data_for_recommended".
See the FAQ entry on pricing source
for the per-engine breakdown.
The engines are intentionally small and dependency-free. Common extensions:
- Different cloud / billing system β replace the Cost Management /query call with whichever billing API your platform exposes; the rest of the pipeline (CSV β enrichment β Issues) is generic.
- Different ticketing system β
context-enricherwrites Markdown bodies per owner underissues/. Swap thegh issue createstep infinops-nightly.ymlforjira,azure boards, ServiceNow, etc. - Different tag taxonomy β edit the
*_KEYStuples at the top ofcontext_enricher.py. Case-insensitive, first match wins. - LLM-ranked rationale β the README of
context-enricherwalks through layering a small model on top of the deterministic rationale once HIGH/MED accept rates are trusted.
The public backlog of new detectors, engine improvements, integrations, and
multi-cloud work lives in ROADMAP.md. Items are grouped by
theme and badged with rough size and risk. Open an Issue if you'd like
something bumped or want to pick one up.
A 12-slide overview deck β punchy, customer-facing, no real data β lives at
docs/finops-engine-overview.pptx.
Use it to introduce the engine to a customer or internal team. The
generator script (docs/build_overview_pptx.py)
is checked in too; edit and re-run with python docs/build_overview_pptx.py
(requires python-pptx β a docs-only dependency; the engines themselves
remain stdlib-only).
This project follows Semantic Versioning applied to
four explicit public contracts (CLI flags, output file shape, Workbook /
Policy IDs, Issue body shape). The full policy β including the deprecation
cycle, branch model, and v1.0.0 shipping criteria β is in
VERSIONING.md. The release-by-release history is in
CHANGELOG.md.
If you are pinning the engine in CI or a downstream Workbook, read
VERSIONING.md first.
Issues and PRs welcome. The engines deliberately avoid third-party Python dependencies (the standard library plus the Azure CLI is the contract); please keep that property if you submit a PR.
MIT.