Microsoft Azure
Export your costs

Introduction

In order to follow and understand your expenses, azure provides a 'cost analysis' section in its portal.
If you wish to learn more about its use, here is the link to the documentation:
https://docs.microsoft.com/en-us/azure/cost-management-billing/costs/quick-acm-cost-analysis

The cost dashboard, provided by azure, is quite easy to use.
Azure even offers to integrate your aws costs so that you have a central console to view your cloud expenses.

Although the azure dashboard allows you to have a central view of your cloud spend, you may need to export this cost data for various reasons:
- because it does not allow you to integrate with any other cloud provider than aws
- because your company already has its own cost tracking solution (splunk dashboard, powerbi or other)
- because you need to present detailed figures to your manager in his favourite tool (excel...)

Summer update =)

https://azure.microsoft.com/en-us/blog/azure-cost-management-billing-updates-august-2020/

In order to simplify life for users who do not do a line of code, Microsoft has recently updated the exports in the Cost analysis section.

Among these new features, you now have the possibility to :
- export amortized costs (including your instance reservations)

- To make a single export by specifying a range (finally! 🙂

Command line data exports

Warning: if you use the exports generated automatically by Azure, you may find yourself at the beginning of the month with certain costs from the previous month, which may be difficult to integrate into your tools. Indeed, some costs are calculated out of date.
The other export methods consist of writing a few lines of code (in powershell, azure cli or the language of your choice by making api calls).
Contrary to the portal which proposes to export the amortized costs (and thus to integrate the reservation part in your export), you will have to go through several commands (or api calls) to recover on one side the azure usage costs and on the other the reservation costs.

User costs

I advise you to collect your usage costs with at least a three-day delay so that all the costs of your day appear.
The other solution is to make a second collection a week later in order to integrate the delta of costs.

In powershell:
You can use the command:

Get-AzConsumptionUsageDetail -StartDate $startDate -EndDate $endDate

The generated export will not contain a ResourceGroup property. You will be able to infer this information from the 'InstanceId' field.
Please note that you should not use the ResourceGroup parameter to retrieve the costs of your entire subscription via ResourceGroup. If you rely on the list of ResourceGroups existing at the time of your export, the deleted ResourceGroups will not appear in your report.

In az cli :

az consumption usage list --start-date 2020-08-01 --end-date 2020-08-31

En api :
Vous devrez faire de l’oauth2 pour utiliser les api azure.
Vous devez donc dans un premier temps récupérer un token depuis l’url login.microsoftonline.com,
Puis avec ce token, faire un GET sur l’url
https://management.azure.com/subscriptions/{subscription_id}/providers/Microsoft.Consumption/usageDetails?api-version=2019-10-01

api doc: https://docs.microsoft.com/fr-fr/rest/api/consumption/usagedetails/list

Booking metrics

Aparté
A quick aside to explain how reservations work.
A reservation allows you to reserve a type of VM instance over 1 or 3 years which allows you to save on the cost of this instance.
A reservation does not mean a machine. Continue to turn off your machines if you don't need them to save money. The machines that start up will draw from the available instances corresponding to their type (provided that the scope is not restricted to a ResourceGroup with a single VM, or that the reservation is of type Capacity priority)
When you buy reservations, you will be charged by the month (whether you use the instances or not).
In order to be able to access the reservation data, you need at least :

  • reader access on each reservation
  • An owner access on subscription

Even if you are the owner of the subscription, you will also need read access to the reservations.
Note that the contributor right is sufficient to list the orderid (id corresponding to the reservation order), but you will need to be the owner of the subscription to access the reservation metrics.

In powershell:
prerequisite, install the az.reservations module
Since reservations have a fixed cost, only the usage statistics of these reservations are provided via powershell commands.
It is necessary to retrieve the reservation costs and usage metrics to calculate the reservation costs per vm.
Here is a script allowing you to generate your reservation costs export in csv format:
(Please note, you must have the rights to the reservations in your subscription. If you do not have access to all the reservations, indicate the reservations to which you do not have access using the parameter ExceptionList_ReservationOrderId )

<#
.SYNOPSIS
  Ce script permet d'exporter les coûts de réservation azure au format csv.
.DESCRIPTION
  Ce script génère un export des coûts de réservation en fonction:
   - du paramètre SpecificDate qui est la date sur laquelle les exports de coûts seront générés
   - du paramètre SplitReservationCosts qui exporte de la manière suivante
        - le coût exacte par vm si le paramètre SplitReservationCosts est à false.
        - le coût réparti par vm en fonction de leur pourcentage d'utilisation de la réservation.
.EXAMPLE
  PS C:\> .\Get-AzResCost.ps1 -SpecificDate '2020/08/15' -ReportFile reservation.csv
  génère un export des réservation en date du '2020/08/15'
#>
[CmdletBinding()]
Param
(
  # Jour sur lequel les coûts de réservation doivent être calculés. Format : AAAA/MM/JJ.
  [Parameter(Position=0, Mandatory = $true)]
  [DateTime]$SpecificDate,
  # Nom du fichier contenant l'export.
  [Parameter(Position=1, Mandatory = $true)]
  [ValidateNotNullOrEmpty()]
  [ValidateScript({
    If ($_ -like '*.csv') {
      $True
    }
    else {
      Throw "le fichier doit est de type .csv"
    }
  })]
  $ReportFile = 'Export-AzureReservationCosts.json',
  # Si ce paramètre est à $true, les coûts de réservations sont répartis en fonction des différentes vm qui les utilisent; même si la réservation n'est pas utilisée à 100%.
  # Si ce paramètre est à $false, les coûts sont calculés en fonction de l'utilisation exacte des réservations par les vm.
  $SplitReservationCosts = $true,
  # List d'exception des ReservationOrderId sur lesquels vous n'avez pas l'accès. exemple: $ExceptionList_ReservationOrderId = ('id1','id2',...).
  $ExceptionList_ReservationOrderId = $null
)
 
#region ---variables---
#Requires -version 5
#Requires -module Az.Accounts,Az.Reservations
$ErrorActionPreference = 'Stop'
$TimeStamp = get-date -format "yyyyMMddHHmmss"
$Log_file = "$PSScriptRoot\logs\$TimeStamp`_Get-AzResCost.log"
$startDate = Get-Date $SpecificDate -Hour 0 -Minute 0 -Second 0
$endDate = Get-Date $SpecificDate -Hour 23 -Minute 59 -Second 59
$context = get-azcontext
# Objet exporté.
$result = @()
#endregion ---variables---
Try {
  # Création du fichier de log.
  [void](New-Item $Log_file -ItemType "file" -Force)
  # demande d'authentification
  if ($null -eq $context)
  {    
         throw "connectez-vous à l'aide de la commande connect-azaccount, puis sélectionnez votre souscription"
  }
  # $res_orderid : Correspond au différents id de réservation.
  foreach ($res_orderid in ((Get-AzReservationOrderId).AppliedReservationOrderId -replace ('/providers/Microsoft.Capacity/reservationorders/','')) )
  {
    write-verbose "orederid : $res_orderid"
    # $res_order : Correspond à l'ordre de réservation de $res_orderid (contient entre autre la date de réservation ainsi que la durée).
    
    $res_order = Get-AzReservationOrder -ReservationOrderId $res_orderid
    # $res :  Contient les détails des réservations (le sku, le scope, la quantité, le type de resource...).
    foreach ($res in (Get-AzReservation -ReservationOrderId $res_orderid))
    {
      Write-Verbose "reservation : $($res.DisplayName)"
      # $ParamsCostReservation : Definition des paramètres pour la commande 'Get-AzReservationQuote'.
      $ParamsCostReservation = @{
        ReservedResourceType = $res.ReservedResourceType;
        Sku = $res.Sku;
        BillingScopeId = "$($res.AppliedScopes)";
        Term  = $res_order.Term;
        Quantity = $res.Quantity;
        AppliedScopeType = $res.AppliedScopeType;
        DisplayName = $res.DisplayName;
        AppliedScope = $res.AppliedScopes.split('/')[-1];
        Location = $res.Location;
      }
      
      # $CostReservation : Correspond au coût de la réservation.
      $CostReservation = Get-AzReservationQuote @ParamsCostReservation 
      
      # $ReservationYears : Correspond au nombre d'année de réservation.
      $ReservationYears = $res.SkuDescription.split(',').where{$_ -match 'Years'}.replace('Years','').trim()
      
      # $ReservationDays : Correspond au nombre de jours de réservations (en fonction des années bissextiles).
      $ReservationDays = ($res.EffectiveDateTime.AddYears($ReservationYears) - $res.EffectiveDateTime).Days
      
      # $ReservationDays : Correspond au coût ramené à la journée.
      $daycost = [int]$CostReservation.BillingCurrencyTotal.split("`n").where{$_ -match 'Amount'}.split(':')[-1].trim() / [int]$ReservationDays
      Write-Verbose "daycost : $daycost"
      
      # $res_detail : Cntient les détailles de réservations par machine sur la journée.
      $res_detail = Get-AzConsumptionReservationDetail -ReservationOrderId $res_orderid -StartDate $startDate -EndDate $endDate
      
      # $res_purcent : Définit le pourcentage d'utilisation de la réseervation sur la journée.
      $unused_res_purcent = 0
      if ((Get-AzConsumptionReservationSummary -Grain daily -ReservationOrderId $res_orderid -StartDate $startDate -EndDate $endDate).MinUtilizationPercentage -ne 100)
      {
        $res_purcent = 0
        $res_detail | %{$res_purcent += $_.UsedHour / $_.TotalReservedQuantity *100 /24 }
        $unused_res_purcent = 100 - $res_purcent
      }
      else
      {
        $res_purcent = 100
      }
      if ($SplitReservationCosts -eq $false)
      {
        $res_purcent = 100
      }
      
      # $CostReservationObject : Objet contenant le coût de réservation par machine.
      foreach ($res_d in $res_detail)
      {
        $CostReservationObject = [PSCustomObject]@{
          ConsumedService = $res_d.Type
          Id = $res_d.Id
          InstanceName = $res_d.InstanceId.split('/')[-1]
          InstanceId = $res_d.InstanceId
          InstanceLocation = $res.Location
          PretaxCost = (($res_d.UsedHour / $res_d.TotalReservedQuantity) /24) * (100 / [double]$res_purcent) * [double]$daycost
          Product = $res.DisplayName + ' - ' + $res.SkuDescription
          SubscriptionGuid = $context.subscription.Id
          SubscriptionName = $context.subscription.Name
          UsageStart = (get-date $res_d.UsageDate -UFormat %s) + '000'
          UsageEnd = (get-date $res_d.UsageDate -Hour 23 -Minute 59 -Second 59 -UFormat %s).split('.')[0] + '000'
        }
        $result += $CostReservationObject
      }
      # ajout d'une entrée pour les coûts de réservation non utilisées
      if ($SplitReservationCosts -eq $false -and $unused_res_purcent -ne 0)
      { 
        $CostReservationObject = [PSCustomObject]@{
          ConsumedService = 'reservation'
          Id = ''
          InstanceName = 'unused resservation'
          InstanceId = ''
          InstanceLocation = ''
          PretaxCost =  ($unused_res_purcent / 100) * [double]$daycost
          Product = ''
          SubscriptionGuid = $context.subscription.Id
          SubscriptionName = $context.subscription.Name
          UsageStart = (get-date $startDate -UFormat %s) + '000'
          UsageEnd = (get-date $endDate -UFormat %s).split('.')[0] + '000'
        }
        $result += $CostReservationObject
      }
    }
  }
  
  # Génération de l'export.
  $result | ConvertTo-csv -Delimiter ';' -NoTypeInformation | out-file -Encoding UTF8 -FilePath $($PSScriptRoot + '\' + $TimeStamp + '_' + $ReportFile)
  write-host "emplacement de l'export : $($PSScriptRoot + '\' + $TimeStamp + '_' + $ReportFile)"
}
Catch {
  $ErrorMessage = $_.Exception.Message
  $ErrorLine = $_.InvocationInfo.ScriptLineNumber
  Write-Output "Get-AzResCost : Error on line $ErrorLine. The error message was: $ErrorMessage" | Tee-Object -Append $Log_file | Write-Error
}
finally {
  Get-ChildItem *.log -Path "$PSScriptRoot\logs" | Sort-Object CreationTime -Descending | Select-Object  -Skip 10 | Remove-Item
}

 

In az cli:

Here are the equivalent commands in az cli:

az reservations reservation-order-id list --subscription-id <subscription_id> 
az reservations reservation list --reservation-order-id <OrderID> 
az reservations reservation-order calculate --sku <sku> … 
az consumption reservation detail list --start-date AAAA-MM-DD --end-date AAAA-MM-DD --reservation-order-id <order-id> --reservation-id <reservation-id>

 

In API:

Liste des order_id: GET sur https://management.azure.com/subscriptions/{subscription_id}/providers/Microsoft.Capacity/appliedReservations?api-version=2019-04-01

Commande d’un order_id: GET sur https://management.azure.com/providers/Microsoft.Capacity/reservationOrders/{order_id}?api-version=2019-04-01

Liste des réservations d’une commande : GET sur https://management.azure.com/providers/Microsoft.Capacity/reservationOrders/{order_id}/reservations?api-version=2019-04-01

Booking fee: POST to https://management.azure.com/providers/Microsoft.Capacity/calculatePrice?api-version=2019-04-01

Body:

{
  "sku": {
    "name": "{sku}"
  },
  "location": "{location}",
  "properties": {
    "reservedResourceType": "VirtualMachines",
    "billingScopeId": "/subscriptions/{subscriptionid}",
    "term": "{term}",
    "quantity": {quantity},
    "displayName": "{res_displayName}",
    "appliedScopeType": "{ScopeType}",
    "appliedScopes": [
      "{subscriptionid}"
    ],
    "reservedResourceProperties": {}
  }
}

Métriques de réservation: GET sur https://management.azure.com/providers/Microsoft.Capacity/reservationorders/{res_order_id}/providers/Microsoft.Consumption/reservationDetails?$filter=properties%2FUsageDate ge {AAAA-MM-JJ} AND properties%2FUsageDate le {AAAA-MM-JJ}&api-version=2018-01-31

Conclusion

I hope this post will be useful to you.
For those of you who would also like to do some costing, the following article, if not helpful, should make you smile:

https://www.commitstrip.com/fr/2020/09/16/the-cloud-its-expensive/?

 

About the author

Article written by Vincent Fléchier
15 years of expertise in IT, including 3 years in the Azure Cloud, Vincent is a certified Azure Cloud Architect.
His specialities: powershell, ansible, azure...