Cost Management Guide¶
Note
Quick Summary: FinOps practices for CSA-in-a-Box covering cost estimation (Bicep + Terraform paths), budget guardrails per environment, CI/CD cost comments, required tagging strategy, optimization tips (reserved instances, auto-pause, storage tiering, right-sizing), and a FinOps maturity roadmap (Crawl → Walk → Run).
This document covers cost estimation, budget guardrails, and FinOps practices for the CSA-in-a-Box platform.
📑 Table of Contents¶
- ⚖️ Cost Shape: CSA/Loom à-la-carte vs. Fabric capacity
- 💰 Cost Estimation Approach
- Bicep Path (Primary)
- Terraform Path (Future)
- 🔧 Running Cost Estimates Locally
- 🔄 CI/CD Integration
- 🚨 Budget Thresholds and Alerts
- 🏷️ Tagging Strategy
- 📉 Cost Optimization Tips
- 📊 FinOps Maturity Model
- 📋 Resource-Specific Pricing Reference
- 📚 Further Reading
⚖️ Cost Shape: CSA/Loom à-la-carte vs. Fabric capacity¶
Before estimating individual resources, understand the cost shape you are signing up for. CSA-in-a-Box / CSA Loom and Microsoft Fabric bill in fundamentally different ways, and the right choice depends on your workload profile, not on a single sticker price. I am not going to quote dollar figures here — list prices move, and EA/CSP/Gov discounts make any headline number misleading. What matters is the shape of the bill and the levers you control.
Two different billing models¶
| CSA / Loom (this platform) | Microsoft Fabric (F SKU) | |
|---|---|---|
| Billing model | À-la-carte Azure consumption — you pay each service's own meter (Databricks DBU/hour, Synapse Serverless per-TB-scanned, ADX cluster-hour, Container Apps vCPU-s + GiB-s, ADLS Gen2 per-GB + per-transaction, Event Hubs TU/hour, Cosmos RU + storage). | A single pooled Capacity Unit (CU) meter. One F SKU (F2…F2048) backs every workload — Lakehouse, Warehouse, pipelines, Spark, KQL, Power BI — drawing from the same CU pool. Storage (OneLake) and networking bill separately. (Learn) |
| Granularity | Per-service, per-meter. You see exactly which service drives the bill and can tune each independently. | Per-capacity. Individual items (a Warehouse, a Lakehouse) cannot be paused alone — only the whole capacity pauses. (Learn) |
| Idle cost | Pay only for what runs. Serverless/consumption services (Synapse Serverless, Functions, Container Apps scale-to-zero, Cosmos serverless) cost ~nothing when idle; always-on services (a running Databricks cluster, an ADX cluster, provisioned Cosmos) bill continuously until paused/stopped. | The F SKU bills per second (1-minute minimum) whenever the capacity is resumed, regardless of whether anyone is running a query. Cost control = explicitly pause the capacity when idle. (Learn) |
| Commitment discount | Reserved Instances / savings plans per service (Databricks DBU pre-purchase, Cosmos/ADX reserved capacity, VM RIs for SHIR). Each negotiated independently. | A single Fabric capacity reservation (1-year or 3-year) discounts the CU meter. It does not cover OneLake storage or networking. (Learn) |
| Scaling | Scale each service independently (cluster size, DWU, TU, RU/s, replica count). | Scale the whole capacity up/down (SKU change), or layer pay-as-you-go on top of a reserved F SKU for predictable peaks. (Learn) |
When each is cheaper¶
Fabric (fixed capacity) tends to win when:
- You have many concurrent workloads with bursty but continuous usage that keep a capacity busy through the day. A fixed CU pool with Fabric's background-operation smoothing (Spark/SQL spread over ~24h) absorbs spikes without you provisioning each engine for peak.
- You want one bill, one number to reserve, one thing to size — the operational simplicity of capacity sizing beats tuning a dozen meters.
- Your team will actually pause the capacity off-hours, or runs it hot enough that a 1- or 3-year reservation pays off (the reservation breaks even only if the capacity is genuinely consumed).
- You add users freely — Fabric does not charge per-user for capacity consumption, so wide read/consume audiences are cheap.
CSA / Loom (à-la-carte) tends to win when:
- Your usage is spiky and intermittent — a nightly batch, an occasional ad-hoc query, a streaming pipeline that idles. Serverless/consumption meters (Synapse Serverless per-TB, Functions, Container Apps scale-to-zero, Cosmos serverless) charge near-zero between runs, where a resumed F SKU bills continuously.
- One engine dominates your cost and you want to optimize only it — e.g. a Databricks-heavy shop buying DBU commitments, or an ADLS-archive-heavy workload riding storage lifecycle tiering — without paying for a capacity that bundles engines you barely touch.
- You need Azure Government / sovereign / dedicated-compute placement where Fabric is not GA, so the comparison is moot: à-la-carte is the only option.
- You want per-service granularity for chargeback — attributing exact cost to a pipeline, a query, or a domain is native to per-meter billing and harder to disentangle from a shared CU pool.
The honest caveats¶
- No clean break-even line. Whether Fabric or à-la-carte is cheaper depends on concurrency, idle ratio, reservation discipline, and your discount agreements. The only reliable method is to measure: provision a trial/pay-as-you-go F SKU and watch the Fabric Capacity Metrics app against your real workload, and run the
estimate-costs.shà-la-carte estimate in parallel. (Learn) - À-la-carte is more knobs, more responsibility. The flexibility that makes CSA/Loom cheaper for spiky workloads is also the thing that lets idle always-on resources (a forgotten Databricks cluster, an un-paused ADX cluster) quietly burn money. The optimization levers in Cost Optimization Tips below — auto-pause, auto-stop, scale-to-zero, storage tiering, RIs — are mandatory hygiene, not nice-to-haves. A Fabric capacity has exactly one knob (pause); CSA/Loom has a dozen, and all of them have to be set.
- Fabric's single-pause granularity cuts both ways. You cannot pause one noisy Warehouse without pausing every workload on that capacity, so a single hot item forces the whole capacity to stay resumed. À-la-carte lets you stop exactly the one service.
- Reservations don't cover everything. A Fabric reservation discounts only CU usage — OneLake storage and networking stay pay-as-you-go. À-la-carte RIs are per-service and equally partial (DBU commit ≠ storage discount). Model storage and egress separately in both worlds.
- Migration cost is real but one-directional. CSA/Loom is designed as an on-ramp: components compose into a future Fabric migration, so starting à-la-carte and moving to a capacity later is a supported path. Going the other way (Fabric → self-operated PaaS) is the harder lift. Factor the on-ramp value into the comparison, not just this month's bill.
Bottom line: Fabric trades flexibility for simplicity — one capacity, one bill, one pause button, best for sustained multi-workload usage by a team that will size and pause it well. CSA/Loom trades simplicity for control — per-service meters that idle cheaply and tune precisely, best for spiky/intermittent workloads, single-engine-dominant cost shapes, sovereign/Gov placement, and granular chargeback — at the price of having many more cost levers you are obligated to manage.
💰 Cost Estimation Approach¶
CSA-in-a-Box supports two IaC paths, each with its own cost estimation strategy.
First, understand the cost shape
Before estimating individual resources, read Cost Shape: CSA/Loom à-la-carte vs. Fabric capacity (above) — it explains how this platform's per-service consumption billing differs from a fixed Fabric F-SKU capacity, when each is cheaper, and the levers you control.
Bicep Path (Primary)¶
The primary deployment path uses Azure Bicep templates under deploy/bicep/. Because Infracost does not natively support Bicep, we provide a custom script that:
- Compiles Bicep to ARM JSON using
az bicep build --stdout - Extracts resource types and counts with
jq - Queries the Azure Retail Prices API for each resource type
- Produces a formatted cost estimate with budget comparison
Script: scripts/deploy/estimate-costs.sh
Important
Bicep estimates are best-effort. The Azure Retail Prices API returns list prices — actual costs depend on EA/CSP agreements, reserved instances, and consumption-based meters.
Terraform Path (Roadmap — not implemented)¶
Warning
A parallel Terraform deployment is on the roadmap, not available today (CSA-0015 / audit approval queue item AQ-0024). deploy/terraform/ does not exist in the repository. The .infracost/terraform.yml configuration is a scaffold that will activate once Terraform modules ship. Until then, use the Bicep path (above) for all cost analyses.
🔧 Running Cost Estimates Locally¶
Prerequisites¶
- Azure CLI with Bicep extension (
az bicep install) jq(JSON processor)curl(HTTP client)bc(calculator, usually pre-installed)
Basic Usage¶
# Estimate costs for the DLZ
./scripts/deploy/estimate-costs.sh deploy/bicep/DLZ/main.bicep
# With parameters file
./scripts/deploy/estimate-costs.sh deploy/bicep/DLZ/main.bicep \
--params deploy/bicep/DLZ/params.dev.json
# JSON output for scripting
./scripts/deploy/estimate-costs.sh deploy/bicep/DLZ/main.bicep \
--format json
# Compare against a budget
./scripts/deploy/estimate-costs.sh deploy/bicep/DLZ/main.bicep \
--budget 5000
# Specify environment (loads budget from .infracost/policy.yml)
./scripts/deploy/estimate-costs.sh deploy/bicep/DLZ/main.bicep \
--environment dev
# Different region and currency
./scripts/deploy/estimate-costs.sh deploy/bicep/DLZ/main.bicep \
--region westus2 --currency EUR
Understanding the Output¶
The table output shows:
| Column | Description |
|---|---|
| Resource Type | ARM resource type (e.g., Storage/storageAccounts) |
| SKU | Pricing SKU returned by the API |
| Unit $ | Per-unit retail price |
| Qty | Number of instances in the template |
| Monthly $ | Estimated monthly cost (unit price × hours/month × qty) |
Resources without a pricing mapping or that cannot be found in the API are flagged as warnings.
Exit Codes¶
| Code | Meaning |
|---|---|
| 0 | Success, within budget (or no budget set) |
| 1 | Script error (missing file, failed build) |
| 2 | Over budget (table mode with --budget) |
🔄 CI/CD Integration¶
GitHub Actions — Cost Estimate Job¶
The bicep-whatif.yml workflow includes a cost-estimate job that runs after the what-if analysis on every PR that modifies Bicep files. It:
- Compiles each changed landing zone's Bicep to ARM JSON
- Runs
estimate-costs.shin JSON mode - Posts a cost summary as a PR comment
The cost estimate job uses the environment-specific budget from .infracost/policy.yml. If the estimate exceeds the budget, the job logs a warning but does not block the PR (to avoid false-positive rejections from list-price estimates).
Adding Cost Estimates to Other Workflows¶
- name: Run Cost Estimate
run: |
chmod +x scripts/deploy/estimate-costs.sh
./scripts/deploy/estimate-costs.sh deploy/bicep/DLZ/main.bicep \
--format json \
--environment ${{ vars.ENVIRONMENT || 'dev' }} \
--budget ${{ vars.COST_BUDGET || '5000' }}
Infracost (Terraform Path)¶
When the Terraform modules are available:
- name: Setup Infracost
uses: infracost/actions/setup@v3
with:
api-key: ${{ secrets.INFRACOST_API_KEY }}
- name: Run Infracost
run: |
infracost breakdown \
--config-file .infracost/terraform.yml \
--format json \
--out-file /tmp/infracost.json
- name: Post Infracost Comment
uses: infracost/actions/comment@v1
with:
path: /tmp/infracost.json
behavior: update
🚨 Budget Thresholds and Alerts¶
Budget thresholds are defined in .infracost/policy.yml:
| Environment | Monthly Budget | Alert Threshold |
|---|---|---|
| dev | $5,000 | 80% |
| staging | $10,000 | 80% |
| prod | $25,000 | 75% |
Policy Rules¶
The policy file also enforces cost guardrails:
| Rule | Environment | Action | Description |
|---|---|---|---|
no-premium-in-dev | dev | warn | Flag Premium SKUs in dev |
no-multi-region-dev | dev | warn | Flag RA-GZRS storage in dev |
enforce-serverless-dev | dev | warn | Prefer serverless Cosmos DB in dev |
max-databricks-nodes-dev | dev | deny | Limit Databricks cluster nodes to 4 in dev |
max-adx-sku-dev | dev | warn | Require Dev/Test ADX SKU in dev |
Azure Cost Management Alerts¶
In addition to pre-deployment estimates, configure Azure Cost Management alerts for runtime monitoring:
# Create a budget in Azure Cost Management
az consumption budget create \
--budget-name "csa-dev-monthly" \
--amount 5000 \
--category cost \
--time-grain Monthly \
--start-date "2024-01-01" \
--end-date "2025-12-31" \
--resource-group "rg-dlz-dev-*" \
--notifications '[{
"enabled": true,
"operator": "GreaterThanOrEqualTo",
"threshold": 80,
"contactEmails": ["platform-team@contoso.com"],
"thresholdType": "Actual"
}]'
🏷️ Tagging Strategy¶
All CSA-in-a-Box resources must include cost-attribution tags. These are enforced in the Bicep templates via the tagsDefault variable in each landing zone's main.bicep.
Required Tags¶
| Tag | Purpose | Example Values |
|---|---|---|
environment | Deployment environment | dev, staging, prod |
CostCenter | Billing/chargeback code | CSA-Platform, DataEng |
Owner | Team or project owner | Platform Team |
Project | Project name | Azure Demo ALZ & CSA |
PrimaryContact | Technical contact email | platform-team@contoso.com |
Toolkit | IaC tool used | Bicep, Terraform |
Enforcement¶
Tags are defined in the tagsDefault variable in each landing zone and merged with resource-specific tags:
var tagsDefault = {
Owner: 'Azure Landing Zone & Cloud Scale Analytics Scenario'
Project: 'Azure Demo ALZ & CSA'
environment: environment
Toolkit: 'Bicep'
PrimaryContact: primaryContact
CostCenter: costCenter
}
Azure Policy can further enforce tagging at the subscription or management group level:
{
"if": {
"field": "[concat('tags[', 'CostCenter', ']')]",
"exists": "false"
},
"then": {
"effect": "deny"
}
}
📉 Cost Optimization Tips¶
Reserved Instances & Savings Plans¶
| Service | Savings Opportunity |
|---|---|
| Databricks | Pre-purchase DBU commit (1-year: ~25%, 3-year: ~40%) |
| Cosmos DB | Reserved capacity for provisioned throughput |
| Data Explorer | Reserved capacity for cluster compute |
| VMs (SHIR) | Reserved Instances for always-on Integration Runtime |
Auto-Pause and Auto-Stop¶
// Synapse SQL Pools — auto-pause after 60 minutes of inactivity
autopauseDelayInMinutes: 60
// ADX Dev clusters — auto-stop enabled
enableAutoStop: true
In dev/staging, always enable auto-pause for:
- Synapse dedicated SQL pools
- Databricks clusters (via cluster policies)
- Data Explorer clusters (Dev SKU auto-stop)
Spot VMs and Low-Priority Compute¶
- Use Spot VMs for Databricks worker nodes in dev/test
- Use Low-Priority nodes for Synapse Spark pools in dev/test
- Typical savings: 60-90% over pay-as-you-go
Storage Tiering¶
| Tier | Use Case | Relative Cost |
|---|---|---|
| Hot | Frequently accessed data | 1.0x |
| Cool | Infrequent access (30+ days) | ~0.5x |
| Archive | Rarely accessed (180+ days) | ~0.1x |
Implement lifecycle management policies for each lake zone:
{
"rules": [
{
"name": "archive-old-data",
"type": "Lifecycle",
"definition": {
"actions": {
"baseBlob": {
"tierToCool": {
"daysAfterModificationGreaterThan": 30
},
"tierToArchive": {
"daysAfterModificationGreaterThan": 180
}
}
},
"filters": {
"blobTypes": ["blockBlob"],
"prefixMatch": ["raw/", "enriched/"]
}
}
}
]
}
Right-Sizing¶
- Event Hubs: Start with Standard tier (1 TU); scale up only when throughput exceeds 1 MB/s ingress
- Stream Analytics: Start with 3 SUs; monitor SU% utilization and adjust
- Functions: Use Consumption plan in dev; switch to Premium only for VNet integration or sustained load
- Data Explorer: Use
Dev(No SLA)_Standard_E2a_v4for dev; move toStandard_E8ads_v5for production
📊 FinOps Maturity Model¶
Stage 1: Crawl (Current)¶
- Pre-deployment cost estimation via
estimate-costs.sh - Budget thresholds in
.infracost/policy.yml - Required cost-attribution tags on all resources
- PR-level cost impact comments
- Azure Cost Management budgets and alerts
Stage 2: Walk¶
- Terraform path with native Infracost support
- Historical cost tracking (Infracost Cloud or Azure Cost Export)
- Automated anomaly detection (Azure Cost Alerts)
- Monthly cost review cadence with team
- Showback reports by
CostCentertag
Stage 3: Run¶
- Chargeback model across domains/teams
- Reserved instance and savings plan optimization
- Automated right-sizing recommendations
- Cost-per-pipeline / cost-per-query attribution
- Integration with organizational FinOps tooling
📋 Resource-Specific Pricing Reference¶
Quick reference for the CSA services tracked by estimate-costs.sh:
| Service | ARM Type | Default SKU (Dev) | Pricing Model |
|---|---|---|---|
| Storage Account | Microsoft.Storage/storageAccounts | Standard_LRS | Per GB stored + ops |
| Event Hubs | Microsoft.EventHub/namespaces | Standard (1 TU) | Per TU/hour + events |
| Data Factory | Microsoft.DataFactory/factories | N/A (pay-per-pipeline) | Per activity run |
| Databricks | Microsoft.Databricks/workspaces | Premium | Per DBU/hour |
| Data Explorer | Microsoft.Kusto/clusters | Dev(No SLA)_Standard_E2a_v4 | Per cluster/hour |
| Key Vault | Microsoft.KeyVault/vaults | Standard | Per operation |
| Cosmos DB | Microsoft.DocumentDB/databaseAccounts | Serverless | Per RU + storage |
| Azure Functions | Microsoft.Web/sites | Consumption / EP1 | Per execution + GB-s |
| Stream Analytics | Microsoft.StreamAnalytics/streamingjobs | Standard (3 SU) | Per SU/hour |
| Log Analytics | Microsoft.OperationalInsights/workspaces | Per GB | Per GB ingested |
| Machine Learning | Microsoft.MachineLearningServices/workspaces | Basic | Per compute/hour |
| Synapse Analytics | Microsoft.Synapse/workspaces | Serverless SQL Pool | Per TB processed |
📚 Further Reading¶
- Azure Pricing Calculator
- Azure Retail Prices API
- Infracost Documentation
- FinOps Foundation
- Azure Cost Management Best Practices
See also:
- ← Previous: Documentation home
- → Next: Rollback
- ⌂ Index: Documentation home