Set an Azure Budget dynamically based on last month’s costs

Azure Budgets offers a great way to plan and drive accountability towards the Azure costs. Azure Budgets allow you to set a threshold value for your Azure environment and helps in proactively manage costs. You can also set alerts on top of the budgets using Azure Action Groups. A budget can be created at a subscription or a resource group level. To learn more on how to set up a budget, please check the official Microsoft documentation here – https://docs.microsoft.com/en-us/azure/cost-management-billing/costs/tutorial-acm-create-budgets

Recently, I was working on a requirement to set up Azure budgets dynamically for the different subscriptions in our environment. Apparently, I figured out that. while assigning the budget, we can only provide a hard-coded value. So, to meet my requirement, I ended up using the Azure REST APIs. Here are the steps I followed.

Step 1: Create a service principal
The first thing that you need is a service principal that should have at least “Cost Management Contributor” permissions on the subscription/resource group for which you’re setting the alert. You will need the client id and client secret for the service principal. If you don’t have a service principal, here is the Azure CLI command to create one:

az ad sp create-for-rbac –name ServicePrincipalName

Here is the response that I got:

Changing "ServicePrincipalName" to a valid URI of "http://ServicePrincipalName", which is the required format used for service principal names 
Creating a role assignment under the scope of "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 
Retrying role assignment creation: 1/36
Retrying role assignment creation: 2/36 

{
  "appId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "displayName": "ServicePrincipalName",
  "name": "http://ServicePrincipalName",
  "password": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "tenant": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

Copy the appId, and password as you would need it later for getting access token.

Step 2: Get the name(GUID) for Cost Management Contributor role
Call the following Azure CLI command to get the Role ID (GUID) for the Cost Management Contributor role:

az role definition list --query "[?roleName=='Cost Management Contributor'].name"

Copy the guid to be used in Step 3

Step 3: Grant the service principal permissions on the subscription
Call the following Azure CLI command to assign the Cost Management Contributor role to the service principal:

az role assignment create --assignee <<client_id_from_step1>> --role<<Role_Guid_from_Step2>>

Here is the response:

{
  "canDelegate": null,
  "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/providers/Microsoft.Authorization/roleAssignments/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "name": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "principalId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "principalType": "ServicePrincipal",
  "roleDefinitionId": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/providers/Microsoft.Authorization/roleDefinitions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "scope": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "type": "Microsoft.Authorization/roleAssignments"
}

Step4: Fetch the access token
Call the following API to fetch the access token:

API : https://login.microsoftonline.com/{Tenant_Id}/oauth2/token
HTTP Method : POST
Body: client_id={appId_from_step1}&client_secret={password_from_step1}&grant_type=client_credentials&resource=https%3A%2F%2Fmanagement.azure.com
Content-Type: application/x-www-form-urlencoded

Here is the response that you will get:

{
  "token_type": "Bearer",
  "expires_in": "3599",
  "ext_expires_in": "3599",
  "expires_on": "1593476005",
  "not_before": "1593472105",
  "resource": "https://management.azure.com",
  "access_token": "xxxxxxxxxxxxxxxxxxxx"
}

Copy the access token as you would need to call the Azure REST API

Step 5: Fetch the last month’s billed amount for the subscription using cost management API:
Call the Azure Cost Management API to fetch the last month’s billed amount:

API : https://management.azure.com/subscriptions/<<Subscription_Id>>/providers/Microsoft.CostManagement/query?api-version=2019-11-01&$top=10000
HTTP Method : POST
Headers: Authorization : Bearer <<access_token_from Step4>>
Content-Type: application/json; charset=utf-8
Body: {
    "type": "ActualCost",
    "dataSet": {
        "granularity": "None",
        "aggregation": {
            "totalCost": {
                "name": "PreTaxCost",
                "function": "Sum"
            },
            "totalCostUSD": {
                "name": "PreTaxCostUSD",
                "function": "Sum"
            }
        },
        "grouping": [{
            "type": "Dimension",
            "name": "ChargeType"
        }, {
            "type": "Dimension",
            "name": "PublisherType"
        }]
    },
    "timeframe": "Custom",
    "timePeriod": {
        "from": "2020-03-01T00:00:00+00:00",
        "to": "2020-03-31T23:59:59+00:00"
    }
}

Note – Update the time period in the body to the first and last days of the last month.

Here is the response that you will get:

{
  "id": "subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/providers/Microsoft.CostManagement/query/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "name": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "type": "Microsoft.CostManagement/query",
  "location": null,
  "sku": null,
  "eTag": null,
  "properties": {
    "nextLink": null,
    "columns": [
      {
        "name": "PreTaxCost",
        "type": "Number"
      },
      {
        "name": "PreTaxCostUSD",
        "type": "Number"
      },
      {
        "name": "ChargeType",
        "type": "String"
      },
      {
        "name": "PublisherType",
        "type": "String"
      },
      {
        "name": "Currency",
        "type": "String"
      }
    ],
    "rows": [
      [
        "xxxx.xx",
        "xxxx.xx",
        "usage",
        "azure",
        "USD"
      ]
    ]
  }
}

Copy the PreTaxCost or PreTaxCostUSD amount from the response.

Step 6: Check if the budget already exists:
Call the Azure REST API to verify if the budget exists already:

API : https://management.azure.com/subscriptions/{Subscription_Id}/providers/Microsoft.Consumption/budgets/{budget_name}?api-version=2019-10-01
HTTP Method : GET
Headers: Authorization : Bearer <<access_token_from Step4>>
Body: none

If the budget exists, you will get the following response:

{
  "id": "subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/providers/Microsoft.Consumption/budgets/xxxxxxxxx",
  "name": "xxxxxxxxx",
  "type": "Microsoft.Consumption/budgets",
  "eTag": "\"1d64bf89c37xyzp\"",
  "properties": {
    "timePeriod": {
      "startDate": "2020-06-01T00:00:00Z",
      "endDate": "2021-06-30T00:00:00Z"
    },
    "timeGrain": "Monthly",
    "amount": xxxx.xx,
    "currentSpend": {
      "amount": xxxx.xx,
      "unit": "USD"
    },
    "category": "Cost",
    "notifications": {      
      "actual_GreaterThan_95_Percent": {
        "enabled": true,
        "operator": "GreaterThan",
        "threshold": 95.00,
        "contactEmails": [],
        "contactRoles": [],
        "contactGroups": [
          "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/xxxxxxxxx/providers/microsoft.insights/actionGroups/xxxxxxxxx"
        ],
        "thresholdType": "Actual"
      }
    },
    "filter": {},
    "currencySetting": "None"
  }
}

Copy the eTag value from the response.

If the budget doesn’t exists, you will get the following response:

{
  "error": {
    "code": "404",
    "message": "No budget found matching budgetName: xxxxxxxxxxxx, under storageScope: EntityType = Subscription, EntityId = xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, ChildScope = , MetaData =   (Request ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)"
  }
}

Step 7: Call the Azure REST API for updating the existing budget.
If the budget exists already, replace the eTag value in the body with the copied value from Step 6. Else, update with a random eTag value as provided in the example. Update the amount value with the last month’s billed amount as fetched in step#5. Also, update the start date and end date for your budget.

API : https://management.azure.com/subscriptions/<<Subscription_Id>>/providers/Microsoft.Consumption/budgets/<<Budget_Name>>?api-version=2019-10-01
HTTP Method : PUT
Headers: Authorization : Bearer <<access_token_from Step4>>
Content-Type: application/json; charset=utf-8
Body: {
  "eTag": "\"1d34d016a593709\"",
  "properties": {
    "category": "Cost",
    "amount": xxxx.xx,
    "timeGrain": "Monthly",
    "timePeriod": {
      "startDate": "2020-07-01T00:00:00Z",
      "endDate": "2021-06-30T00:00:00Z"
    },
    "notifications": {
      "Actual_GreaterThan_95_Percent": {
        "enabled": true,
        "operator": "GreaterThan",
        "threshold": 95,
        "contactEmails": [
          "<<Email_Addresses>>"
        ],
        "contactRoles": [],
        "contactGroups": [],
        "thresholdType": "Actual"
      }
    }
  }
}

Here is the response you will get, once the budget is created/updated:

{
  "id": "subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/providers/Microsoft.Consumption/budgets/xxxxxxxxx",
  "name": "xxxxxxxxx",
  "type": "Microsoft.Consumption/budgets",
  "eTag": "\"1d34d016a593709\"",
  "properties": {
    "timePeriod": {
      "startDate": "2020-07-01T00:00:00Z",
      "endDate": "2021-06-30T00:00:00Z"
    },
    "timeGrain": "Monthly",
    "amount": "xxxx.xx",
    "currentSpend": null,
    "category": "Cost",
    "notifications": {
      "actual_GreaterThan_95_Percent": {
        "enabled": true,
        "operator": "GreaterThan",
        "threshold": 95.00,
        "contactEmails": [
          "<<Email_Addresses>>"
        ],
        "contactRoles": [],
        "contactGroups": [],
        "thresholdType": "Actual"
      }
    },
    "filter": {},
    "currencySetting": "None"
  }
}

If you will go to the Azure portal, under your subscription, you will find the new budget has been created or the existed budget has been updated:

In my next blog post, I will share how to setup the logic app where to run these steps recursively once a month.

I hope you will find the post helpful.