Keeping cost under control is not easy task. Different teams have different needs and it’s hard to predict how much resources they will need. In this article I will show you how to track Azure subscription forecast using Azure Cost Management and Billing API.

Azure Cost CLI

There is a tool called Azure Cost CLI which allows you to get cost of your Azure subscription. The subscription view provide you with the following information:

  • current cost
  • forecasted cost
  • cost by service name
  • cost by location

Azure CLI view

I created an Azure DevOps template which allows you to run this tool in your pipeline.


parameters:
- name: environment
  type: string
- name: groups
  type: object
  default: []
- name: poolName
  type: string
  default: DevLinux

stages:
- stage: ${{ parameters.environment }}'
  pool:
    name: ${{ parameters.poolName }}
  condition: and(succeeded(), or(eq(variables['build.sourceBranch'], 'refs/heads/master'), ne(variables['Build.Reason'], 'PullRequest'), eq(variables['Build.Reason'], 'Manual')))
  jobs:
  - job: Run_Azure_Cost_CLI
    displayName: ${{ parameters.environment }} Cost
    variables:
    - name: TerraEnv
      value: ${{ lower(parameters.environment) }}
    - ${{ each value in parameters.groups }}:
      - group: ${{ value }}
    workspace:
      clean: all
    steps:
    - checkout: self
      persistCredentials: true
      clean: true

    - script: |
        az login --identity
        az account set --subscription $(TF_VAR_SUBSCRIPTION_ID)
      displayName: 'Login to Azure'

    - bash: |
        installedTools=$(dotnet tool list -g)

        echo "$installedTools"

        # Install the tool only if it isn't installed
        if [[ $installedTools != *"azure-cost-cli"* ]]; then
            dotnet tool install -g azure-cost-cli
        fi

        TOOL_PATH=$HOME/.dotnet/tools
        case ":$PATH:" in
          *:$TOOL_PATH:*) echo Terraform is already in the path;;
          *)
            echo "##[debug]Adding $TOOL_PATH to PATH" 
            echo "##vso[task.prependpath]$TOOL_PATH"
            export PATH=`pwd`:$PATH 
            ;;
        esac 
      displayName: 'Install Azure Cost CLI'

    - script: |
        azure-cost accumulatedCost --subscription $(TF_VAR_SUBSCRIPTION_ID)
      displayName: 'Run Azure Cost CLI'

And use it for each subscription you want to track:


name: 'Subscriptions Cost $(Date:yyyyMMdd)'
trigger:
  - master

schedules:
- cron: "0 10 * * *"
  displayName: Daily build
  branches:
    include:
    - master
  always: true

stages:
- template: display-cost.yml
  parameters:
    environment: Sub1
    groups:
    - Terraform-Azurerm-Provider-Settings-Sub1

- template: display-cost.yml
  parameters:
    environment: Sub2
    groups:
    - Terraform-Azurerm-Provider-Settings-Sub2

The tools is very useful but it has one drawback. You get a lot of details and you have to run in for each subscription. It would be nice to have a tool which will allow you to get forecast for all subscriptions in one place.

Azure Cost Management API

We will use two endpoints of Azure Cost Management API:

  • Query - for getting current cost
  • Forecast - for getting forecast

Both endpoints return data for individual resources and we need to aggregate them.

And to get data for all subscriptions we need to iterate over them. We can do this using Azure CLI and command az account list. We will use homeTenantId to filter subscriptions for current tenant.

Here is the script which allows you to get current cost and forecast for all subscriptions in your tenant:

#!/bin/bash

# Based on this
# https://github.com/Azure/azure-cli/issues/17102

# Set the start and end dates in YYYY-MM-DD format
startDate="2023-09-01"
endDate="2023-09-30"

echo "Start date: $startDate"
echo "End date: $endDate"

# Set the query to get the cost data
query='{
    "type": "ActualCost",
    "timeframe": "Custom",
    "timePeriod": {
        "from": "'"$startDate"'",
        "to": "'"$endDate"'"
    },
    "dataset": {
        "granularity": "Monthly",
        "aggregation": {
            "totalCost": {
                "name": "Cost",
                "function": "Sum"
            }
        },
        "grouping": [
            {
                "type": "Dimension",
                "name": "SubscriptionId"
            },
            {
                "type": "Dimension",
                "name": "BillingMonth"
            }
        ]
    }
}'

# Get the current month and year
current_month=$(date +%m)
current_year=$(date +%Y)

# Calculate the first day of the month
first_day_of_month="${current_year}-${current_month}-01"

# Calculate the last day of the month
last_day_of_month=$(gdate -d "${first_day_of_month} +1 month -1 day" +%Y-%m-%d)

echo "First day of the current month: ${first_day_of_month}"
echo "Last day of the current month: ${last_day_of_month}"

queryForecast='{
    "type": "Usage",
    "timeframe": "Custom",
    "timePeriod": {
        "from": "'"$first_day_of_month"'",
        "to": "'"$last_day_of_month"'"
    },
    "dataset": {
        "granularity": "Monthly",
        "aggregation": {
            "totalCost": {
                "name": "Cost",
                "function": "Sum"
            }
        }
    },
    "includeActualCost": true,
    "includeFreshPartialCost": true
}'

currentTenant=$(az account show --query 'homeTenantId' -o tsv)

for id in $(az account list --query "[?homeTenantId=='$currentTenant'].{Id:id}" -o tsv); do
    subscriptionName=$(az account subscription show --subscription-id $id  | jq '.displayName')
    echo "========================================================="
    echo -e "\033[32mSubscription name: $subscriptionName\033[0m"
    subscriptionId=$id

    authToken=$(az account get-access-token --subscription $subscriptionId --resource=https://management.azure.com/ --query accessToken -o tsv)

    # Call the Cost Management API to get the cost data
    response=$(curl -s -X POST -H "Authorization: Bearer $authToken" -H "Content-Type: application/json" -d "$query" "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.CostManagement/query?api-version=2023-03-01")
    
    previousMonth=$(echo $response | jq '.properties.rows[] | first ' | awk '{sum=sum+$0} END{print sum}')
    echo "Previous month: $previousMonth"

    responseForecast=$(curl -s -X POST -H "Authorization: Bearer $authToken" -H "Content-Type: application/json" -d "$queryForecast" "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.CostManagement/forecast?api-version=2023-03-01")
    
    forecast=$(echo $responseForecast | jq '.properties.rows[] | first ' | awk '{sum=sum+$0} END{print sum}')
    echo "Forecast: $forecast"

    sleep 30s
done

I hardcoded start and end date to get data for September 2023. You can change it to get data for different period.

As a result you will get something like this:

=========================================================
Subscription name: "Sub1"
Previous month: 100.45
Forecast: 53.8079
WARNING: Command group 'account subscription' is experimental and under development. Reference and support levels: https://aka.ms/CLI_refstatus
=========================================================
Subscription name: "Sub2"
Previous month: 2334.31
Forecast: 2478.87
WARNING: Command group 'account subscription' is experimental and under development. Reference and support levels: https://aka.ms/CLI_refstatus
=========================================================

Summary

In this article I showed you how to track Azure subscription forecast using Azure Cost Management and Billing API. I hope you will find it useful.

And what do you think about putting this script in Azure DevOps pipeline or maybe Azure Function? Please share your opinions in comments.