bicep-cost
Estimate the monthly Azure cost of a Bicep (or compiled ARM) deployment from the command line, using the public Azure Retail Prices API (the same list prices the Azure pricing calculator uses). No Azure login needed.
uv tool install . # or: uvx --from . bicep-cost ...
bicep-cost estimate app/infra/main.bicep --set administratorLoginPassword=x
bicep-cost estimate other-app/infra/parameters/prod.bicepparam # uses the file's `using` template
bicep-cost estimate main.bicep -p parameters/prod.bicepparam --currency CHF --detail
bicep-cost estimate compiled.json -p main.parameters.json --format markdown # azd ${VAR} placeholders read from env
RESOURCE FIXED USAGE* TOTAL
serverfarms: app-plan 259.88 0.00 259.88
containerApps: app-api 217.28 0.00 217.28
flexibleServers: app-postgres 152.57 0.00 152.57
...
Provisioned (fixed): 869.11 USD
Usage-based* (assumed): 43.91 USD
TOTAL per month: 913.02 USD
How it works
.bicep/.bicepparamare compiled with the Bicep CLI ($BICEP_CLI,bicepon PATH,~/.azure/bin/bicep, oraz bicep). A compiled ARM.jsonskips this step.- A small ARM evaluator expands the template: parameters, variables,
condition,copyloops, nested deployments (Bicep modules) and their outputs. Values that only exist after deployment (reference()of live resources,listKeys(), unset parameters) become unknown and never silently turn into guesses. - One estimator per resource type turns resources into meter quantities; prices are looked up in the Retail Prices
API (cached in
~/.cache/bicep-cost, 7 days) and tiers / free grants are applied per subscription or workspace.
Fixed vs. usage-based
Every line is either fixed (follows from the template: plan SKU x instances, database compute, always-on
replicas, private endpoint hours, alert rules, cron-scheduled replicas) or usage-based* (depends on traffic or
data volume: storage GB and operations, log ingestion, session hours, function executions, AI tokens). Without
measurements, usage-based numbers come from small built-in defaults (e.g. 10 GB per storage account, 2 GB of logs) -
good enough to see the shape, not to quote. --detail lists every assumption used and the report says how many
figures are still defaults.
Measure the real values from a deployed environment
az login
bicep-cost collect app/infra/main.bicep --set administratorLoginPassword=x \
--resource-group my-resource-group # -> main.usage.toml (next to the template)
bicep-cost collect other-app/infra/main.bicep -p other-app/infra/parameters/prod.bicepparam \
--resource-group <rg> --days 60 # -> parameters/prod.usage.toml
bicep-cost estimate app/infra/main.bicep ... # picks main.usage.toml up automatically
collect is read-only (GETs, Azure Monitor metric reads and two read-only POST queries: Log Analytics Usage and
Cost Management query). It evaluates the template with the real subscription and resource group so that resource ids
match the deployed ones, then writes a commented usage file:
| Resource | Measured from | Usage key |
|---|---|---|
| Storage account | blob service BlobCapacity, Transactions by ApiName (read / write / other) |
stored_gb, *_operations_10k |
| File share | FileCapacity, Transactions (account total split over its shares on standard accounts) |
used_gb, *_operations_10k |
| Log Analytics | Usage table (billable GB) + top tables; App Insights in the same workspace is set to 0 |
ingest_gb_per_month |
| Container App | Replicas (hourly), UsageNanoCores/RxBytes/TxBytes (5-min idle share), Requests |
replica_hours, idle_percent, requests_millions |
| Session pool, AI/Foundry account | Cost Management actual cost (pool metrics do not reflect billed session-hours) | monthly_cost |
| Function app (Consumption/Flex) | FunctionExecutionCount, FunctionExecutionUnits |
executions_per_month, gb_seconds_per_month |
| Key Vault / private endpoint / private DNS zone / Service Bus | ServiceApiHit / PEBytesIn/Out / QueryVolume / message counts |
operations_10k / *_gb / queries_millions / operations_millions |
| App Service plan, PostgreSQL | live resource: instance count, storageSizeGB (auto-grow) |
instances, storage_gb |
It also prints drift (template says 32 GB / 2 instances, live says 64 GB / 3) and each resource's actual cost of the
window as a comment, so you can sanity-check the estimate. Files are merged key by key (--usage a.toml --usage b.toml, later wins), so hand-written overrides can live in a separate file. bicep-cost init-usage main.bicep writes a
file with every tunable value and its default instead.
Usage file lookup for estimate: explicit --usage (repeatable), else bicep-cost.usage.toml and
<params-or-template stem>.usage.toml next to the input. Tables match the resource name, exact or glob
([storage."stprod*"]); see examples/usage.toml.
Caveats of the measurements: windows are scaled to a month (730 h); idle_percent is judged on 5-minute windows and
can be lower than what is billed; Container Apps free grants are subscription-wide, so if other apps in the
subscription use them up, the estimate understates; monthly_cost values are in the billing currency recorded as
[meta] cost_currency (the estimate then defaults to that currency, no conversion happens).
Negotiated discounts
List prices are what the API returns. If you pay less (EA/MCA discounts, reservations), say so per resource type:
[discounts]
"Microsoft.Web/serverfarms" = 30 # percent off list
"Microsoft.DBforPostgreSQL/flexibleServers" = 30
default = 0
Supported resource types and tiers
| Area | Types | Tiers / SKUs covered |
|---|---|---|
| App Service | serverfarms, sites, slots |
Free; Basic B1-3; Standard S1-3; Premium v2/v3/v4 (incl. P1mv3); Isolated v2 (stamp fee not included); Linux and Windows. Sites/slots ride on the plan; sites on plans outside the template are reported as external |
| Functions / Logic Apps | serverfarms + sites |
Consumption (Y1), Flex (FC1: always-ready + on-demand), Elastic Premium EP1-3, Logic Apps Standard WS1-3 |
| Container Apps | managedEnvironments, containerApps, sessionPools |
Consumption profile (active/idle rates, free grants, KEDA cron schedules), dedicated profiles D4-D32 / E4-E32 (+ plan management fee), dynamic sessions (Python/Node) and custom-container pools |
| Storage | storageAccounts, file shares |
LRS / ZRS / GRS / RA-GRS / GZRS / RA-GZRS; Hot / Cool / Cold; Blob, ADLS Gen2 (HNS), standard file shares (TransactionOptimized / Hot / Cool). Not Premium |
| PostgreSQL | flexibleServers |
Burstable, General Purpose, Memory Optimized (any series); HA doubles compute + storage; Premium SSD v2 IOPS / throughput; backup storage above the free allowance (geo-redundant aware) |
| Azure SQL | servers/databases, servers/elasticPools, managedInstances |
vCore: General Purpose (provisioned and serverless), Business Critical, Hyperscale (+ HA replicas), zone redundancy, Gen5 / DC / Fsv2 / M families; DTU: Basic, Standard S0-S12, Premium P1-P15; DTU elastic pools; Managed Instance GP / BC. Databases in a pool are included. Usage keys use the full name server/database. Azure Hybrid Benefit is not applied (price shown includes the license). Not Synapse dedicated pools, LTR backups |
| Virtual machines | virtualMachines, virtualMachineScaleSets, disks, publicIPAddresses |
any VM size (by armSkuName), Linux / Windows / Hybrid Benefit, Spot; OS + data disks (Premium / Standard SSD / Standard HDD, LRS / ZRS, tier from size); ephemeral OS disks free; VMSS = instances x VM (set instances for autoscale). Not Ultra / Premium SSD v2 disks, SQL / RHEL / SUSE license surcharges, load balancers, gateways |
| Service Bus | namespaces |
Basic, Standard (base fee + tiered operations, first 13 M included), Premium (1/2/4/8/16 messaging units) |
| Monitoring | workspaces, components, scheduledQueryRules |
PerGB2018 (free 5 GB, tiers), commitment tiers, retention > 31 d. Not Basic/Auxiliary table plans |
| Key Vault | vaults |
Standard / Premium operations. Not HSM keys or managed HSM |
| Network | privateEndpoints, privateDnsZones |
per-hour + data processed; zone tiers (first 25 zones). Not gateways, firewalls, public IPs |
| AI | CognitiveServices/accounts |
no base fee; spend via monthly_cost (measured by collect). Not provisioned throughput (PTU) |
| Free (explicitly known) | role assignments, VNets/subnets, DNS records and links, budgets, dashboards, diagnostic settings, secrets, access policies, databases/configs, blob containers, queues/topics, ... |
Anything else is listed under Not covered by this tool (cost NOT included); use --strict to fail in that case.
Premium storage, ASE stamps, bandwidth/egress and reservations are not modelled; prices are pay-as-you-go list
prices unless you add [discounts] or opt into account price sheets (below).
Add a resource type by writing estimate_x(res, ctx) -> ResourceEstimate in bicep_cost/estimators/ and registering it
in estimators/__init__.py.
Other commands and flags
bicep-cost estimate ... --format json -o head.json # also: markdown (PR comments), csv
bicep-cost diff base.json head.json # per-resource cost change between two reports
bicep-cost estimate ... --fail-above 1500 # exit 3 above a monthly budget (CI guard rail)
bicep-cost estimate ... --strict # exit 2 if something could not be priced
bicep-cost estimate ... --group-by type|module # summary table instead of per-resource rows
bicep-cost prices --service "Azure Container Apps" --meter-contains Idle # browse meters
Other options: --location (default region and resourceGroup().location, default westeurope), --resource-group,
--subscription-id, --offline / --refresh / --no-cache, -v (log API calls).
Development
uv sync --group test --group dev
uv run -m pytest
uv run ruff check . && uv run ruff format --check .
uv run ty check --error-on-warning
Currency
Estimates use USD by default. Set --currency CHF (or EUR, for example) on
estimate and prices to request that currency directly from Azure:
bicep-cost estimate main.bicep --currency CHF
bicep-cost prices --service "Key Vault" --currency EUR
For estimates the precedence is --currency, then [meta] cost_currency in the
usage TOML, then USD. Codes are case insensitive. Reports and budget limits use
the selected currency; cached prices are isolated by currency. Manual
monthly_cost values must use the same currency as the estimate; conflicting
usage-file metadata is rejected. No exchange-rate conversion is performed.
Diffs require both reports to use the same currency.
Azure's non-USD retail rates are budget estimates, not negotiated billing rates. See Microsoft's Retail Prices API currency documentation.
Percentage cost caps in CI
Save a baseline estimate (using the same currency and usage assumptions), then fail if the new monthly cost grows by more than 10 percent:
bicep-cost estimate main.bicep --format json -o base.json
# After changing the template:
bicep-cost estimate main.bicep --baseline base.json --fail-increase-percent 10 --fail-above 1500
# Or compare two saved JSON reports:
bicep-cost diff base.json head.json --fail-increase-percent 10 --fail-above 1500
Either exceeded cap returns 3. Equality and decreases pass; any positive cost
from a zero baseline fails the relative cap. Thresholds must be finite and
nonnegative. The full report is written before a budget failure, including with
--output, so CI can always retain it. --strict failures still take precedence
(exit 2). Baseline reports must use the same currency; no conversion is applied.
Cost breakdown charts
Text and Markdown estimates include horizontal bars ordered from the most
expensive resource to the least. Each bar shows the monthly amount and share of
the known total. Text --group-by type or --group-by module aggregates both the
chart and table. Zero-cost resources do not get bars; unresolved and unsupported
resources remain listed below, since their costs are not included in the total.
Charts are plain text for readable CI logs and Markdown PR comments, and are
written even when a budget cap fails. JSON and CSV remain machine-readable.
Automatic releases
Like bmsui, main pushes run reusable CI, detect a new package version, publish to PyPI using OIDC, and create a GitHub release with wheel/sdist assets. PR CI checks Ruff, ty, tests, builds and an installed-wheel smoke test on Python 3.11 and 3.14.
Before the first merge, a PyPI maintainer must register a
pending trusted publisher:
project bicep-cost, owner bmsuisse, repository bicep-cost, workflow
release.yml, environment pypi. Use the same environment in GitHub;
restrict its deployment branches to main. No long-lived PyPI token is needed.
Bump [project] version in pyproject.toml, run uv lock, and merge the PR.
Main publishes that version after CI passes; an existing GitHub release is
skipped. The Release workflow can also be dispatched manually from main; its
dry_run input defaults to true, validating without publishing. Release jobs
are serialized. If publication succeeded but release creation failed, rerun the
workflow: existing PyPI files are skipped and GitHub release creation is retried.
Do not change files for a version already uploaded to PyPI; bump the version.
Azure account prices
Retail prices remain the default and require no login. To use negotiated consumption rates through the logged-in Azure CLI user:
az login
bicep-cost estimate main.bicep --azure-prices --subscription-id YOUR_SUBSCRIPTION_ID
# Persist a private snapshot while estimating:
bicep-cost estimate main.bicep --azure-prices --save-price-sheet account-prices.json
# Or download separately:
bicep-cost price-sheet --subscription-id YOUR_SUBSCRIPTION_ID -o account-prices.json
# Reuse without Azure login:
bicep-cost estimate main.bicep --price-sheet account-prices.json
Without a subscription argument, downloads use the CLI's current subscription. The read-only Consumption Price Sheet API requires access to the subscription's pricing and a supported billing agreement. An ordinary Azure login alone does not guarantee access. Unsupported agreements, missing sheets and denied access produce an error. Within a successfully loaded price sheet, missing meter rates fall back to retail prices with warnings.
For MCA/MPA accounts, or when subscription downloads are unavailable, download
and extract the current consumption price-sheet CSV from
Cost Management + Billing,
then use --price-sheet prices.csv. The importer accepts case-insensitive column
names meterId, optional skuId, currencyCode or currency, unitOfMeasure,
unitPrice, optional tierMinimumUnits and priceType. It uses price currency,
not billingCurrency; reservations and savings-plan rows are excluded. Import a
single current price period: conflicting duplicate rates are rejected. Legacy
CSV layouts must be converted to these columns first. Included-quantity grants
are rejected because the sheet does not supply enough billing-scope information
to allocate them safely.
Account mode discovers meter identities through the Retail API, then replaces
rates by meter/SKU ID, normalizing compatible block units (for example 100 Hours
to 1 Hour). Missing account meter rates use retail prices with an explicit pricing
warning in text, Markdown, and JSON reports; --strict does not block this
fallback. Resources with no retail meter still become unresolved. Manual [discounts] are ignored because account rates
already include discounts. Reports record the sheet's origin and snapshot time.
Explicit --currency and usage-file currency must match the sheet; by default
its currency is selected. No exchange-rate conversion is performed.
Snapshots contain negotiated rates, not Azure tokens, and are saved atomically
with owner-only permissions. Keep them private and out of source control. Saved
sheets can be used without login; completely offline estimates also need the
Retail API metadata in the normal price cache. Use --price-sheet FILE --offline
for that; --azure-prices always downloads fresh rates. Snapshots are not refreshed
automatically, so download a new sheet when your agreement or billing period changes.
Quality gates
Ruff uses the strict BMS Python rule families from init-app-stack (annotations,
modern syntax, imports, bugbear, performance, pathlib, naming and logging).
The target stays Python 3.11, matching package support. FastAPI-only rules do
not apply to this CLI; CLI printing is allowed specifically in cli.py and the
file-size guard. Relative imports are banned. ANN401 (explicit Any) is a
nonblocking warning, matching the BMS scaffold. Production and pytest function
signatures are annotated; no test-wide annotation exception is used.
Ty checks fail on warnings, with possibly missing attributes/imports and missing generic type arguments treated as errors. Default type diagnostics remain active; optional experimental rules are not enabled indiscriminately. CI runs the same checks and formatting verification on Python 3.11 and 3.14.
uv sync --locked --all-groups
uv run prek install # enable the pre-commit hook
uv run prek run --all-files # format, lint, typecheck, YAML and file-size checks
The hooks use the repository's locked tool versions. Re-run after automatic fixes and stage the resulting changes before committing. Pytest remains a CI check; run focused tests locally for behavior you change.
Metadata
Release files for bicep-cost 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| bicep_cost-0.1.1.tar.gz | 101.5 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| bicep_cost-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 185.0 kB
Release files / bicep_cost-0.1.1.tar.gz
| Download URL | bicep_cost-0.1.1.tar.gz |
|---|---|
| Size | 101.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bb7f7cbf15223cad984fce314154575356e4ed2cf68ea7597f3cd4876577fb9a
|
|
BLAKE2b-256 checksum How to use checksums |
3ef000723bb707ffa1f3976df7d8badb3abc4ede38f1cf7351841fba59693409
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.
Transparency logRelease files / bicep_cost-0.1.1-py3-none-any.whl
| Download URL | bicep_cost-0.1.1-py3-none-any.whl |
|---|---|
| Size | 83.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e83622702dc5a5450bab8ae2a9e600572c61fa556669a9c4a3402bb7b4ce14f5
|
|
BLAKE2b-256 checksum How to use checksums |
a55d97e766952ce0cda0529e96f438581be546b78ac88c5b3ec01be90bf68ceb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.
Transparency logSecurity for bicep-cost 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for bicep_cost-0.1.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.