Azure, Bicep, Professional

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

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?

Screen showing budget with the defaults

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:

Screenshot of budget showing default params being overwritten

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.

Screen shot of a budget at the Resource Group Level

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.