bicep-cost 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bicep_cost-0.1.0/.github/workflows/ci.yml +32 -0
- bicep_cost-0.1.0/.github/workflows/release.yml +73 -0
- bicep_cost-0.1.0/.gitignore +5 -0
- bicep_cost-0.1.0/PKG-INFO +272 -0
- bicep_cost-0.1.0/README.md +264 -0
- bicep_cost-0.1.0/bicep_cost/__init__.py +1 -0
- bicep_cost-0.1.0/bicep_cost/armexpr.py +872 -0
- bicep_cost-0.1.0/bicep_cost/calc.py +104 -0
- bicep_cost-0.1.0/bicep_cost/cli.py +414 -0
- bicep_cost-0.1.0/bicep_cost/collect/__init__.py +1 -0
- bicep_cost-0.1.0/bicep_cost/collect/arm.py +214 -0
- bicep_cost-0.1.0/bicep_cost/collect/collectors.py +543 -0
- bicep_cost-0.1.0/bicep_cost/collect/toml_out.py +43 -0
- bicep_cost-0.1.0/bicep_cost/compile.py +161 -0
- bicep_cost-0.1.0/bicep_cost/context.py +122 -0
- bicep_cost-0.1.0/bicep_cost/engine.py +544 -0
- bicep_cost-0.1.0/bicep_cost/estimators/__init__.py +99 -0
- bicep_cost-0.1.0/bicep_cost/estimators/aca.py +297 -0
- bicep_cost-0.1.0/bicep_cost/estimators/appservice.py +214 -0
- bicep_cost-0.1.0/bicep_cost/estimators/common.py +68 -0
- bicep_cost-0.1.0/bicep_cost/estimators/cron.py +72 -0
- bicep_cost-0.1.0/bicep_cost/estimators/keyvault.py +36 -0
- bicep_cost-0.1.0/bicep_cost/estimators/monitor.py +121 -0
- bicep_cost-0.1.0/bicep_cost/estimators/network.py +59 -0
- bicep_cost-0.1.0/bicep_cost/estimators/postgres.py +117 -0
- bicep_cost-0.1.0/bicep_cost/estimators/servicebus.py +55 -0
- bicep_cost-0.1.0/bicep_cost/estimators/sql.py +290 -0
- bicep_cost-0.1.0/bicep_cost/estimators/storage.py +128 -0
- bicep_cost-0.1.0/bicep_cost/estimators/vm.py +226 -0
- bicep_cost-0.1.0/bicep_cost/model.py +72 -0
- bicep_cost-0.1.0/bicep_cost/prices.py +345 -0
- bicep_cost-0.1.0/bicep_cost/pricesheet.py +192 -0
- bicep_cost-0.1.0/bicep_cost/report.py +314 -0
- bicep_cost-0.1.0/bicep_cost/usage.py +129 -0
- bicep_cost-0.1.0/examples/usage.toml +59 -0
- bicep_cost-0.1.0/pyproject.toml +36 -0
- bicep_cost-0.1.0/tests/__init__.py +0 -0
- bicep_cost-0.1.0/tests/conftest.py +281 -0
- bicep_cost-0.1.0/tests/fixtures/app.json +43 -0
- bicep_cost-0.1.0/tests/fixtures/main.bicep +14 -0
- bicep_cost-0.1.0/tests/fixtures/prod.bicepparam +3 -0
- bicep_cost-0.1.0/tests/test_armexpr.py +100 -0
- bicep_cost-0.1.0/tests/test_cli.py +324 -0
- bicep_cost-0.1.0/tests/test_collect.py +348 -0
- bicep_cost-0.1.0/tests/test_cron.py +32 -0
- bicep_cost-0.1.0/tests/test_engine.py +202 -0
- bicep_cost-0.1.0/tests/test_estimators.py +447 -0
- bicep_cost-0.1.0/tests/test_prices.py +107 -0
- bicep_cost-0.1.0/tests/test_pricesheet.py +199 -0
- bicep_cost-0.1.0/tests/test_report.py +28 -0
- bicep_cost-0.1.0/tests/test_servicebus.py +42 -0
- bicep_cost-0.1.0/tests/test_sql_vm.py +227 -0
- bicep_cost-0.1.0/uv.lock +137 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
workflow_call:
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
check:
|
|
12
|
+
name: Python ${{ matrix.python }}
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
python: ['3.11', '3.14']
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: astral-sh/setup-uv@v7
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python }}
|
|
22
|
+
- run: uv sync --locked --all-groups
|
|
23
|
+
- run: uv run ruff check .
|
|
24
|
+
- run: uv run ty check
|
|
25
|
+
- run: uv run pytest
|
|
26
|
+
- run: uv build
|
|
27
|
+
- name: Smoke-test installed wheel outside the checkout
|
|
28
|
+
run: |
|
|
29
|
+
uv venv /tmp/bicep-cost-smoke
|
|
30
|
+
uv pip install --python /tmp/bicep-cost-smoke/bin/python dist/*.whl
|
|
31
|
+
cd /tmp
|
|
32
|
+
/tmp/bicep-cost-smoke/bin/bicep-cost --help
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
inputs:
|
|
8
|
+
dry_run:
|
|
9
|
+
description: Validate and report the release without publishing
|
|
10
|
+
type: boolean
|
|
11
|
+
default: true
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: release
|
|
18
|
+
cancel-in-progress: false
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
test:
|
|
22
|
+
uses: ./.github/workflows/ci.yml
|
|
23
|
+
|
|
24
|
+
plan:
|
|
25
|
+
needs: test
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
outputs:
|
|
28
|
+
version: ${{ steps.plan.outputs.version }}
|
|
29
|
+
release: ${{ steps.plan.outputs.release }}
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v4
|
|
32
|
+
- uses: actions/setup-python@v5
|
|
33
|
+
with:
|
|
34
|
+
python-version: '3.14'
|
|
35
|
+
- id: plan
|
|
36
|
+
env:
|
|
37
|
+
GH_TOKEN: ${{ github.token }}
|
|
38
|
+
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
|
|
39
|
+
run: |
|
|
40
|
+
VERSION=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')
|
|
41
|
+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
42
|
+
if gh release view "v$VERSION" >/dev/null 2>&1; then
|
|
43
|
+
echo 'release=false' >> "$GITHUB_OUTPUT"
|
|
44
|
+
echo "v$VERSION already released" >> "$GITHUB_STEP_SUMMARY"
|
|
45
|
+
else
|
|
46
|
+
echo 'release=true' >> "$GITHUB_OUTPUT"
|
|
47
|
+
echo "Ready to release v$VERSION (dry run: $DRY_RUN)" >> "$GITHUB_STEP_SUMMARY"
|
|
48
|
+
fi
|
|
49
|
+
publish:
|
|
50
|
+
needs: [test, plan]
|
|
51
|
+
if: >-
|
|
52
|
+
github.ref == 'refs/heads/main' && needs.plan.outputs.release == 'true' &&
|
|
53
|
+
(github.event_name != 'workflow_dispatch' || !inputs.dry_run)
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
environment: pypi
|
|
56
|
+
permissions:
|
|
57
|
+
contents: write
|
|
58
|
+
id-token: write
|
|
59
|
+
steps:
|
|
60
|
+
- uses: actions/checkout@v4
|
|
61
|
+
- uses: astral-sh/setup-uv@v7
|
|
62
|
+
with:
|
|
63
|
+
python-version: '3.14'
|
|
64
|
+
- run: uv build
|
|
65
|
+
- name: Publish to PyPI with OIDC
|
|
66
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
67
|
+
with:
|
|
68
|
+
skip-existing: true
|
|
69
|
+
- name: Create GitHub release and tag
|
|
70
|
+
env:
|
|
71
|
+
GH_TOKEN: ${{ github.token }}
|
|
72
|
+
VERSION: ${{ needs.plan.outputs.version }}
|
|
73
|
+
run: gh release create "v$VERSION" dist/* --target "$GITHUB_SHA" --title "v$VERSION" --generate-notes
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: bicep-cost
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Estimate monthly Azure costs from Bicep / ARM templates using the public Azure Retail Prices API
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# bicep-cost
|
|
10
|
+
|
|
11
|
+
Estimate the monthly Azure cost of a Bicep (or compiled ARM) deployment from the command line, using the public
|
|
12
|
+
[Azure Retail Prices API](https://learn.microsoft.com/rest/api/cost-management/retail-prices/azure-retail-prices)
|
|
13
|
+
(the same list prices the Azure pricing calculator uses). No Azure login needed.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv tool install . # or: uvx --from . bicep-cost ...
|
|
17
|
+
|
|
18
|
+
bicep-cost estimate app/infra/main.bicep --set administratorLoginPassword=x
|
|
19
|
+
bicep-cost estimate other-app/infra/parameters/prod.bicepparam # uses the file's `using` template
|
|
20
|
+
bicep-cost estimate main.bicep -p parameters/prod.bicepparam --currency CHF --detail
|
|
21
|
+
bicep-cost estimate compiled.json -p main.parameters.json --format markdown # azd ${VAR} placeholders read from env
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
RESOURCE FIXED USAGE* TOTAL
|
|
26
|
+
serverfarms: app-plan 259.88 0.00 259.88
|
|
27
|
+
containerApps: app-api 217.28 0.00 217.28
|
|
28
|
+
flexibleServers: app-postgres 152.57 0.00 152.57
|
|
29
|
+
...
|
|
30
|
+
Provisioned (fixed): 869.11 USD
|
|
31
|
+
Usage-based* (assumed): 43.91 USD
|
|
32
|
+
TOTAL per month: 913.02 USD
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## How it works
|
|
36
|
+
|
|
37
|
+
1. `.bicep` / `.bicepparam` are compiled with the Bicep CLI (`$BICEP_CLI`, `bicep` on PATH, `~/.azure/bin/bicep`, or
|
|
38
|
+
`az bicep`). A compiled ARM `.json` skips this step.
|
|
39
|
+
2. A small ARM evaluator expands the template: parameters, variables, `condition`, `copy` loops, nested deployments
|
|
40
|
+
(Bicep modules) and their outputs. Values that only exist after deployment (`reference()` of live resources,
|
|
41
|
+
`listKeys()`, unset parameters) become *unknown* and never silently turn into guesses.
|
|
42
|
+
3. One estimator per resource type turns resources into meter quantities; prices are looked up in the Retail Prices
|
|
43
|
+
API (cached in `~/.cache/bicep-cost`, 7 days) and tiers / free grants are applied per subscription or workspace.
|
|
44
|
+
|
|
45
|
+
## Fixed vs. usage-based
|
|
46
|
+
|
|
47
|
+
Every line is either **fixed** (follows from the template: plan SKU x instances, database compute, always-on
|
|
48
|
+
replicas, private endpoint hours, alert rules, cron-scheduled replicas) or **usage-based\*** (depends on traffic or
|
|
49
|
+
data volume: storage GB and operations, log ingestion, session hours, function executions, AI tokens). Without
|
|
50
|
+
measurements, usage-based numbers come from small built-in defaults (e.g. 10 GB per storage account, 2 GB of logs) -
|
|
51
|
+
good enough to see the shape, not to quote. `--detail` lists every assumption used and the report says how many
|
|
52
|
+
figures are still defaults.
|
|
53
|
+
|
|
54
|
+
### Measure the real values from a deployed environment
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
az login
|
|
58
|
+
bicep-cost collect app/infra/main.bicep --set administratorLoginPassword=x \
|
|
59
|
+
--resource-group my-resource-group # -> main.usage.toml (next to the template)
|
|
60
|
+
bicep-cost collect other-app/infra/main.bicep -p other-app/infra/parameters/prod.bicepparam \
|
|
61
|
+
--resource-group <rg> --days 60 # -> parameters/prod.usage.toml
|
|
62
|
+
|
|
63
|
+
bicep-cost estimate app/infra/main.bicep ... # picks main.usage.toml up automatically
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`collect` is **read-only** (GETs, Azure Monitor metric reads and two read-only POST queries: Log Analytics `Usage` and
|
|
67
|
+
Cost Management `query`). It evaluates the template with the real subscription and resource group so that resource ids
|
|
68
|
+
match the deployed ones, then writes a commented usage file:
|
|
69
|
+
|
|
70
|
+
| Resource | Measured from | Usage key |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| Storage account | blob service `BlobCapacity`, `Transactions` by `ApiName` (read / write / other) | `stored_gb`, `*_operations_10k` |
|
|
73
|
+
| File share | `FileCapacity`, `Transactions` (account total split over its shares on standard accounts) | `used_gb`, `*_operations_10k` |
|
|
74
|
+
| Log Analytics | `Usage` table (billable GB) + top tables; App Insights in the same workspace is set to 0 | `ingest_gb_per_month` |
|
|
75
|
+
| Container App | `Replicas` (hourly), `UsageNanoCores`/`RxBytes`/`TxBytes` (5-min idle share), `Requests` | `replica_hours`, `idle_percent`, `requests_millions` |
|
|
76
|
+
| Session pool, AI/Foundry account | Cost Management actual cost (pool metrics do not reflect billed session-hours) | `monthly_cost` |
|
|
77
|
+
| Function app (Consumption/Flex) | `FunctionExecutionCount`, `FunctionExecutionUnits` | `executions_per_month`, `gb_seconds_per_month` |
|
|
78
|
+
| Key Vault / private endpoint / private DNS zone / Service Bus | `ServiceApiHit` / `PEBytesIn/Out` / `QueryVolume` / message counts | `operations_10k` / `*_gb` / `queries_millions` / `operations_millions` |
|
|
79
|
+
| App Service plan, PostgreSQL | live resource: instance count, `storageSizeGB` (auto-grow) | `instances`, `storage_gb` |
|
|
80
|
+
|
|
81
|
+
It also prints **drift** (template says 32 GB / 2 instances, live says 64 GB / 3) and each resource's actual cost of the
|
|
82
|
+
window as a comment, so you can sanity-check the estimate. Files are merged key by key (`--usage a.toml --usage
|
|
83
|
+
b.toml`, later wins), so hand-written overrides can live in a separate file. `bicep-cost init-usage main.bicep` writes a
|
|
84
|
+
file with every tunable value and its default instead.
|
|
85
|
+
|
|
86
|
+
Usage file lookup for `estimate`: explicit `--usage` (repeatable), else `bicep-cost.usage.toml` and
|
|
87
|
+
`<params-or-template stem>.usage.toml` next to the input. Tables match the resource name, exact or glob
|
|
88
|
+
(`[storage."stprod*"]`); see [`examples/usage.toml`](examples/usage.toml).
|
|
89
|
+
|
|
90
|
+
Caveats of the measurements: windows are scaled to a month (730 h); `idle_percent` is judged on 5-minute windows and
|
|
91
|
+
can be lower than what is billed; Container Apps free grants are subscription-wide, so if other apps in the
|
|
92
|
+
subscription use them up, the estimate understates; `monthly_cost` values are in the billing currency recorded as
|
|
93
|
+
`[meta] cost_currency` (the estimate then defaults to that currency, no conversion happens).
|
|
94
|
+
|
|
95
|
+
### Negotiated discounts
|
|
96
|
+
|
|
97
|
+
List prices are what the API returns. If you pay less (EA/MCA discounts, reservations), say so per resource type:
|
|
98
|
+
|
|
99
|
+
```toml
|
|
100
|
+
[discounts]
|
|
101
|
+
"Microsoft.Web/serverfarms" = 30 # percent off list
|
|
102
|
+
"Microsoft.DBforPostgreSQL/flexibleServers" = 30
|
|
103
|
+
default = 0
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Supported resource types and tiers
|
|
107
|
+
|
|
108
|
+
| Area | Types | Tiers / SKUs covered |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| 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* |
|
|
111
|
+
| Functions / Logic Apps | `serverfarms` + `sites` | Consumption (Y1), Flex (FC1: always-ready + on-demand), Elastic Premium EP1-3, Logic Apps Standard WS1-3 |
|
|
112
|
+
| 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 |
|
|
113
|
+
| 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 |
|
|
114
|
+
| 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) |
|
|
115
|
+
| 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 |
|
|
116
|
+
| 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 |
|
|
117
|
+
| Service Bus | `namespaces` | Basic, Standard (base fee + tiered operations, first 13 M included), Premium (1/2/4/8/16 messaging units) |
|
|
118
|
+
| Monitoring | `workspaces`, `components`, `scheduledQueryRules` | PerGB2018 (free 5 GB, tiers), commitment tiers, retention > 31 d. **Not** Basic/Auxiliary table plans |
|
|
119
|
+
| Key Vault | `vaults` | Standard / Premium operations. **Not** HSM keys or managed HSM |
|
|
120
|
+
| Network | `privateEndpoints`, `privateDnsZones` | per-hour + data processed; zone tiers (first 25 zones). **Not** gateways, firewalls, public IPs |
|
|
121
|
+
| AI | `CognitiveServices/accounts` | no base fee; spend via `monthly_cost` (measured by `collect`). **Not** provisioned throughput (PTU) |
|
|
122
|
+
| Free (explicitly known) | role assignments, VNets/subnets, DNS records and links, budgets, dashboards, diagnostic settings, secrets, access policies, databases/configs, blob containers, queues/topics, ... | |
|
|
123
|
+
|
|
124
|
+
Anything else is listed under *Not covered by this tool (cost NOT included)*; use `--strict` to fail in that case.
|
|
125
|
+
Premium storage, ASE stamps, bandwidth/egress and reservations are not modelled; prices are pay-as-you-go **list
|
|
126
|
+
prices** unless you add `[discounts]` or opt into account price sheets (below).
|
|
127
|
+
|
|
128
|
+
Add a resource type by writing `estimate_x(res, ctx) -> ResourceEstimate` in `bicep_cost/estimators/` and registering it
|
|
129
|
+
in `estimators/__init__.py`.
|
|
130
|
+
|
|
131
|
+
## Other commands and flags
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
bicep-cost estimate ... --format json -o head.json # also: markdown (PR comments), csv
|
|
135
|
+
bicep-cost diff base.json head.json # per-resource cost change between two reports
|
|
136
|
+
bicep-cost estimate ... --fail-above 1500 # exit 3 above a monthly budget (CI guard rail)
|
|
137
|
+
bicep-cost estimate ... --strict # exit 2 if something could not be priced
|
|
138
|
+
bicep-cost estimate ... --group-by type|module # summary table instead of per-resource rows
|
|
139
|
+
bicep-cost prices --service "Azure Container Apps" --meter-contains Idle # browse meters
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Other options: `--location` (default region and `resourceGroup().location`, default `westeurope`), `--resource-group`,
|
|
143
|
+
`--subscription-id`, `--offline` / `--refresh` / `--no-cache`, `-v` (log API calls).
|
|
144
|
+
|
|
145
|
+
## Development
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
uv sync --group test --group dev
|
|
149
|
+
uv run -m pytest
|
|
150
|
+
uv run ruff check . && uv run ty check
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Currency
|
|
154
|
+
|
|
155
|
+
Estimates use **USD** by default. Set `--currency CHF` (or `EUR`, for example) on
|
|
156
|
+
`estimate` and `prices` to request that currency directly from Azure:
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
bicep-cost estimate main.bicep --currency CHF
|
|
160
|
+
bicep-cost prices --service "Key Vault" --currency EUR
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
For estimates the precedence is `--currency`, then `[meta] cost_currency` in the
|
|
164
|
+
usage TOML, then USD. Codes are case insensitive. Reports and budget limits use
|
|
165
|
+
the selected currency; cached prices are isolated by currency. Manual
|
|
166
|
+
`monthly_cost` values must use the same currency as the estimate; conflicting
|
|
167
|
+
usage-file metadata is rejected. No exchange-rate conversion is performed.
|
|
168
|
+
Diffs require both reports to use the same currency.
|
|
169
|
+
|
|
170
|
+
Azure's non-USD retail rates are budget estimates, not negotiated billing rates.
|
|
171
|
+
See [Microsoft's Retail Prices API currency documentation](https://learn.microsoft.com/rest/api/cost-management/retail-prices/azure-retail-prices).
|
|
172
|
+
|
|
173
|
+
## Percentage cost caps in CI
|
|
174
|
+
|
|
175
|
+
Save a baseline estimate (using the same currency and usage assumptions), then
|
|
176
|
+
fail if the new monthly cost grows by more than 10 percent:
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
bicep-cost estimate main.bicep --format json -o base.json
|
|
180
|
+
# After changing the template:
|
|
181
|
+
bicep-cost estimate main.bicep --baseline base.json --fail-increase-percent 10 --fail-above 1500
|
|
182
|
+
# Or compare two saved JSON reports:
|
|
183
|
+
bicep-cost diff base.json head.json --fail-increase-percent 10 --fail-above 1500
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Either exceeded cap returns **3**. Equality and decreases pass; any positive cost
|
|
187
|
+
from a zero baseline fails the relative cap. Thresholds must be finite and
|
|
188
|
+
nonnegative. The full report is written before a budget failure, including with
|
|
189
|
+
`--output`, so CI can always retain it. `--strict` failures still take precedence
|
|
190
|
+
(exit 2). Baseline reports must use the same currency; no conversion is applied.
|
|
191
|
+
|
|
192
|
+
## Cost breakdown charts
|
|
193
|
+
|
|
194
|
+
Text and Markdown estimates include horizontal bars ordered from the most
|
|
195
|
+
expensive resource to the least. Each bar shows the monthly amount and share of
|
|
196
|
+
the known total. Text `--group-by type` or `--group-by module` aggregates both the
|
|
197
|
+
chart and table. Zero-cost resources do not get bars; unresolved and unsupported
|
|
198
|
+
resources remain listed below, since their costs are not included in the total.
|
|
199
|
+
Charts are plain text for readable CI logs and Markdown PR comments, and are
|
|
200
|
+
written even when a budget cap fails. JSON and CSV remain machine-readable.
|
|
201
|
+
|
|
202
|
+
## Automatic releases
|
|
203
|
+
|
|
204
|
+
Like [bmsui](https://github.com/bmsuisse/bmsui/blob/main/.github/workflows/release.yml),
|
|
205
|
+
main pushes run reusable CI, detect a new package version, publish to PyPI using
|
|
206
|
+
OIDC, and create a GitHub release with wheel/sdist assets. PR CI checks Ruff, ty,
|
|
207
|
+
tests, builds and an installed-wheel smoke test on Python 3.11 and 3.14.
|
|
208
|
+
|
|
209
|
+
Before the first merge, a PyPI maintainer must register a
|
|
210
|
+
[pending trusted publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/):
|
|
211
|
+
project **bicep-cost**, owner **bmsuisse**, repository **bicep-cost**, workflow
|
|
212
|
+
**release.yml**, environment **pypi**. Use the same environment in GitHub;
|
|
213
|
+
restrict its deployment branches to `main`. No long-lived PyPI token is needed.
|
|
214
|
+
|
|
215
|
+
Bump `[project] version` in `pyproject.toml`, run `uv lock`, and merge the PR.
|
|
216
|
+
Main publishes that version after CI passes; an existing GitHub release is
|
|
217
|
+
skipped. The Release workflow can also be dispatched manually from main; its
|
|
218
|
+
**dry_run** input defaults to true, validating without publishing. Release jobs
|
|
219
|
+
are serialized. If publication succeeded but release creation failed, rerun the
|
|
220
|
+
workflow: existing PyPI files are skipped and GitHub release creation is retried.
|
|
221
|
+
Do not change files for a version already uploaded to PyPI; bump the version.
|
|
222
|
+
|
|
223
|
+
## Azure account prices
|
|
224
|
+
|
|
225
|
+
Retail prices remain the default and require no login. To use negotiated
|
|
226
|
+
consumption rates through the logged-in Azure CLI user:
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
az login
|
|
230
|
+
bicep-cost estimate main.bicep --azure-prices --subscription-id YOUR_SUBSCRIPTION_ID
|
|
231
|
+
# Persist a private snapshot while estimating:
|
|
232
|
+
bicep-cost estimate main.bicep --azure-prices --save-price-sheet account-prices.json
|
|
233
|
+
# Or download separately:
|
|
234
|
+
bicep-cost price-sheet --subscription-id YOUR_SUBSCRIPTION_ID -o account-prices.json
|
|
235
|
+
# Reuse without Azure login:
|
|
236
|
+
bicep-cost estimate main.bicep --price-sheet account-prices.json
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Without a subscription argument, downloads use the CLI's current subscription.
|
|
240
|
+
The read-only [Consumption Price Sheet API](https://learn.microsoft.com/rest/api/consumption/price-sheet/get)
|
|
241
|
+
requires access to the subscription's pricing and a supported billing agreement.
|
|
242
|
+
An ordinary Azure login alone does not guarantee access. Unsupported agreements,
|
|
243
|
+
missing sheets and denied access produce an error. Within a successfully loaded
|
|
244
|
+
price sheet, missing meter rates fall back to retail prices with warnings.
|
|
245
|
+
|
|
246
|
+
For MCA/MPA accounts, or when subscription downloads are unavailable, download
|
|
247
|
+
and extract the current consumption price-sheet CSV from
|
|
248
|
+
[Cost Management + Billing](https://learn.microsoft.com/azure/cost-management-billing/manage/ea-pricing),
|
|
249
|
+
then use `--price-sheet prices.csv`. The importer accepts case-insensitive column
|
|
250
|
+
names `meterId`, optional `skuId`, `currencyCode` or `currency`, `unitOfMeasure`,
|
|
251
|
+
`unitPrice`, optional `tierMinimumUnits` and `priceType`. It uses **price currency**,
|
|
252
|
+
not `billingCurrency`; reservations and savings-plan rows are excluded. Import a
|
|
253
|
+
single current price period: conflicting duplicate rates are rejected. Legacy
|
|
254
|
+
CSV layouts must be converted to these columns first. Included-quantity grants
|
|
255
|
+
are rejected because the sheet does not supply enough billing-scope information
|
|
256
|
+
to allocate them safely.
|
|
257
|
+
|
|
258
|
+
Account mode discovers meter identities through the Retail API, then replaces
|
|
259
|
+
rates by meter/SKU ID, normalizing compatible block units (for example 100 Hours
|
|
260
|
+
to 1 Hour). Missing account meter rates use retail prices with an explicit pricing
|
|
261
|
+
warning in text, Markdown, and JSON reports; `--strict` does not block this
|
|
262
|
+
fallback. Resources with no retail meter still become unresolved. Manual `[discounts]` are ignored because account rates
|
|
263
|
+
already include discounts. Reports record the sheet's origin and snapshot time.
|
|
264
|
+
Explicit `--currency` and usage-file currency must match the sheet; by default
|
|
265
|
+
its currency is selected. No exchange-rate conversion is performed.
|
|
266
|
+
|
|
267
|
+
Snapshots contain negotiated rates, not Azure tokens, and are saved atomically
|
|
268
|
+
with owner-only permissions. Keep them private and out of source control. Saved
|
|
269
|
+
sheets can be used without login; completely offline estimates also need the
|
|
270
|
+
Retail API metadata in the normal price cache. Use `--price-sheet FILE --offline`
|
|
271
|
+
for that; `--azure-prices` always downloads fresh rates. Snapshots are not refreshed
|
|
272
|
+
automatically, so download a new sheet when your agreement or billing period changes.
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# bicep-cost
|
|
2
|
+
|
|
3
|
+
Estimate the monthly Azure cost of a Bicep (or compiled ARM) deployment from the command line, using the public
|
|
4
|
+
[Azure Retail Prices API](https://learn.microsoft.com/rest/api/cost-management/retail-prices/azure-retail-prices)
|
|
5
|
+
(the same list prices the Azure pricing calculator uses). No Azure login needed.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv tool install . # or: uvx --from . bicep-cost ...
|
|
9
|
+
|
|
10
|
+
bicep-cost estimate app/infra/main.bicep --set administratorLoginPassword=x
|
|
11
|
+
bicep-cost estimate other-app/infra/parameters/prod.bicepparam # uses the file's `using` template
|
|
12
|
+
bicep-cost estimate main.bicep -p parameters/prod.bicepparam --currency CHF --detail
|
|
13
|
+
bicep-cost estimate compiled.json -p main.parameters.json --format markdown # azd ${VAR} placeholders read from env
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
RESOURCE FIXED USAGE* TOTAL
|
|
18
|
+
serverfarms: app-plan 259.88 0.00 259.88
|
|
19
|
+
containerApps: app-api 217.28 0.00 217.28
|
|
20
|
+
flexibleServers: app-postgres 152.57 0.00 152.57
|
|
21
|
+
...
|
|
22
|
+
Provisioned (fixed): 869.11 USD
|
|
23
|
+
Usage-based* (assumed): 43.91 USD
|
|
24
|
+
TOTAL per month: 913.02 USD
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## How it works
|
|
28
|
+
|
|
29
|
+
1. `.bicep` / `.bicepparam` are compiled with the Bicep CLI (`$BICEP_CLI`, `bicep` on PATH, `~/.azure/bin/bicep`, or
|
|
30
|
+
`az bicep`). A compiled ARM `.json` skips this step.
|
|
31
|
+
2. A small ARM evaluator expands the template: parameters, variables, `condition`, `copy` loops, nested deployments
|
|
32
|
+
(Bicep modules) and their outputs. Values that only exist after deployment (`reference()` of live resources,
|
|
33
|
+
`listKeys()`, unset parameters) become *unknown* and never silently turn into guesses.
|
|
34
|
+
3. One estimator per resource type turns resources into meter quantities; prices are looked up in the Retail Prices
|
|
35
|
+
API (cached in `~/.cache/bicep-cost`, 7 days) and tiers / free grants are applied per subscription or workspace.
|
|
36
|
+
|
|
37
|
+
## Fixed vs. usage-based
|
|
38
|
+
|
|
39
|
+
Every line is either **fixed** (follows from the template: plan SKU x instances, database compute, always-on
|
|
40
|
+
replicas, private endpoint hours, alert rules, cron-scheduled replicas) or **usage-based\*** (depends on traffic or
|
|
41
|
+
data volume: storage GB and operations, log ingestion, session hours, function executions, AI tokens). Without
|
|
42
|
+
measurements, usage-based numbers come from small built-in defaults (e.g. 10 GB per storage account, 2 GB of logs) -
|
|
43
|
+
good enough to see the shape, not to quote. `--detail` lists every assumption used and the report says how many
|
|
44
|
+
figures are still defaults.
|
|
45
|
+
|
|
46
|
+
### Measure the real values from a deployed environment
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
az login
|
|
50
|
+
bicep-cost collect app/infra/main.bicep --set administratorLoginPassword=x \
|
|
51
|
+
--resource-group my-resource-group # -> main.usage.toml (next to the template)
|
|
52
|
+
bicep-cost collect other-app/infra/main.bicep -p other-app/infra/parameters/prod.bicepparam \
|
|
53
|
+
--resource-group <rg> --days 60 # -> parameters/prod.usage.toml
|
|
54
|
+
|
|
55
|
+
bicep-cost estimate app/infra/main.bicep ... # picks main.usage.toml up automatically
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`collect` is **read-only** (GETs, Azure Monitor metric reads and two read-only POST queries: Log Analytics `Usage` and
|
|
59
|
+
Cost Management `query`). It evaluates the template with the real subscription and resource group so that resource ids
|
|
60
|
+
match the deployed ones, then writes a commented usage file:
|
|
61
|
+
|
|
62
|
+
| Resource | Measured from | Usage key |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| Storage account | blob service `BlobCapacity`, `Transactions` by `ApiName` (read / write / other) | `stored_gb`, `*_operations_10k` |
|
|
65
|
+
| File share | `FileCapacity`, `Transactions` (account total split over its shares on standard accounts) | `used_gb`, `*_operations_10k` |
|
|
66
|
+
| Log Analytics | `Usage` table (billable GB) + top tables; App Insights in the same workspace is set to 0 | `ingest_gb_per_month` |
|
|
67
|
+
| Container App | `Replicas` (hourly), `UsageNanoCores`/`RxBytes`/`TxBytes` (5-min idle share), `Requests` | `replica_hours`, `idle_percent`, `requests_millions` |
|
|
68
|
+
| Session pool, AI/Foundry account | Cost Management actual cost (pool metrics do not reflect billed session-hours) | `monthly_cost` |
|
|
69
|
+
| Function app (Consumption/Flex) | `FunctionExecutionCount`, `FunctionExecutionUnits` | `executions_per_month`, `gb_seconds_per_month` |
|
|
70
|
+
| Key Vault / private endpoint / private DNS zone / Service Bus | `ServiceApiHit` / `PEBytesIn/Out` / `QueryVolume` / message counts | `operations_10k` / `*_gb` / `queries_millions` / `operations_millions` |
|
|
71
|
+
| App Service plan, PostgreSQL | live resource: instance count, `storageSizeGB` (auto-grow) | `instances`, `storage_gb` |
|
|
72
|
+
|
|
73
|
+
It also prints **drift** (template says 32 GB / 2 instances, live says 64 GB / 3) and each resource's actual cost of the
|
|
74
|
+
window as a comment, so you can sanity-check the estimate. Files are merged key by key (`--usage a.toml --usage
|
|
75
|
+
b.toml`, later wins), so hand-written overrides can live in a separate file. `bicep-cost init-usage main.bicep` writes a
|
|
76
|
+
file with every tunable value and its default instead.
|
|
77
|
+
|
|
78
|
+
Usage file lookup for `estimate`: explicit `--usage` (repeatable), else `bicep-cost.usage.toml` and
|
|
79
|
+
`<params-or-template stem>.usage.toml` next to the input. Tables match the resource name, exact or glob
|
|
80
|
+
(`[storage."stprod*"]`); see [`examples/usage.toml`](examples/usage.toml).
|
|
81
|
+
|
|
82
|
+
Caveats of the measurements: windows are scaled to a month (730 h); `idle_percent` is judged on 5-minute windows and
|
|
83
|
+
can be lower than what is billed; Container Apps free grants are subscription-wide, so if other apps in the
|
|
84
|
+
subscription use them up, the estimate understates; `monthly_cost` values are in the billing currency recorded as
|
|
85
|
+
`[meta] cost_currency` (the estimate then defaults to that currency, no conversion happens).
|
|
86
|
+
|
|
87
|
+
### Negotiated discounts
|
|
88
|
+
|
|
89
|
+
List prices are what the API returns. If you pay less (EA/MCA discounts, reservations), say so per resource type:
|
|
90
|
+
|
|
91
|
+
```toml
|
|
92
|
+
[discounts]
|
|
93
|
+
"Microsoft.Web/serverfarms" = 30 # percent off list
|
|
94
|
+
"Microsoft.DBforPostgreSQL/flexibleServers" = 30
|
|
95
|
+
default = 0
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Supported resource types and tiers
|
|
99
|
+
|
|
100
|
+
| Area | Types | Tiers / SKUs covered |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| 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* |
|
|
103
|
+
| Functions / Logic Apps | `serverfarms` + `sites` | Consumption (Y1), Flex (FC1: always-ready + on-demand), Elastic Premium EP1-3, Logic Apps Standard WS1-3 |
|
|
104
|
+
| 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 |
|
|
105
|
+
| 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 |
|
|
106
|
+
| 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) |
|
|
107
|
+
| 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 |
|
|
108
|
+
| 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 |
|
|
109
|
+
| Service Bus | `namespaces` | Basic, Standard (base fee + tiered operations, first 13 M included), Premium (1/2/4/8/16 messaging units) |
|
|
110
|
+
| Monitoring | `workspaces`, `components`, `scheduledQueryRules` | PerGB2018 (free 5 GB, tiers), commitment tiers, retention > 31 d. **Not** Basic/Auxiliary table plans |
|
|
111
|
+
| Key Vault | `vaults` | Standard / Premium operations. **Not** HSM keys or managed HSM |
|
|
112
|
+
| Network | `privateEndpoints`, `privateDnsZones` | per-hour + data processed; zone tiers (first 25 zones). **Not** gateways, firewalls, public IPs |
|
|
113
|
+
| AI | `CognitiveServices/accounts` | no base fee; spend via `monthly_cost` (measured by `collect`). **Not** provisioned throughput (PTU) |
|
|
114
|
+
| Free (explicitly known) | role assignments, VNets/subnets, DNS records and links, budgets, dashboards, diagnostic settings, secrets, access policies, databases/configs, blob containers, queues/topics, ... | |
|
|
115
|
+
|
|
116
|
+
Anything else is listed under *Not covered by this tool (cost NOT included)*; use `--strict` to fail in that case.
|
|
117
|
+
Premium storage, ASE stamps, bandwidth/egress and reservations are not modelled; prices are pay-as-you-go **list
|
|
118
|
+
prices** unless you add `[discounts]` or opt into account price sheets (below).
|
|
119
|
+
|
|
120
|
+
Add a resource type by writing `estimate_x(res, ctx) -> ResourceEstimate` in `bicep_cost/estimators/` and registering it
|
|
121
|
+
in `estimators/__init__.py`.
|
|
122
|
+
|
|
123
|
+
## Other commands and flags
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
bicep-cost estimate ... --format json -o head.json # also: markdown (PR comments), csv
|
|
127
|
+
bicep-cost diff base.json head.json # per-resource cost change between two reports
|
|
128
|
+
bicep-cost estimate ... --fail-above 1500 # exit 3 above a monthly budget (CI guard rail)
|
|
129
|
+
bicep-cost estimate ... --strict # exit 2 if something could not be priced
|
|
130
|
+
bicep-cost estimate ... --group-by type|module # summary table instead of per-resource rows
|
|
131
|
+
bicep-cost prices --service "Azure Container Apps" --meter-contains Idle # browse meters
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Other options: `--location` (default region and `resourceGroup().location`, default `westeurope`), `--resource-group`,
|
|
135
|
+
`--subscription-id`, `--offline` / `--refresh` / `--no-cache`, `-v` (log API calls).
|
|
136
|
+
|
|
137
|
+
## Development
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
uv sync --group test --group dev
|
|
141
|
+
uv run -m pytest
|
|
142
|
+
uv run ruff check . && uv run ty check
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Currency
|
|
146
|
+
|
|
147
|
+
Estimates use **USD** by default. Set `--currency CHF` (or `EUR`, for example) on
|
|
148
|
+
`estimate` and `prices` to request that currency directly from Azure:
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
bicep-cost estimate main.bicep --currency CHF
|
|
152
|
+
bicep-cost prices --service "Key Vault" --currency EUR
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
For estimates the precedence is `--currency`, then `[meta] cost_currency` in the
|
|
156
|
+
usage TOML, then USD. Codes are case insensitive. Reports and budget limits use
|
|
157
|
+
the selected currency; cached prices are isolated by currency. Manual
|
|
158
|
+
`monthly_cost` values must use the same currency as the estimate; conflicting
|
|
159
|
+
usage-file metadata is rejected. No exchange-rate conversion is performed.
|
|
160
|
+
Diffs require both reports to use the same currency.
|
|
161
|
+
|
|
162
|
+
Azure's non-USD retail rates are budget estimates, not negotiated billing rates.
|
|
163
|
+
See [Microsoft's Retail Prices API currency documentation](https://learn.microsoft.com/rest/api/cost-management/retail-prices/azure-retail-prices).
|
|
164
|
+
|
|
165
|
+
## Percentage cost caps in CI
|
|
166
|
+
|
|
167
|
+
Save a baseline estimate (using the same currency and usage assumptions), then
|
|
168
|
+
fail if the new monthly cost grows by more than 10 percent:
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
bicep-cost estimate main.bicep --format json -o base.json
|
|
172
|
+
# After changing the template:
|
|
173
|
+
bicep-cost estimate main.bicep --baseline base.json --fail-increase-percent 10 --fail-above 1500
|
|
174
|
+
# Or compare two saved JSON reports:
|
|
175
|
+
bicep-cost diff base.json head.json --fail-increase-percent 10 --fail-above 1500
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Either exceeded cap returns **3**. Equality and decreases pass; any positive cost
|
|
179
|
+
from a zero baseline fails the relative cap. Thresholds must be finite and
|
|
180
|
+
nonnegative. The full report is written before a budget failure, including with
|
|
181
|
+
`--output`, so CI can always retain it. `--strict` failures still take precedence
|
|
182
|
+
(exit 2). Baseline reports must use the same currency; no conversion is applied.
|
|
183
|
+
|
|
184
|
+
## Cost breakdown charts
|
|
185
|
+
|
|
186
|
+
Text and Markdown estimates include horizontal bars ordered from the most
|
|
187
|
+
expensive resource to the least. Each bar shows the monthly amount and share of
|
|
188
|
+
the known total. Text `--group-by type` or `--group-by module` aggregates both the
|
|
189
|
+
chart and table. Zero-cost resources do not get bars; unresolved and unsupported
|
|
190
|
+
resources remain listed below, since their costs are not included in the total.
|
|
191
|
+
Charts are plain text for readable CI logs and Markdown PR comments, and are
|
|
192
|
+
written even when a budget cap fails. JSON and CSV remain machine-readable.
|
|
193
|
+
|
|
194
|
+
## Automatic releases
|
|
195
|
+
|
|
196
|
+
Like [bmsui](https://github.com/bmsuisse/bmsui/blob/main/.github/workflows/release.yml),
|
|
197
|
+
main pushes run reusable CI, detect a new package version, publish to PyPI using
|
|
198
|
+
OIDC, and create a GitHub release with wheel/sdist assets. PR CI checks Ruff, ty,
|
|
199
|
+
tests, builds and an installed-wheel smoke test on Python 3.11 and 3.14.
|
|
200
|
+
|
|
201
|
+
Before the first merge, a PyPI maintainer must register a
|
|
202
|
+
[pending trusted publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/):
|
|
203
|
+
project **bicep-cost**, owner **bmsuisse**, repository **bicep-cost**, workflow
|
|
204
|
+
**release.yml**, environment **pypi**. Use the same environment in GitHub;
|
|
205
|
+
restrict its deployment branches to `main`. No long-lived PyPI token is needed.
|
|
206
|
+
|
|
207
|
+
Bump `[project] version` in `pyproject.toml`, run `uv lock`, and merge the PR.
|
|
208
|
+
Main publishes that version after CI passes; an existing GitHub release is
|
|
209
|
+
skipped. The Release workflow can also be dispatched manually from main; its
|
|
210
|
+
**dry_run** input defaults to true, validating without publishing. Release jobs
|
|
211
|
+
are serialized. If publication succeeded but release creation failed, rerun the
|
|
212
|
+
workflow: existing PyPI files are skipped and GitHub release creation is retried.
|
|
213
|
+
Do not change files for a version already uploaded to PyPI; bump the version.
|
|
214
|
+
|
|
215
|
+
## Azure account prices
|
|
216
|
+
|
|
217
|
+
Retail prices remain the default and require no login. To use negotiated
|
|
218
|
+
consumption rates through the logged-in Azure CLI user:
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
az login
|
|
222
|
+
bicep-cost estimate main.bicep --azure-prices --subscription-id YOUR_SUBSCRIPTION_ID
|
|
223
|
+
# Persist a private snapshot while estimating:
|
|
224
|
+
bicep-cost estimate main.bicep --azure-prices --save-price-sheet account-prices.json
|
|
225
|
+
# Or download separately:
|
|
226
|
+
bicep-cost price-sheet --subscription-id YOUR_SUBSCRIPTION_ID -o account-prices.json
|
|
227
|
+
# Reuse without Azure login:
|
|
228
|
+
bicep-cost estimate main.bicep --price-sheet account-prices.json
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Without a subscription argument, downloads use the CLI's current subscription.
|
|
232
|
+
The read-only [Consumption Price Sheet API](https://learn.microsoft.com/rest/api/consumption/price-sheet/get)
|
|
233
|
+
requires access to the subscription's pricing and a supported billing agreement.
|
|
234
|
+
An ordinary Azure login alone does not guarantee access. Unsupported agreements,
|
|
235
|
+
missing sheets and denied access produce an error. Within a successfully loaded
|
|
236
|
+
price sheet, missing meter rates fall back to retail prices with warnings.
|
|
237
|
+
|
|
238
|
+
For MCA/MPA accounts, or when subscription downloads are unavailable, download
|
|
239
|
+
and extract the current consumption price-sheet CSV from
|
|
240
|
+
[Cost Management + Billing](https://learn.microsoft.com/azure/cost-management-billing/manage/ea-pricing),
|
|
241
|
+
then use `--price-sheet prices.csv`. The importer accepts case-insensitive column
|
|
242
|
+
names `meterId`, optional `skuId`, `currencyCode` or `currency`, `unitOfMeasure`,
|
|
243
|
+
`unitPrice`, optional `tierMinimumUnits` and `priceType`. It uses **price currency**,
|
|
244
|
+
not `billingCurrency`; reservations and savings-plan rows are excluded. Import a
|
|
245
|
+
single current price period: conflicting duplicate rates are rejected. Legacy
|
|
246
|
+
CSV layouts must be converted to these columns first. Included-quantity grants
|
|
247
|
+
are rejected because the sheet does not supply enough billing-scope information
|
|
248
|
+
to allocate them safely.
|
|
249
|
+
|
|
250
|
+
Account mode discovers meter identities through the Retail API, then replaces
|
|
251
|
+
rates by meter/SKU ID, normalizing compatible block units (for example 100 Hours
|
|
252
|
+
to 1 Hour). Missing account meter rates use retail prices with an explicit pricing
|
|
253
|
+
warning in text, Markdown, and JSON reports; `--strict` does not block this
|
|
254
|
+
fallback. Resources with no retail meter still become unresolved. Manual `[discounts]` are ignored because account rates
|
|
255
|
+
already include discounts. Reports record the sheet's origin and snapshot time.
|
|
256
|
+
Explicit `--currency` and usage-file currency must match the sheet; by default
|
|
257
|
+
its currency is selected. No exchange-rate conversion is performed.
|
|
258
|
+
|
|
259
|
+
Snapshots contain negotiated rates, not Azure tokens, and are saved atomically
|
|
260
|
+
with owner-only permissions. Keep them private and out of source control. Saved
|
|
261
|
+
sheets can be used without login; completely offline estimates also need the
|
|
262
|
+
Retail API metadata in the normal price cache. Use `--price-sheet FILE --offline`
|
|
263
|
+
for that; `--azure-prices` always downloads fresh rates. Snapshots are not refreshed
|
|
264
|
+
automatically, so download a new sheet when your agreement or billing period changes.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Estimate Azure costs from Bicep / ARM templates."""
|