Update (October 26, 2026): Microsoft has notified customers that prices for affected VM and Storage services rise by 8% to 17% in seven Azure regions, six of them in Europe, from February 1, 2027. Read the update.
Microsoft Azure provides a broad set of native tools for enterprise FinOps. This guide walks step by step through building a FinOps practice with Azure-native services and Microsoft’s open-source FinOps toolkit, following the FinOps Foundation’s Inform, Optimize and Operate phases.
Bottom line: for most Azure-centric organizations, native tools cover the core FinOps capabilities without extra licensing and integrate deeply with Azure Resource Manager, Azure Policy and Microsoft Entra ID. Third-party platforms earn their place mainly for multi-cloud or SaaS needs.
Azure’s Native FinOps Tools
Core Cost Management Tools
- Microsoft Cost Management: cost analysis, budgets, alerts, cost allocation, tag inheritance and exports, in the Cost Management + Billing experience and at every scope
- Azure Advisor: cost recommendations for idle and underused resources, reservations and savings plans
- Azure Monitor: utilization metrics to validate rightsizing
- Azure Resource Graph: inventory queries across subscriptions, including Advisor data
- Microsoft FinOps toolkit: open-source FinOps hubs, Power BI reports, workbooks, Azure Optimization Engine, PowerShell and Bicep modules
- Azure Copilot: natural-language cost questions and forecasts
Optimization and Rightsizing Tools
- Azure reservations: one- or three-year commitments for stable workloads
- Savings plans for compute and databases: hourly spend commitments across eligible services and regions
- Azure Hybrid Benefit: reuse eligible Windows Server and SQL Server licenses
- Azure Spot Virtual Machines: deep discounts for interruptible workloads
Governance and Automation Tools
- Azure Policy: preventive guardrails and tag governance
- Bicep and ARM templates: infrastructure as code; use the pricing calculator or Retail Prices API for estimates
- Azure Automation and Azure Functions: scheduled cost tasks, including Start/Stop VMs v2
- Azure Logic Apps: approval and notification workflows
- Management groups: hierarchy for policy inheritance and cost visibility
Phase 1: Foundation Setup – Building Cost Visibility
Step 1: Configure Cost Management Exports
Cost Management collects data automatically, so the key setup step is exports. Choose the FOCUS dataset, which combines actual and amortized costs, and deliver it to Azure Data Lake Storage:
- Use Parquet with Snappy compression, partitioning and overwrite
- Export at billing account or billing profile scope; FOCUS isn’t supported at management group scope or for pay-as-you-go (MOSP) billing scopes
- Add the price sheet and reservation datasets for rate analysis
- Backfill up to 13 months in the portal, or up to 7 years through the REST API
- Storage behind a firewall needs a managed identity, which requires permission to assign roles on the account
The FinOps toolkit PowerShell module creates a FOCUS export in one command; it defaults to the FocusCost dataset and the 1.2-preview schema:
Install-Module -Name FinOpsToolkit
Connect-AzAccount
$scope = "/providers/Microsoft.Billing/billingAccounts/<billing-account-id>/billingProfiles/<billing-profile-id>"
$storageId = "/subscriptions/<subscription-id>/resourceGroups/rg-finops/providers/Microsoft.Storage/storageAccounts/<storage-account>"
New-FinOpsCostExport -Name "focus-daily" `
-Scope $scope `
-Dataset FocusCost `
-StorageAccountId $storageId `
-StorageContainer "msexports" `
-Backfill 12 `
-Execute
Rather than writing your own processing functions, deploy FinOps hubs. They ingest exports into Azure Data Explorer or Microsoft Fabric Real-Time Intelligence, normalize them to FOCUS, feed the toolkit’s reports, and now also ingest Advisor and custom Resource Graph recommendations.
Step 2: Implement a Tagging Strategy
Each resource, resource group and subscription can have up to 50 tag name-value pairs, though a few resource types, such as Automation accounts, support only 15. Tag names are case-insensitive for operations but values are case-sensitive, so standardize both.
An example of an Azure tag schema:
{
"Environment": "production|staging|development|sandbox",
"Project": "project-name",
"Owner": "team-or-individual-email",
"CostCenter": "finance-cost-center-code",
"BusinessUnit": "marketing|engineering|sales|operations",
"Application": "application-name",
"Workload": "web-tier|app-tier|data-tier|analytics",
"Schedule": "24x7|business-hours|weekend-only|on-demand",
"AutoShutdown": "enabled|disabled",
"BackupPolicy": "daily|weekly|monthly|none"
}
Require tags with Azure Policy. Start in audit mode, then switch to deny once teams have adapted:
{
"properties": {
"displayName": "Require cost allocation tags",
"policyType": "Custom",
"mode": "Indexed",
"description": "Requires the tags used for cost allocation.",
"metadata": { "category": "Tags" },
"parameters": {
"effect": {
"type": "String",
"allowedValues": ["Audit", "Deny", "Disabled"],
"defaultValue": "Audit"
}
},
"policyRule": {
"if": {
"anyOf": [
{ "field": "tags['Environment']", "exists": "false" },
{ "field": "tags['Project']", "exists": "false" },
{ "field": "tags['Owner']", "exists": "false" },
{ "field": "tags['CostCenter']", "exists": "false" }
]
},
"then": { "effect": "[parameters('effect')]" }
}
}
}
Inherit tags instead of scripting them. The built-in policy “Inherit a tag from the resource group if missing” adds a resource group’s tag to new or updated resources, and a remediation task fixes existing ones. Also enable tag inheritance in Cost Management so subscription and resource group tags apply to cost records.
Step 3: Configure Budgets and Cost Alerts
Budgets work at every scope, and at subscription and resource group scope they can notify an action group to trigger automation. A cost center budget in Bicep:
targetScope = 'subscription'
param budgetName string
param monthlyAmount int
param startDate string = '2026-11-01' // first day of a month
param costCenter string
param contactEmails array
param actionGroupId string
resource budget 'Microsoft.Consumption/budgets@2024-08-01' = {
name: budgetName
properties: {
category: 'Cost'
amount: monthlyAmount
timeGrain: 'Monthly'
timePeriod: {
startDate: startDate
}
filter: {
tags: {
name: 'CostCenter'
operator: 'In'
values: [
costCenter
]
}
}
notifications: {
Actual_GreaterThan_80_Percent: {
enabled: true
operator: 'GreaterThan'
threshold: 80
thresholdType: 'Actual'
contactEmails: contactEmails
}
Forecasted_GreaterThan_100_Percent: {
enabled: true
operator: 'GreaterThan'
threshold: 100
thresholdType: 'Forecasted'
contactEmails: contactEmails
contactGroups: [
actionGroupId
]
}
}
}
}
Deploy it once per department or cost center. Add anomaly alerts too; detection runs daily per subscription, with up to five alert rules each. The FinOps toolkit’s Bicep Registry module creates one:
module anomalyAlert 'br/public:cost/subscription-scheduled-action:1.0.2' = {
name: 'anomalyAlert'
params: {
name: 'AnomalyAlert'
kind: 'InsightAlert'
displayName: 'Daily cost anomaly check'
emailRecipients: [ '[email protected]' ]
notificationEmail: '[email protected]'
}
}
Scheduled alerts email a saved cost view to stakeholders daily, weekly or monthly.
Phase 2: Deep Cost Analysis With Cost Management
Cost Analysis Views and Queries
Use Cost analysis views for interactive work and exports for anything at scale. With FinOps hubs on Data Explorer or Fabric, you can query FOCUS data with KQL, for example last month’s effective cost by cost center and service:
Costs()
| where ChargePeriodStart >= startofmonth(now(), -1) and ChargePeriodStart < startofmonth(now())
| extend CostCenter = tostring(Tags['CostCenter'])
| summarize EffectiveCost = sum(EffectiveCost) by CostCenter, ServiceName
| order by EffectiveCost desc
There’s no need to build your own anomaly detection; the built-in anomaly alerts cover the common cases.
Azure Resource Graph Queries for Cost Optimization
Resource Graph finds common waste across all subscriptions. Stopped but not deallocated VMs, which still incur compute charges:
Resources
| where type =~ 'microsoft.compute/virtualmachines'
| extend powerState = tostring(properties.extended.instanceView.powerState.code)
| where powerState =~ 'PowerState/stopped'
| project name, resourceGroup, subscriptionId, location, vmSize = tostring(properties.hardwareProfile.vmSize)
Unattached managed disks:
Resources
| where type =~ 'microsoft.compute/disks'
| where tostring(properties.diskState) =~ 'Unattached'
| project name, resourceGroup, subscriptionId, location, sku = tostring(sku.name), sizeGB = toint(properties.diskSizeGB)
| order by sizeGB desc
Public IP addresses not associated with any resource:
Resources
| where type =~ 'microsoft.network/publicipaddresses'
| where isempty(properties.ipConfiguration) and isempty(properties.natGateway)
| project name, resourceGroup, subscriptionId, location, sku = tostring(sku.name)
For storage, enable blob last access time tracking and use lifecycle management rules to move cold data to cooler tiers.
Phase 3: Resource Optimization With Native Tools
Azure Advisor
Query Advisor data in Resource Graph rather than parsing recommendation text. Potential savings by recommendation:
AdvisorResources
| where type == 'microsoft.advisor/recommendations'
| where properties.category == 'Cost'
| extend resources = tostring(properties.resourceMetadata.resourceId),
savings = todouble(properties.extendedProperties.savingsAmount),
solution = tostring(properties.shortDescription.solution),
currency = tostring(properties.extendedProperties.savingsCurrency)
| summarize dcount(resources), bin(sum(savings), 0.01) by solution, currency
| project solution, dcount_resources, sum_savings, currency
| order by sum_savings desc
Two caveats: rightsizing savings are estimated at retail rates, while commitment savings use your actual rates, and the recommendations overlap. Act in order (rightsize or shut down, then reservations, then savings plans), let recommendations refresh between steps, and route actions to owners through Logic Apps or your ITSM tool rather than applying them automatically.
Reservations, Savings Plans and Azure Hybrid Benefit
Choose the right commitment. Reservations give the greatest savings for stable workloads with a fixed VM family and region; savings plans suit dynamic workloads because the hourly commitment applies across eligible services and regions. Recommendations are in Advisor, the purchase experience, the Benefit Recommendations API and the reservation recommendations export.
Plan for policy changes. Reservations purchased after February 1, 2027 for services covered by savings plans won’t be exchangeable; earlier ones can be exchanged one final time after that date. Trade-ins to savings plans remain available, and cancellations stay limited to 50,000 USD in a rolling 12-month window per billing profile or enrollment.
Apply Azure Hybrid Benefit only with valid licenses: active Software Assurance or qualifying subscription licenses, with at least 8 core licenses per VM. Changing the license type doesn’t restart the VM. This script lists Windows VMs without the benefit and applies it only with -Apply, after your license team confirms entitlement:
param(
[switch]$Apply
)
# Windows Server VMs without the benefit (excludes Windows client multitenant hosting)
$candidates = Get-AzVM | Where-Object {
$_.StorageProfile.OsDisk.OsType -eq "Windows" -and
$_.LicenseType -notin @("Windows_Server", "Windows_Client")
}
$candidates | Select-Object ResourceGroupName, Name, @{n = "Size"; e = { $_.HardwareProfile.VmSize }}
if ($Apply) {
foreach ($item in $candidates) {
$vm = Get-AzVM -ResourceGroupName $item.ResourceGroupName -Name $item.Name
$vm.LicenseType = "Windows_Server"
Update-AzVM -ResourceGroupName $vm.ResourceGroupName -VM $vm | Out-Null
Write-Output "Azure Hybrid Benefit enabled on $($vm.Name)"
}
}
For SQL Server VMs, use Update-AzSqlVM -LicenseType AHUB. If you’re planning a larger move, our guide to datacenter migration to Azure covers licensing in the business case.
Phase 4: Advanced Governance and Automation
Cost Control With Azure Policy
Require justification for high-cost VM sizes, maintaining the list from your own price list:
{
"properties": {
"displayName": "Require business justification for high-cost VM sizes",
"policyType": "Custom",
"mode": "Indexed",
"metadata": { "category": "Cost Management" },
"parameters": {
"highCostVmSizes": {
"type": "Array",
"defaultValue": [
"Standard_M416ms_v2",
"Standard_E96ds_v5",
"Standard_NC80adis_H100_v5",
"Standard_ND96isr_H100_v5"
]
},
"allowedJustifications": {
"type": "Array",
"defaultValue": [
"high-performance-computing",
"machine-learning-training",
"large-database-workload",
"approved-by-finance"
]
}
},
"policyRule": {
"if": {
"allOf": [
{ "field": "type", "equals": "Microsoft.Compute/virtualMachines" },
{ "field": "Microsoft.Compute/virtualMachines/sku.name", "in": "[parameters('highCostVmSizes')]" },
{
"not": {
"field": "tags['BusinessJustification']",
"in": "[parameters('allowedJustifications')]"
}
}
]
},
"then": { "effect": "deny" }
}
}
}
Deploy auto-shutdown for non-production VMs. This DeployIfNotExists policy creates a shutdown schedule for VMs tagged development, test or staging, passing the VM name and location into the template. Its managed identity uses Virtual Machine Contributor, which includes schedule permissions:
{
"properties": {
"displayName": "Deploy auto-shutdown for non-production VMs",
"policyType": "Custom",
"mode": "Indexed",
"parameters": {
"effect": {
"type": "String",
"allowedValues": ["DeployIfNotExists", "AuditIfNotExists", "Disabled"],
"defaultValue": "DeployIfNotExists"
},
"shutdownTime": { "type": "String", "defaultValue": "1900" },
"timeZoneId": { "type": "String", "defaultValue": "W. Europe Standard Time" }
},
"policyRule": {
"if": {
"allOf": [
{ "field": "type", "equals": "Microsoft.Compute/virtualMachines" },
{ "field": "tags['Environment']", "in": ["development", "test", "staging"] }
]
},
"then": {
"effect": "[parameters('effect')]",
"details": {
"type": "Microsoft.DevTestLab/schedules",
"name": "[concat('shutdown-computevm-', field('name'))]",
"existenceCondition": {
"field": "Microsoft.DevTestLab/schedules/status",
"equals": "Enabled"
},
"roleDefinitionIds": [
"/providers/Microsoft.Authorization/roleDefinitions/9980e02c-c2be-4d73-94e8-173b1dc7cf3c"
],
"deployment": {
"properties": {
"mode": "incremental",
"parameters": {
"vmName": { "value": "[field('name')]" },
"location": { "value": "[field('location')]" },
"shutdownTime": { "value": "[parameters('shutdownTime')]" },
"timeZoneId": { "value": "[parameters('timeZoneId')]" }
},
"template": {
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"vmName": { "type": "string" },
"location": { "type": "string" },
"shutdownTime": { "type": "string" },
"timeZoneId": { "type": "string" }
},
"resources": [
{
"type": "Microsoft.DevTestLab/schedules",
"apiVersion": "2018-09-15",
"name": "[concat('shutdown-computevm-', parameters('vmName'))]",
"location": "[parameters('location')]",
"properties": {
"status": "Enabled",
"taskType": "ComputeVmShutdownTask",
"dailyRecurrence": { "time": "[parameters('shutdownTime')]" },
"timeZoneId": "[parameters('timeZoneId')]",
"targetResourceId": "[resourceId('Microsoft.Compute/virtualMachines', parameters('vmName'))]"
}
}
]
}
}
}
}
}
}
}
}
For start and stop schedules across subscriptions, use Start/Stop VMs v2.
Automated Cleanup Runbook
This Azure Automation runbook reports unused resources and removes them only with -Remove. It uses each disk’s LastOwnershipUpdateTime (when it was detached) rather than its creation time, skips anything tagged DoNotDelete, and leaves pricing to your export data instead of hard-coded rates.
param(
[Parameter(Mandatory = $true)]
[string]$SubscriptionId,
[int]$UnusedDays = 30,
[switch]$Remove
)
Connect-AzAccount -Identity | Out-Null
Set-AzContext -SubscriptionId $SubscriptionId | Out-Null
$cutoff = (Get-Date).AddDays(-$UnusedDays)
$report = @{ Date = Get-Date; UnattachedDisks = @(); UnusedPublicIPs = @(); IdleVMs = @() }
# Unattached managed disks
foreach ($disk in Get-AzDisk | Where-Object { $_.DiskState -eq "Unattached" }) {
if ($disk.Tags -and $disk.Tags.ContainsKey("DoNotDelete") -and $disk.Tags["DoNotDelete"] -eq "true") { continue }
$since = if ($disk.LastOwnershipUpdateTime) { $disk.LastOwnershipUpdateTime } else { $disk.TimeCreated }
if ($since -gt $cutoff) { continue }
$report.UnattachedDisks += @{ Name = $disk.Name; ResourceGroup = $disk.ResourceGroupName; SizeGB = $disk.DiskSizeGB; Sku = $disk.Sku.Name; Since = $since }
if ($Remove) { Remove-AzDisk -ResourceGroupName $disk.ResourceGroupName -DiskName $disk.Name -Force | Out-Null }
}
# Public IP addresses without an IP configuration or NAT gateway
foreach ($pip in Get-AzPublicIpAddress | Where-Object { -not $_.IpConfiguration -and -not $_.NatGateway }) {
if ($pip.Tag -and $pip.Tag["DoNotDelete"] -eq "true") { continue }
$report.UnusedPublicIPs += @{ Name = $pip.Name; ResourceGroup = $pip.ResourceGroupName; Sku = $pip.Sku.Name }
if ($Remove) { Remove-AzPublicIpAddress -ResourceGroupName $pip.ResourceGroupName -Name $pip.Name -Force }
}
# Running VMs with low average CPU over the period (report only)
foreach ($vm in Get-AzVM -Status | Where-Object { $_.PowerState -eq "VM running" }) {
$cpu = Get-AzMetric -ResourceId $vm.Id -MetricName "Percentage CPU" -StartTime $cutoff -EndTime (Get-Date) `
-TimeGrain 01:00:00 -AggregationType Average -WarningAction SilentlyContinue
$avg = ($cpu.Data | Where-Object { $null -ne $_.Average } | Measure-Object -Property Average -Average).Average
if ($null -ne $avg -and $avg -lt 5) {
$report.IdleVMs += @{ Name = $vm.Name; ResourceGroup = $vm.ResourceGroupName; Size = $vm.HardwareProfile.VmSize; AvgCpu = [math]::Round($avg, 2) }
}
}
# Send the report to a Logic App for notification
$logicAppUrl = Get-AutomationVariable -Name "CleanupReportLogicAppUrl"
Invoke-RestMethod -Uri $logicAppUrl -Method Post -Body ($report | ConvertTo-Json -Depth 4) -ContentType "application/json"
Treat low CPU as a prompt to talk to the owner, not proof of waste.
Phase 5: Reporting and Analytics
- FinOps toolkit Power BI reports: cost summary, rate optimization, invoicing and chargeback, and more, on top of exports or FinOps hubs
- Microsoft Fabric: a FinOps workspace can read FOCUS exports through lakehouse shortcuts
- FinOps workbooks: the toolkit’s Cost optimization and Governance workbooks for operations teams
- Cost analysis and scheduled alerts: shareable views without building anything
Avoid custom Power BI push datasets. The older Cost Management connector for Power BI is in maintenance mode and lacks some savings plan details, so Microsoft recommends exports-based reports.
Implementation Roadmap
Rough planning ranges for one Azure tenant; large or regulated estates take longer.
Weeks 1–2: Foundation Setup
- Configure FOCUS and price sheet exports
- Deploy tagging policies and enable tag inheritance
- Set up budgets, anomaly alerts and scheduled alerts
- Review Azure Advisor cost recommendations
Weeks 3–4: Analysis and Optimization Tools
- Deploy FinOps hubs and the Power BI reports
- Add Resource Graph waste queries
- Analyze reservation and savings plan options
- Validate Azure Hybrid Benefit entitlement
Weeks 5–6: Governance Implementation
- Deploy cost control policies
- Set up auto-shutdown and Start/Stop VMs v2
- Add approval workflows for high-cost resources
- Run the cleanup runbook in report mode
Weeks 7–8: Reporting and Continuous Improvement
- Publish executive Power BI reports
- Roll out workbooks to operations teams
- Move proven cleanup steps from report to remove mode
- Establish a regular FinOps review cadence
Best Practices and Pro Tips
Azure-Specific Optimization Strategies
- Use Azure Hybrid Benefit wherever you hold eligible licenses, and track entitlement centrally
- Use Spot VMs for interruptible batch and test workloads
- Autoscale on Azure Monitor metrics
- Tier storage with lifecycle management rules
Governance
- Use management groups for policy inheritance and cost visibility
- Apply resource locks so cleanup automation can’t remove critical resources
- Use auto-shutdown and Start/Stop VMs v2 for development and test environments
- Consider Azure DevTest Labs for development and test scenarios with built-in cost controls
Automation
- Use managed identities for all automation
- Handle errors and log actions so automation doesn’t disrupt operations
- Route expensive requests through Logic Apps approval workflows
- Monitor the automation itself for failures
Cultural Integration
- Show cost estimates in deployment pipelines
- Train developers on Azure pricing models
- Assign ownership of costs to business unit owners
- Review costs monthly and share optimization wins
Measuring Success
| KPI category | Examples |
|---|---|
| Cost efficiency | Cost per business unit or application, month-over-month variance, reservation and savings plan utilization and coverage, Azure Hybrid Benefit adoption, waste removed |
| Operations | Time to detect anomalies, automated optimization actions, tag compliance, budget forecast accuracy |
| Business value | Savings achieved, engineering time saved, chargeback accuracy, budget freed for new initiatives |
The toolkit’s reports calculate commitment utilization, coverage and savings, so you don’t need custom KPI scripts. Set targets that fit your own baseline.
Conclusion
Azure’s native tools and the Microsoft FinOps toolkit give you an integrated, low-cost FinOps foundation, with distinctive strengths in policy-based governance through management groups, FOCUS-aligned exports and Power BI reporting, Microsoft Entra ID integration, and licensing levers such as Azure Hybrid Benefit.
Start with FOCUS exports and tagging governance, then add budgets, anomaly alerts and FinOps hubs, and build toward automation. The foundations you set now become more valuable as your Azure estate grows.
Sources
- Tutorial: Create and manage Cost Management exports – Microsoft Learn
- FOCUS cost and usage details file schema – Microsoft Learn
- FinOps hubs – Microsoft Learn
- Use tags to organize your Azure resources and management hierarchy – Microsoft Learn
- Identify anomalies and unexpected changes in cost – Microsoft Learn
- Decide between a savings plan and a reservation – Microsoft Learn
- Changes to the Azure reservation exchange policy – Microsoft Learn
- Explore Azure Hybrid Benefit for Windows VMs – Microsoft Learn
Last reviewed: October 2026.

Leave a Reply