Azure Budgets leveraging Bicep Registries
Introduction
This post is a part of Azure Spring Clean which is a community event focused on Azure management topics from March 14-18, 2022. Thanks to Joe Carlyle and Thomas Thornton for putting in the time and organizing this event. From a participant’s perspective it’s been enjoyable to contribute. This topic specifically outlines how to leverage a Bicep registry with modules to configure Azure Budgets.
What are Azure Budgets?
Azure Budgets are a tool which can be implemented to help track and monitor current and future spend. In a consumption-based billing model, these budgets are crucial for alerting any anomalies or increase in costs. These budgets can be set at the Subscription, Resource Group, and even the individual resource level….that is with a little understanding of how budgets can be configured and implemented.
Approach
For this walkthrough we are going to leverage Bicep and specifically structure it as a module. If unfamiliar with Bicep modules check out Dave Rendon’s Spring Clean article Azure Bicep Language – Build a WordPress environment using Bicep modules
This budget module can be implemented as a local module or via a private registry I am going to walk through the registry option. However; to use locally just need to include the budget.module.bicep
file in your code and update the references accordingly. The hope here is to leave you with confidence on how to use an Azure Bicep Budget Module with Registry
Prerequisites
- An Active Azure account: You can create an account for free.
- Azure Bicep installed in your local machine.
- An Azure Container (optional)
Module Design
When designing reusable modules, I try to default what I can while requiring what’s required. The hope here is to make things easier while still providing the ability to override. If reusing a module specific to budget’s the only thing that’ can’t be defaulted is the resource the budget is being applied to.
Wait what? What about the actual budget? Well, if we assign a default of something like 100 maybe that’s good enough for the majority of the time, yet we should accommodate for the ability to override.
The Code
I’m just going to dive into this one as feels it could be easiest.
budget.module.bicep
param resourceId string
param budgetAmount int = 100
param firstThreshold int = 80
param secondThreshold int = 110
param contactEmails array = []
param startDate string = '${utcNow('yyyy-MM')}-01'
param timeGrain string = 'Monthly'
param filterName string = 'ResourceId'
param contactRoles array = [
'Owner'
]
param category string = 'Costs'
var operator = 'GreaterThan'
resource budget 'Microsoft.Consumption/budgets@2021-10-01' = {
name: '${substring(resourceId,lastIndexOf(resourceId,'/'),(length(resourceId)-lastIndexOf(resourceId,'/')))}-ConsumptionBudget'
properties: {
timePeriod: {
startDate: startDate
}
timeGrain: timeGrain
category: category
amount: budgetAmount
notifications:{
NotificationForExceededBudget1: {
enabled: true
contactEmails: contactEmails
contactRoles: contactRoles
threshold: firstThreshold
operator: operator
}
NotificationForExceededBudget2: {
enabled: true
contactEmails: contactEmails
contactRoles: contactRoles
threshold: secondThreshold
operator: operator
}
}
filter: {
and: [
{
dimensions: {
name: filterName
operator: 'In'
values: [
resourceId
]
}
}
]
}
}
}
Alright now to break this down. There are a few ‘unique’ things occurring here that you should be aware of. When assigning budgets, the startDate
is required. Furthermore, it is required to be the first day of a month, and it cannot be in the future. So, let’s do some magic on the utc
() operation and add the first day of the month to follow ‘YYYY-MM-01’ format. Something like '${utcNow('yyyy-MM')}-01'
should do the trick.
The budget name has some complex Bicep functionality to it as well. This didn’t have to be done this way; however, I like to give items a meaningful name. Check out my post on The All Mighty Importance of a Uniformed Naming Standard. When working with Bicep one of the easier things to do is grab the Resource ID of an Azure resource. Furthermore, a budget query can filter by Resource ID so why not use that as a starting point?
The hiccup is that’s one nasty name to use on the name of the actual budget. To accommodate that the resource we are assigning the budget toshould be last segment of the Resource ID. This is known just by the way an Azure Resource ID is structured.
Luckily Bicep has functionality that allows us to parse out the last segment with a call like: '${substring(resourceId,lastIndexOf(resourceId,'/'),(length(resourceId)-lastIndexOf(resourceId,'/')))}'
This will take the passed in resource ID find the position of the last ‘/’, which denotes segment. This will be the starting point of the substring and we will go the different between the position of the last ‘/’ and the length of the Resource ID.
So for example:
/subscriptions/88888888-4444-4444-4444-9999999999/resourceGroups/rg-springclean-dev-cus/providers/Microsoft.Storage/storageAccounts/saspringcleandevcus
The length is 150, the location of the last ‘/’ is 130. So, the substring will be between characters 131-150, which is saspringcleandevcus
, the name of our resource.
Notification Budgets
Believe it or not I kept this part simple. This is structured to send just two emails, based on two thresholds (firstThreshold
and secondThreshold
. Ideally these could be configured and passed in as objects similar to my post Nested Loops in Azure Bicep with an RBAC Example
I am not a fan of having ContactRoles
being attached; however, a budget requires some form of Contact to be filled out. This example can be ContactRoles
or ContactEmails
; however, since I like being nice, we will default some values for ContactRoles
just so there is a default.
Filtering
Little known fact. Budgets are technically set at a Subscription level with filters to better scope against the amount we pass in. This explains in the portal and going Subscription->Budgets all budgets are displayed. The budget is actually filtered by Resource Group or other items such as Resource ID.
Thus, if this section of code is implemented correctly it can both be filtered at a Resource Group or Resource ID level. It just depends on the conditions passed in.
For this exercise I have defaulted it to be Resource ID based; however, we can override to denote the Resource Group which I will also show how to do.
Registry
Ideally this budget module should be able to reused across multiple resource group deployments. To achieve this most efficiently. If unfamiliar here is the link on Bicep Registries. The TL/DR version of this is we have the module file stored in a location in Azure. The individual deployments will need access to this registry. For our purposes the only difference is how to call the module which I will show.
Deploying Budgets
So, at this point we have the budget.module.bicep
defined. You may have chosen to put this in a module within your local codebase or housed in a registry via container. These examples are for registry; however, again feel free to substitute and reference the module local to the codebase.
With Defaults
Here is a generic Key Vault module with the budget included at the end:
param location string = resourceGroup().location
param tags object
param tenantId string = subscription().tenantId
param keyVaultName string
param principalID string
var roleID= 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7'
var principalType = 'ServicePrincipal'
resource keyVault 'Microsoft.KeyVault/vaults@2021-11-01-preview' = {
name: keyVaultName
location: location
tags: tags
properties: {
enabledForTemplateDeployment: true
enablePurgeProtection: true
enableRbacAuthorization: true
enableSoftDelete: true
sku: {
family: 'A'
name: 'standard'
}
softDeleteRetentionInDays: 30
tenantId: tenantId
}
}
resource roleAssignment'Microsoft.Authorization/roleAssignments@2020-08-01-preview' ={
name: guid(principalID, roleID,keyVaultName)
scope: keyVault
properties: {
roleDefinitionId: '/providers/Microsoft.Authorization/roleDefinitions/${roleID}'
principalId: principalID
principalType: principalType
}
}
module budget 'br:springcleaningbicepregistrydeveus.azurecr.io/bicep/modules/budget:v2'= {
name: '${keyVault.name}-budget'
params: {
resourceId: keyVault.id
}
}
output keyVaultNameOutput string = keyVault.name
Specifically look at
module budget 'br:springcleaningbicepregistrydeveus.azurecr.io/bicep/modules/budget:v2'= {
name: '${keyVault.name}-budget'
params: {
resourceId: keyVault.id
}
}
So prove this works?
Can see that the budget information has accepted the defaults we passed in. In this case a budget of $100 USD, the creation start date of the first of month, the alert conditions, and a lack of emails.
Pretty easy right? So what if we want to customize on a difference resource?
Resource Budget with Custom Params
So for this one let’s use a basic storage account:
param location string = resourceGroup().location
param storageAccountName string
param budgetAmount int
param contactEmails array =[
'JohnDoe@Microsoft.com'
]
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-08-01' = {
name: storageAccountName
location: location
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}
module budget 'br:springcleaningbicepregistrydeveus.azurecr.io/bicep/modules/budget:v2'= {
name: '${storageAccount.name}-budget'
params:{
resourceId: storageAccount.id
budgetAmount: budgetAmount
contactEmails: contactEmails
}
}
This one we have provided a default contact emails and a budgetAmount
parameter which is actually fed from a parameter file. We do this as it provides better tuning on thresholds.
This can be confirmed in the portal:
Can verify the $20 USD passed in from a parameters file made it to the budget. In addition the alert for “JohnDoe@Microsoft.com” has been included. Also, notice the filter here is down to the storage account.
Resource Group Budgets
So, what about Resource Group Budgets? Odds are a Resource Group Budget is much more practical then a Resource budget so what would that look like?
Well really not much different than the resources:
targetScope = 'subscription'
param location string
param resourceGroupName string
param tags object
resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = {
name: resourceGroupName
location: location
tags: tags
}
module budget 'br:springcleaningbicepregistrydeveus.azurecr.io/bicep/modules/budget:v2'= {
scope: resourceGroup
name: '${resourceGroup.name}-budget'
params:{
resourceId: resourceGroup.id
filterName: 'ResourceGroup'
}
}
The only exception here is the filterName
on the budget is being overwritten to ResourceGroup
. Resource Groups also have a Resource ID which will again be parsed to the just the last segment which is the Resource Group itself.
Can see here the Filter has been updated to reflect a Resource Group.
Conclusion
This was kind of cool right? We wrote one module for handling budgets. That module is hosted in a centralized registry and can be applied to any number of scenarios. This blog post just showed how it can be scoped to a few individual resources as well as Resource Groups. Hopefully this gives you a bit of an idea on how something like this can be scalable across an Azure ecosystem.
Hi John,
Great article on the Azure budget template! I was wondering if there’s a way to make the “startdate” parameter dynamic, apparently it won’t allow me to parse the provided parameter into the bicep file when i deploy. Thank you for sharing your knowledge on this topic!
Hey Simon,
Great to hear you found it helpful! So above this is dynamic by taking the start day of the current month.
param startDate string = '${utcNow('yyyy-MM')}-01'
This is really something I do not like about the budget provider API as the date has to be 1.) the first day of the month and 2.) the month cannot be earlier then the current month.
If wanting to provide users the ability to set a budget at future start date I’d recommend just having a parameter for startMonth in format MM and do something like:
param startDateYear string = utcNow('yyyy')
var startDate string = '${startDateyear}-${startMonth}-01'
This is in part because the utcNow() function can only be used at the parameter level.
Hope this helps!