Exporter vos coûts azure

Introduction

Afin de suivre et comprendre vos dépenses, azure met à disposition dans son portail une section ‘cost analysis’.
Si vous souhaitez vous documenter sur son utilisation, voici le lien de la documentation:
https://docs.microsoft.com/en-us/azure/cost-management-billing/costs/quick-acm-cost-analysis

Le dashboard de coût, fourni par azure, se prend assez facilement en main.
Azure vous propose même d’intégrer vos coûts aws afin d’avoir une console centrale pour visualiser vos dépenses cloud.

Bien que le dashboard azure vous permette d’avoir une vue centrale de vos dépenses cloud, vous pourriez à l’inverse avoir besoin d’exporter ces données de coûts pour diverses raisons :
– parce qu’il ne permet pas d’intégrer un autre cloud provider qu’aws
– parce que votre entreprise possède déjà sa propre solution de suivi de coûts (dashboard splunk, powerbi ou autre)
– parce que vous avez besoin de présenter des chiffres détaillés à votre responsable dans son outil favori (excel…)

Mise à jour estivale =)

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

Afin de simplifier la vie des utilisateurs qui ne font pas une ligne de code, Microsoft a récemment mis à jour les exports dans la section Cost analysis.

Parmis ces nouveautés, vous avez à présent la possibilité :
–  d’exporter les coûts amortis (incluant vos réservations d’instances)

–   De faire une exportation unique en spécifiant une plage (enfin! 🙂

Exports des données en ligne de commande

Attention : si vous utilisez les exports générés automatiquement par azure, vous risquez de vous retrouver en début de mois avec certains coûts du mois précédent, ce qui peut être pénible à intégrer dans vos outils. En effet, certains coûts sont calculés en décalé.
Les autres méthodes d’exportation consistent à écrire quelques lignes de code (en powershell, azure cli ou le langage de votre choix en faisant des appels api).
Contrairement au portail qui vous propose d’exporter les coûts amortis (et donc d’intégrer la partie réservation à votre export),  il vous faudra passer par plusieurs commandes (ou appels api) pour récupérer d’un côté les coûts d’utilisation azure et de l’autre les coûts de réservation.

Les coûts d’utilisation

Je vous conseille de récupérer vos coûts d’utilisation avec au moins trois jours de décalage afin que l’ensemble des coûts de votre journée apparaissent.
L’autre solution consiste à faire une seconde collecte une semaine plus tard afin d’intégrer le delta de coûts.

En powershell:
Vous pouvez utiliser la commande:

Get-AzConsumptionUsageDetail -StartDate $startDate -EndDate $endDate

 

L’export généré ne contiendra pas de propriété ResourceGroup. Vous pourrez déduire cette information du champ ‘InstanceId’.
Attention, n’utilisez pas le paramètre ResourceGroup pour récupérer par ResourceGroup les coûts de toute votre souscription. En effet, si vous vous appuyez sur la liste des ResourceGroup  existant au moment de votre export, les ResourceGroup  supprimés n’apparaîtront pas dans votre rapport.

En 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

Doc de l’api: https://docs.microsoft.com/fr-fr/rest/api/consumption/usagedetails/list

Les métriques de réservation

Aparté
Petit aparté pour expliquer comment fonctionnent les réservations.
Une réservation vous permet de réserver un type d’instance de VM sur 1 ou 3 ans ce qui vous permet d’économiser sur le coût de cette instance.
Une réservation ne veut pas dire une machine. Continuez à éteindre vos machines si vous n’en avez pas besoin afin de faire des économies. Les machines qui démarreront piocheront dans les instances disponibles correspondant à leur type (à condition que le scope ne soit pas restreint à un ResourceGroup avec une seule VM, ou encore que la réservation soit de type Capacity priority)
Lorsque vous achetez des réservations, le coût vous sera refacturé au mois (que vous utilisiez ou non ces instances).
Afin de pouvoir accéder aux données de réservation, il vous faut au minimum :

  • un accès reader sur chacune des réservations
  • Un accès owner sur la souscription

Même en étant owner de la souscription, il vous faudra en plus un accès en lecture sur les réservations.
Attention, le droit contributeur est suffisant pour lister les orderid (id correspondant à l’ordre de réservation), mais il vous faudra être owner de la souscription pour accéder aux métriques de réservation.

En powershell:
prérequis, installer le module az.reservations
Les réservations ayant un coût fixe, seules les statistiques d’utilisation de ces réservations sont fournies via les commandes powershell.
Il est nécessaire de récupérer les coûts de réservation et les métriques d’utilisation pour calculer les coûts de réservation par vm.
Voici un script vous permettant de générer votre export des coûts de réservation au format csv :
(Attention, vous devez avoir les droits sur les réservations de votre souscription. Si vous n’avez pas l’accès à l’ensemble des réservations, indiquez les réservations auxquelles vous n’avez pas l’accès à l’aide paramètre 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
}

 

En az cli:

Voici les commandes équivalentes en 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>

 

En 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

Coût de réservation : POST sur 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

J’espère que ce post vous sera utile.
Pour ceux qui en plus souhaiteraient faire des coûts prévisionnels, l’article suivant, à défaut de vous aider, devrait vous faire sourire :

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

 

A propos de l’auteur

Article écrit par Vincent Fléchier
15 ans d’expertise dans l’IT dont 3 dans le Cloud Azure, Vincent est certifié Azure Cloud Architect.
Ses spécialistés: powershell, ansible, azure…

2020-09-29T11:43:28+02:00septembre 28th, 2020|BLOG, Microsoft Azure|