The Easy Way to Generate Cost Diagrams

Microsoft offers a plethora of options for exporting usage out of Azure. You can then slice and dice the raw data and generate some reports. What happens if I don’t need to do all of that? What if I just like the out-of-the-box report that is presented in the Azure portal? I can either save this report and share it via a URL, or I can download it as a PNG and share that.

The former is pretty straight-forward, but how can I automatically download the PNG and send that out?

This diagram has a nice area chart, aggregated costs of each service, locations, and enrollment accounts. You can change the scope to a specific subscription or set the root management group to get all your subscription costs. This is exactly what I would want as a quick 1-page summary.

If you look at the ribbon bar, you can see “Download” and the option to download it as a PNG.

Looking at our network calls in the dev tools, you can see a call to the publish API, then another URL is returned that we call to actually download the PNG. This is exactly how the portal is generating your PNG for you to download and save.

https://management.azure.com/providers/Microsoft.Management/managementGroups/<tenantId>/providers/Microsoft.CostManagement/publish?api-version=2021-04-01-preview

Looking at the payload, it is sending the following

{
  "properties": {
    "format": [
      1
    ],
    "validityDuration": 1,
    "properties": {
      "currency": null,
      "dateRange": "ThisMonth",
      "query": {
        "type": "ActualCost",
        "dataSet": {
          "granularity": "Daily",
          "aggregation": {
            "totalCost": {
              "name": "Cost",
              "function": "Sum"
            },
            "totalCostUSD": {
              "name": "CostUSD",
              "function": "Sum"
            }
          },
          "sorting": [
            {
              "direction": "ascending",
              "name": "UsageDate"
            }
          ]
        },
        "timeframe": "None"
      },
      "chart": "Area",
      "accumulated": "true",
      "pivots": [
        {
          "type": "Dimension",
          "name": "ServiceName"
        },
        {
          "type": "Dimension",
          "name": "ResourceLocation"
        },
        {
          "type": "Dimension",
          "name": "Subscription"
        }
      ],
      "scope": "providers/Microsoft.Management/managementGroups/<tenantId>",
      "kpis": [
        {
          "type": "Forecast",
          "enabled": true
        }
      ],
      "displayName": "AccumulatedCosts"
    }
  }
}

All we need to do now is craft up the PowerShell.

$azContext = Get-AzContext
$azProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile
$profileClient = New-Object -TypeName Microsoft.Azure.Commands.ResourceManager.Common.RMProfileClient -ArgumentList ($azProfile)


$token = $profileClient.AcquireAccessToken($azContext.Subscription.TenantId)

$authHeader = @{
    'Content-Type'='application/json'
    'Authorization'='Bearer ' + $token.AccessToken 
}

$body = @"
{
  "properties": {
    "format": [
      1
    ],
    "validityDuration": 1,
    "properties": {
      "currency": null,
      "dateRange": "ThisMonth",
      "query": {
        "type": "ActualCost",
        "dataSet": {
          "granularity": "Daily",
          "aggregation": {
            "totalCost": {
              "name": "Cost",
              "function": "Sum"
            },
            "totalCostUSD": {
              "name": "CostUSD",
              "function": "Sum"
            }
          },
          "sorting": [
            {
              "direction": "ascending",
              "name": "UsageDate"
            }
          ]
        },
        "timeframe": "None"
      },
      "chart": "Area",
      "accumulated": "true",
      "pivots": [
        {
          "type": "Dimension",
          "name": "ServiceName"
        },
        {
          "type": "Dimension",
          "name": "ResourceLocation"
        },
        {
          "type": "Dimension",
          "name": "Subscription"
        }
      ],
      "scope": "providers/Microsoft.Management/managementGroups/<tenantId>",
      "kpis": [
        {
          "type": "Forecast",
          "enabled": true
        }
      ],
      "displayName": "AccumulatedCosts"
    }
  }
}
"@

#copy the URL from your dev tools. Depending on your account type EA, MCA, Pay as you go, etc, this will be different for the URL.

$restUri = "https://management.azure.com/providers/Microsoft.Management/managementGroups/<tenantId>/providers/Microsoft.CostManagement/publish?api-version=2021-04-01-preview"

$response = Invoke-RestMethod -Uri $restUri -Method Post -Headers $authHeader -body $body 

Now that the PNG is being generated, the API will return a URL where to actually download the PNG when ready. We can get that URL from the response object as it is a synchronous call.

$response.properties.url

$date=(get-date).tostring('MM-dd-yyyy') 
invoke-restmethod -uri $response.properties.url -outfile "c:\temp\YearToDate-$($date).png"

We now have two options for sharing our report. We can generate a private URL that the user must view, or we can generate the PNG and do whatever we want with it. You can easily use the Azure portal to build the report you want then view the JSON body to submit in your payload!

Reactivate an Azure Subscription via API – Gov Cloud Edition

I recently had to reactivate an Azure subscription that was cancelled, but I noticed the instructions https://docs.microsoft.com/en-us/azure/cost-management-billing/manage/subscription-disabled#the-subscription-was-accidentally-canceled do not work in Azure Gov Cloud. There is no button to reactivate, so I was forced to submit a ticket to Microsoft and they fixed me up. Typically, if a subscription was cancelled, it was done by mistake and the end user needs access ASAP. I didn’t want to wait hours by submitting a ticket to Microsoft in the future, so I started figuring out how I could do this self service style in Azure gov.

I started to research the AZ CLI and PowerShell cmdlets, but nothing was coming up. As a last resort, I look at the API documentation and to my surprise, I found the POST call to enable a subscription https://docs.microsoft.com/en-us/rest/api/subscription/2019-03-01-preview/subscriptions/enable If you noticed, I linked to API version 2019-03-01-preview. The latest version of 2020-09-01 was not working in management.usgovcloudapi.net. I put a code snippet below:

$azContext = Get-AzContext
$azProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile
$profileClient = New-Object -TypeName Microsoft.Azure.Commands.ResourceManager.Common.RMProfileClient -ArgumentList ($azProfile)
$token = $profileClient.AcquireAccessToken($azContext.Subscription.TenantId)
 
$authHeader = @{
    'Content-Type'='application/json'
    'Authorization'='Bearer ' + $token.AccessToken 
}

#commercial uri management.azure.com
#gov uri management.usgovcloudapi.net
$restUri = "https://management.usgovcloudapi.net/subscriptions/$($subscriptionId)/providers/Microsoft.Subscription/enable?api-version=2019-10-01-preview"
Invoke-RestMethod -uri $restUri -Method POST -Headers $authHeader

In larger organizations, this code could be used towards Service Now automation, Azure Automation, Azure Functions, etc to get the client up and running faster. I hope this helps you with your Azure journey. 🙂

Azure Run Command via API

I had a scenario where I needed an end user to be able to run a few adhoc commands via Azure automation runbook and return the results. I am a big fan of Azure Automation as it has a nice display of the jobs and how it categorizes exceptions, warnings and output. The VM is running Ubuntu, but unfortunately, you cannot run adhoc commands using the Invoke-AzVmRunCommand cmdlet. You need to pass in a script 😦 I tried to do an inline script and also export it out then reference it in the runbook, but it would just display nothing. Knowing that az cli can run adhoc commands, I figured I would research the API.

I was getting no where with the Microsoft docs as the response was not the one I was getting. One simple trick I did was run the web browser developer tools and just monitor the API call being sent from the portal. In the picture below, you can see the API call and the JSON body which has a simple command of calling date. You can copy the API call directly from the devs tools in the specific format you want.

Now that I can make the call, I noticed it is sent asynchronous. Looking at the next call in my dev tools, I saw this URI being called with some GUIDs.

I tried to research this call https://docs.microsoft.com/en-us/rest/api/compute/operations/list#computeoperationlistresult but I didn’t see an explanation for the guid’s in the URI. What I did figure out is that the response from invoke-webrequest has a header key called Location and azure-asyncoperation which both have a URI that matches the call Azure was using in the portal. We can do a simple while loop to wait until the invoke-webrequest populates content which has our stdout from the runcommand. It will look something like this in an Azure runbook:

Connect-AzAccount -Identity

$azContext = Get-AzContext
$azProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile
$profileClient = New-Object -TypeName Microsoft.Azure.Commands.ResourceManager.Common.RMProfileClient -ArgumentList ($azProfile)

$token = $profileClient.AcquireAccessToken($azContext.Subscription.TenantId)

$auth = @{
    'Content-Type'  = 'application/json'
    'Authorization' = 'Bearer ' + $token.AccessToken 
}

$response = Invoke-WebRequest -useBasicParsing -Uri "https://management.azure.com/subscriptions/$($((Get-AzContext).Subscription.Id))/resourceGroups/ubuntu/providers/Microsoft.Compute/virtualMachines/ubuntu/runCommand?api-version=2018-04-01" `
-Method "POST" `
-Headers $auth `
-ContentType "application/json" `
-Body "{`"commandId`":`"RunShellScript`",`"script`":[`"date`"]}"

Foreach ($key in ($response.Headers.GetEnumerator() | Where-Object {$_.Key -eq "Location"}))
{
       $checkStatus = $Key.Value
}

$contentCheck = Invoke-WebRequest -UseBasicParsing -Uri $checkStatus -Headers $auth
while (($contentCheck.content).count -eq 0) {
$contentCheck = Invoke-WebRequest -UseBasicParsing -Uri $checkStatus -Headers $auth
Write-output "Waiting for async call to finish..."
Start-Sleep -s 15
}

($contentCheck.content | convertfrom-json).value.message

As you can see, I am using a managed identity and logging in with it. The runbook calls the runcommand with a POST then it hits a while loop to wait for it to finish then output the results.