Azure Solutions

Data Acquisition: Azure Cost Management

image

It is not possible to simply query the Azure cost details dataset with your favorite data tools. It is possible, however, to get the data.

Microsoft Cost Management + Billing documentation abstracts multiple options: 1) Azure Portal, 2) Power BI, 3) API, and 4) Exports.

This documentation details step-by-step instructions for some of these options.

Option Pros Cons
Azure Portal - Easy-to-Use
- No additional tools or coding required
- Targets “small cost detail” datasets (<2GB)
Power BI ??? - Requires permissions most people lack
API - Can pull historical data greater than 1 year - No bulk query
- Limited to 15k read requests per hour (more)
…via Data Factory - Familiar orchestration engine - Nested iteration impossible
…via Logic App - Nested iteration possible - Challenging interface
- Slow processing
- Difficult to monitor progress
…via Function App - Nested iteration possible
- Visual Studio {i.e., IntelliSense, NuGet, and GitHub}
- Requires coding expertise
Exports - “Most scalable solution” - No historical data (pull begins day you instantiate)

This list of Pros and Cons is based on my research, experience and perception ONLY.

Your answer to “why use Option X?” may be as simple as the fact that you favor that option.

API, via Data Factory (or Synapse Pipeline)

image

This use case considers the following requirement statements:

Step 1: Prepare Infrastructure

In addition to the items listed at the beginning of this documentation, this solution requires the following resources:

Step 2: Create Destination Table

In this step, we will create the Data Explorer table which will serve as destination for our Synapse Pipeline

Note: This initial version of the table includes only a few of the columns that might be included as you evolve your query logic

Step 3: Create Linked Service

In this step, we will create the Linked Service we will use to get our Bearer Token.

Note: Consider including the Subscription Name in your Linked Service and Dataset names

Step 4: Create Dataset

In this step, we will create the Dataset we will use to get our Bearer Token.

Step 5: Create Pipeline

In this step, we will create a Pipeline and add Activities.

Step 5a: Create Activity 1, Get Token

This activity will make a REST API call and get a bearer token.

Step 5b: Create Activity 2, Copy Data

This activity will make a REST API call, parse the response and write the data to Data Explorer.

“Source” tab

Note: If your “Preview Data” fails and you get an “The access token is invalid” error, it is likely that the token expired… simply Debug to get a new token

“Sink” tab

“Mapping” tab

Step 6: Confirm Success

In this step, we will process the pipeline and review the resulting data.

API, via Logic App

image

This use case considers the following requirement statements:


The solution described in this documentation will:

Notes:

Step 1: Prepare Resources

In addition to the items listed at the beginning of this documentation, this solution requires the following resources:

Step 2: Prepare Workflow

In this step, we will create a workflow, initialize variables, and add parameters.

Workflow

Initialize Variable, Date

Initialize Variable, Scope

Initialize Variable, KQL

Parameters

Step 3: Get Bearer Token

In this step, we will request an access token from the Client Credentials Token URL and initialize a Token variable.

HTTP, Get Token

Initialize Variable, Token

Confirm Success

Step 4: Prepare Dates Array

In this step, we will iterate through dates between StartDate and EndDate and append to the Dates array.

Initialize Variable, Counter

Initialize Variable, Dates

Do..Until

Append to Array Variable, Date

Increment Variable, Counter

Confirm Success

Step 5: Iterate through Subscriptions

In this step, we will create a “For Each” action for Subscriptions.

For Each, Subscription

HTTP, Get Resource Groups

Confirm Success

Step 6: Iterate through Resource Groups

In this step, we will create a “For Each” action for Resource Groups {aka Scopes}.

For Each, Resource Group

Set Variable, Scope

Confirm Success

Step 7: Iterate through Dates

In this step, we will nest “For Each” actions for Dates.

For Each, Date

Set Variable, Date

Confirm Success

Step 8: Get Cost Data

In this step, we will request and process data from the Cost Management API.

HTTP, Get Costs

Parse JSON

Confirm Success

Step 9: Ingest to Data Explorer

In this step, we will send the Cost Management API response to Data Explorer using an .ingest inline command.

For Each, Response Row

Set Variable, KQL

ADX Command, Ingest

Confirm Success

Step 10: Query Data

API, via Function App

image

This use case considers the following requirement statements:

Step 1: Prepare Resources

In addition to the items listed at the beginning of this documentation, this solution requires the following resources:

Step 2: Create Visual Studio Project

Step 3: “Function1.cs” Logic, Update Method

Step 4: Install NuGet, Microsoft.Azure.WebJobs.Extensions.EventHubs

Step 5: “Function1.cs” Logic, Add Variables and Parameters

Step 6: “Function1.cs” Logic, Iterate Subscriptions

Step 7: Install NuGet, Microsoft.Azure.Management…

Step 8: “Function1.cs” Logic, Iterate Resource Groups

Continue on the “Function1.cs” tab

Step 9: “Function1.cs” Logic, Iterate Dates and Request Cost Management Data

Step 10: “Function1.cs” Logic, Send to Event Hub

Function1.cs

The final end-to-end code in Function1.cs:

using Microsoft.Azure.Management.ResourceManager.Fluent;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;


namespace CostManagement
{
    public class Function1
    {
        [FunctionName("Function1")]
        public async Task Run(
            [TimerTrigger("*/1 * * * *")] TimerInfo theTimer,
            [EventHub("dest", Connection = "EventHubConnectionAppSetting")] IAsyncCollector<string> theEventHub,
            ILogger theLogger)
        {
            string clientId = "CLIENT_ID",
                clientsecret = "CLIENT_SECRET",
                tenantid = "TENANT_ID",
                startDate = "11/1/2022",
                endDate = "11/5/2022";

            string[] subscriptions = new string[] { "SUBSCRIPTION_ID1", "SUBSCRIPTION_ID2", "SUBSCRIPTION_ID3" };

            /* ************************* Iterate Subscriptions */

            foreach (string subscription in subscriptions)
            {
                var credentials = SdkContext.AzureCredentialsFactory.FromServicePrincipal(
                    clientId: clientId,
                    clientSecret: clientsecret,
                    tenantId: tenantid,
                    environment: AzureEnvironment.AzureGlobalCloud
                );

                var azure = Microsoft.Azure.Management.Fluent.Azure
                    .Authenticate(credentials)
                    .WithSubscription(subscriptionId: subscription)
                    ;

                /* ************************* Iterate Resource Groups */

                foreach (var resourceGroup in azure.ResourceGroups.List())
                {
                    /* ************************* Get Token */

                    string token;

                    using (HttpClient client = new HttpClient())
                    {
                        FormUrlEncodedContent content = new FormUrlEncodedContent(new[]{
                        new KeyValuePair<string, string>("grant_type", "client_credentials"),
                        new KeyValuePair<string, string>("client_id", clientId),
                        new KeyValuePair<string, string>("client_secret", clientsecret),
                        new KeyValuePair<string, string>("resource", "https://management.azure.com/")
                    });

                        content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");

                        HttpResponseMessage response = await client.PostAsync(
                            requestUri: new Uri("https://login.microsoftonline.com/" + tenantid + "/oauth2/token"),
                            content: content);

                        OAuth2? oauth2 = JsonSerializer.Deserialize<OAuth2>(response.Content.ReadAsStringAsync().Result);

                        token = oauth2.access_token;
                    }

                    /* ************************* Iterate Dates */

                    for (var date = DateTime.Parse(startDate); date <= DateTime.Parse(endDate); date = date.AddDays(1))
                    {
                        Console.WriteLine(Environment.NewLine + resourceGroup.Id + " >> " + date.ToString("MMMM dd yyyy"));

                        /* ************************* Request Cost Management Data */

                        using (HttpClient client = new HttpClient())
                        {
                            client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);

                            HttpRequestMessage request = new HttpRequestMessage(
                                method: new HttpMethod("POST"),
                                requestUri: "https://management.azure.com/" + resourceGroup.Id + "/providers/Microsoft.CostManagement/query?api-version=2021-10-01");

                            request.Content = new StringContent(
                                content: "{'dataset':{'aggregation':{'totalCost':{'function':'Sum','name':'PreTaxCost'}},'granularity':'Daily','grouping':[{'name':'ResourceGroupName','type':'Dimension'},{'name':'ResourceType','type':'Dimension'},{'name':'ResourceId','type':'Dimension'},{'name':'ResourceLocation','type':'Dimension'},{'name':'MeterCategory','type':'Dimension'},{'name':'MeterSubCategory','type':'Dimension'},{'name':'Meter','type':'Dimension'},{'name':'ServiceName','type':'Dimension'},{'name':'PartNumber','type':'Dimension'},{'name':'PricingModel','type':'Dimension'},{'name':'ChargeType','type':'Dimension'},{'name':'ReservationName','type':'Dimension'},{'name':'Frequency','type':'Dimension'}]},'timePeriod':{'from':'" + date + "','to':'" + date + "'},'timeframe':'Custom','type':'Usage'}",
                                encoding: Encoding.UTF8,
                                mediaType: "application/json");

                            HttpResponseMessage response = client.Send(request);

                            string output = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();

                            theLogger.LogInformation(output);

                            await theEventHub.AddAsync(response.Content.ReadAsStringAsync().GetAwaiter().GetResult());
                        }
                    }
                }
            }
        }
        public class OAuth2
        {
            public string? access_token { get; set; }
        }
    }
}

Step 11: Update “local.settings.json”

Reference