7 min read
Azure Cost Management Budgets and Alerts
Introduction
Azure Cost Management budgets provide proactive cost control by alerting you when spending approaches or exceeds defined thresholds. Combined with automation, budgets can trigger actions to prevent cost overruns. This guide covers practical implementations for effective cost governance.
Creating Budgets via Azure CLI
# Create monthly budget at subscription level
az consumption budget create \
--budget-name "Monthly-Subscription-Budget" \
--amount 10000 \
--time-grain Monthly \
--start-date "2021-06-01" \
--end-date "2022-06-01" \
--category Cost \
--resource-group "" \
--notifications "{
\"Notification1\": {
\"enabled\": true,
\"operator\": \"GreaterThan\",
\"threshold\": 80,
\"contactEmails\": [\"finance@company.com\", \"ops@company.com\"],
\"thresholdType\": \"Actual\"
},
\"Notification2\": {
\"enabled\": true,
\"operator\": \"GreaterThan\",
\"threshold\": 100,
\"contactEmails\": [\"finance@company.com\", \"ops@company.com\", \"cto@company.com\"],
\"thresholdType\": \"Actual\"
},
\"Notification3\": {
\"enabled\": true,
\"operator\": \"GreaterThan\",
\"threshold\": 100,
\"contactEmails\": [\"finance@company.com\"],
\"thresholdType\": \"Forecasted\"
}
}"
# Create budget for specific resource group
az consumption budget create \
--budget-name "Development-RG-Budget" \
--amount 1000 \
--time-grain Monthly \
--start-date "2021-06-01" \
--end-date "2022-06-01" \
--category Cost \
--resource-group "rg-development" \
--notifications "{
\"OverBudget\": {
\"enabled\": true,
\"operator\": \"GreaterThan\",
\"threshold\": 90,
\"contactEmails\": [\"dev-lead@company.com\"]
}
}"
Bicep Budget Definitions
// budgets.bicep
targetScope = 'subscription'
@description('Monthly budget amount')
param budgetAmount int = 10000
@description('Budget start date')
param startDate string = '2021-06-01'
@description('Alert email addresses')
param alertEmails array = []
@description('Action group resource ID for automation')
param actionGroupId string = ''
resource monthlyBudget 'Microsoft.Consumption/budgets@2021-10-01' = {
name: 'Monthly-Budget'
properties: {
category: 'Cost'
amount: budgetAmount
timeGrain: 'Monthly'
timePeriod: {
startDate: startDate
endDate: '2025-12-31'
}
notifications: {
Actual_80_Percent: {
enabled: true
operator: 'GreaterThan'
threshold: 80
thresholdType: 'Actual'
contactEmails: alertEmails
contactGroups: !empty(actionGroupId) ? [actionGroupId] : []
}
Actual_100_Percent: {
enabled: true
operator: 'GreaterThan'
threshold: 100
thresholdType: 'Actual'
contactEmails: alertEmails
contactGroups: !empty(actionGroupId) ? [actionGroupId] : []
}
Forecasted_100_Percent: {
enabled: true
operator: 'GreaterThan'
threshold: 100
thresholdType: 'Forecasted'
contactEmails: alertEmails
}
}
}
}
// Budget with filter for specific services
resource computeBudget 'Microsoft.Consumption/budgets@2021-10-01' = {
name: 'Compute-Budget'
properties: {
category: 'Cost'
amount: 5000
timeGrain: 'Monthly'
timePeriod: {
startDate: startDate
endDate: '2025-12-31'
}
filter: {
dimensions: {
name: 'ServiceName'
operator: 'In'
values: [
'Virtual Machines'
'Virtual Machine Scale Sets'
]
}
}
notifications: {
HighCompute: {
enabled: true
operator: 'GreaterThan'
threshold: 90
thresholdType: 'Actual'
contactEmails: alertEmails
}
}
}
}
Budget-Triggered Automation
Azure Function for Budget Alerts
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Azure.Identity;
using Azure.ResourceManager;
using Azure.ResourceManager.Compute;
public class BudgetAlertFunction
{
private readonly ILogger<BudgetAlertFunction> _logger;
public BudgetAlertFunction(ILogger<BudgetAlertFunction> logger)
{
_logger = logger;
}
[Function("BudgetAlertHandler")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
{
var alertData = await req.ReadFromJsonAsync<BudgetAlert>();
_logger.LogInformation(
"Budget alert received: {BudgetName}, Threshold: {Threshold}%, Current: {Current}",
alertData.BudgetName,
alertData.NotificationThresholdAmount,
alertData.SpentAmount);
// Check if we need to take action
if (alertData.SpentAmount >= alertData.BudgetAmount * 0.95m)
{
// Critical threshold - stop non-essential resources
await StopNonEssentialResourcesAsync(alertData.SubscriptionId);
}
else if (alertData.SpentAmount >= alertData.BudgetAmount * 0.80m)
{
// Warning threshold - scale down resources
await ScaleDownResourcesAsync(alertData.SubscriptionId);
}
var response = req.CreateResponse(System.Net.HttpStatusCode.OK);
await response.WriteAsJsonAsync(new { Status = "Processed" });
return response;
}
private async Task StopNonEssentialResourcesAsync(string subscriptionId)
{
var armClient = new ArmClient(new DefaultAzureCredential());
var subscription = await armClient.GetSubscriptions().GetAsync(subscriptionId);
// Find VMs tagged as non-essential
await foreach (var vm in subscription.Value.GetVirtualMachinesAsync())
{
if (vm.Data.Tags?.TryGetValue("Essential", out var value) == true &&
value?.ToLower() == "false")
{
_logger.LogInformation("Stopping non-essential VM: {VmName}", vm.Data.Name);
await vm.DeallocateAsync(Azure.WaitUntil.Started);
}
}
}
private async Task ScaleDownResourcesAsync(string subscriptionId)
{
var armClient = new ArmClient(new DefaultAzureCredential());
var subscription = await armClient.GetSubscriptions().GetAsync(subscriptionId);
// Scale down App Service Plans
// Note: Simplified - actual implementation would need more logic
_logger.LogInformation("Initiating scale-down for subscription: {SubId}", subscriptionId);
}
}
public record BudgetAlert
{
public string BudgetName { get; init; }
public string SubscriptionId { get; init; }
public decimal BudgetAmount { get; init; }
public decimal SpentAmount { get; init; }
public decimal NotificationThresholdAmount { get; init; }
}
Logic App for Budget Response
{
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"contentVersion": "1.0.0.0",
"triggers": {
"When_a_HTTP_request_is_received": {
"type": "Request",
"kind": "Http",
"inputs": {
"schema": {
"type": "object",
"properties": {
"budgetName": { "type": "string" },
"notificationThresholdAmount": { "type": "number" },
"spentAmount": { "type": "number" },
"subscriptionId": { "type": "string" }
}
}
}
}
},
"actions": {
"Check_Threshold": {
"type": "If",
"expression": {
"and": [
{
"greaterOrEquals": [
"@triggerBody()?['spentAmount']",
"@mul(triggerBody()?['notificationThresholdAmount'], 0.9)"
]
}
]
},
"actions": {
"Send_Critical_Alert": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['teams']['connectionId']"
}
},
"method": "post",
"path": "/v3/teams/{teamId}/channels/{channelId}/messages",
"body": {
"body": {
"content": "CRITICAL: Budget @{triggerBody()?['budgetName']} has reached @{triggerBody()?['spentAmount']} of @{triggerBody()?['notificationThresholdAmount']}"
}
}
}
},
"Create_PagerDuty_Incident": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "https://events.pagerduty.com/v2/enqueue",
"headers": {
"Content-Type": "application/json"
},
"body": {
"routing_key": "@{parameters('pagerDutyKey')}",
"event_action": "trigger",
"payload": {
"summary": "Azure Budget Alert - Critical",
"severity": "critical",
"source": "Azure Cost Management"
}
}
}
}
}
}
}
}
}
Cost Analysis Queries
PowerShell Cost Reporting
# Get cost breakdown by service
function Get-MonthlyCostReport {
param(
[DateTime]$StartDate = (Get-Date).AddDays(-30),
[DateTime]$EndDate = (Get-Date)
)
$subscriptionId = (Get-AzContext).Subscription.Id
$query = @{
type = "ActualCost"
timeframe = "Custom"
timePeriod = @{
from = $StartDate.ToString("yyyy-MM-dd")
to = $EndDate.ToString("yyyy-MM-dd")
}
dataset = @{
granularity = "None"
aggregation = @{
totalCost = @{
name = "Cost"
function = "Sum"
}
}
grouping = @(
@{
type = "Dimension"
name = "ServiceName"
}
)
}
}
$response = Invoke-AzRestMethod `
-Path "/subscriptions/$subscriptionId/providers/Microsoft.CostManagement/query?api-version=2021-10-01" `
-Method POST `
-Payload ($query | ConvertTo-Json -Depth 10)
$result = $response.Content | ConvertFrom-Json
$costs = $result.properties.rows | ForEach-Object {
[PSCustomObject]@{
ServiceName = $_[1]
Cost = [math]::Round($_[0], 2)
Currency = $result.properties.columns[0].name
}
}
return $costs | Sort-Object Cost -Descending
}
# Get cost trend
function Get-CostTrend {
param(
[int]$DaysBack = 30
)
$subscriptionId = (Get-AzContext).Subscription.Id
$query = @{
type = "ActualCost"
timeframe = "Custom"
timePeriod = @{
from = (Get-Date).AddDays(-$DaysBack).ToString("yyyy-MM-dd")
to = (Get-Date).ToString("yyyy-MM-dd")
}
dataset = @{
granularity = "Daily"
aggregation = @{
totalCost = @{
name = "Cost"
function = "Sum"
}
}
}
}
$response = Invoke-AzRestMethod `
-Path "/subscriptions/$subscriptionId/providers/Microsoft.CostManagement/query?api-version=2021-10-01" `
-Method POST `
-Payload ($query | ConvertTo-Json -Depth 10)
$result = $response.Content | ConvertFrom-Json
return $result.properties.rows | ForEach-Object {
[PSCustomObject]@{
Date = $_[1]
Cost = [math]::Round($_[0], 2)
}
}
}
Cost Allocation Tags
Enforcing Cost Center Tags
// Tag policy assignment
targetScope = 'subscription'
resource requireCostCenterPolicy 'Microsoft.Authorization/policyAssignments@2021-06-01' = {
name: 'require-costcenter-tag'
properties: {
displayName: 'Require CostCenter tag on resource groups'
policyDefinitionId: '/providers/Microsoft.Authorization/policyDefinitions/96670d01-0a4d-4649-9c89-2d3abc0a5025'
parameters: {
tagName: {
value: 'CostCenter'
}
}
enforcementMode: 'Default'
}
}
resource inheritTagPolicy 'Microsoft.Authorization/policyAssignments@2021-06-01' = {
name: 'inherit-costcenter-tag'
properties: {
displayName: 'Inherit CostCenter tag from resource group'
policyDefinitionId: '/providers/Microsoft.Authorization/policyDefinitions/cd3aa116-8754-49c9-a813-ad46512ece54'
parameters: {
tagName: {
value: 'CostCenter'
}
}
enforcementMode: 'Default'
}
}
Scheduled Cost Reports
Azure Function for Weekly Reports
[Function("WeeklyCostReport")]
public async Task Run(
[TimerTrigger("0 0 8 * * MON")] TimerInfo timer)
{
var costData = await _costService.GetWeeklyCostSummaryAsync();
var report = new StringBuilder();
report.AppendLine("# Weekly Azure Cost Report");
report.AppendLine($"Period: {costData.StartDate:d} - {costData.EndDate:d}");
report.AppendLine($"\n## Total Cost: ${costData.TotalCost:N2}");
report.AppendLine($"Change from last week: {costData.PercentageChange:+0.0;-0.0}%");
report.AppendLine("\n## Cost by Service");
foreach (var service in costData.ServiceCosts.OrderByDescending(s => s.Cost).Take(10))
{
report.AppendLine($"- {service.ServiceName}: ${service.Cost:N2}");
}
report.AppendLine("\n## Budget Status");
foreach (var budget in costData.BudgetStatus)
{
var percentage = (budget.Spent / budget.Amount) * 100;
var status = percentage > 90 ? "CRITICAL" : percentage > 75 ? "WARNING" : "OK";
report.AppendLine($"- {budget.Name}: ${budget.Spent:N2} of ${budget.Amount:N2} ({percentage:0}%) [{status}]");
}
await _emailService.SendReportAsync(
"finance@company.com",
"Weekly Azure Cost Report",
report.ToString());
}
Conclusion
Effective cost management requires proactive monitoring and automated responses. Azure Cost Management budgets provide the foundation for cost governance, while automation enables rapid response to spending anomalies. By combining budgets with alerts, tags, and automated actions, you can maintain cost control without sacrificing operational agility.