<# .SYNOPSIS Shows what each resource group in an Azure subscription has cost over a date range. .DESCRIPTION Calls the Azure Cost Management Query API through Invoke-AzRestMethod and totals actual cost by resource group, biggest spender first. Optionally exports to CSV. .PARAMETER SubscriptionId The subscription to report on. Defaults to the subscription in your current Az context. .PARAMETER Days How many days back to look. Default: 30. .PARAMETER CostColumn PreTaxCost for most pay-as-you-go and EA subscriptions; Cost for Microsoft Customer Agreement. .PARAMETER CsvPath Optional path to save the results as a CSV file. .EXAMPLE .\Get-AzCostByResourceGroup.ps1 -Days 7 .EXAMPLE .\Get-AzCostByResourceGroup.ps1 -SubscriptionId 00000000-0000-0000-0000-000000000000 -CsvPath .\costs.csv #> [CmdletBinding()] param( [string]$SubscriptionId, [ValidateRange(1, 365)][int]$Days = 30, [ValidateSet('PreTaxCost', 'Cost')][string]$CostColumn = 'PreTaxCost', [string]$CsvPath ) $ErrorActionPreference = 'Stop' # Sign in only if there's no existing session. if (-not (Get-AzContext)) { Connect-AzAccount | Out-Null } if (-not $SubscriptionId) { $SubscriptionId = (Get-AzContext).Subscription.Id } $to = (Get-Date).Date $from = $to.AddDays(-$Days) # One query: actual cost, summed, grouped by resource group. $body = @{ type = 'ActualCost' timeframe = 'Custom' timePeriod = @{ from = $from.ToString('yyyy-MM-dd'); to = $to.ToString('yyyy-MM-dd') } dataset = @{ granularity = 'None' aggregation = @{ totalCost = @{ name = $CostColumn; function = 'Sum' } } grouping = @(@{ type = 'Dimension'; name = 'ResourceGroupName' }) } } | ConvertTo-Json -Depth 6 $uri = "https://management.azure.com/subscriptions/$SubscriptionId/providers/Microsoft.CostManagement/query?api-version=2023-11-01" $rows = @() do { # Cost Management throttles hard. Back off and retry a few times on HTTP 429. for ($attempt = 1; $attempt -le 4; $attempt++) { $response = Invoke-AzRestMethod -Uri $uri -Method POST -Payload $body if ($response.StatusCode -ne 429) { break } Write-Warning "Cost Management is throttling requests. Waiting 30 seconds (attempt $attempt of 4)..." Start-Sleep -Seconds 30 } if ($response.StatusCode -ge 400) { throw "Cost query failed ($($response.StatusCode)): $($response.Content)" } $result = $response.Content | ConvertFrom-Json $columns = @($result.properties.columns.name) $rows += $result.properties.rows $uri = $result.properties.nextLink } while ($uri) $costIndex = [array]::IndexOf($columns, $CostColumn) $groupIndex = [array]::IndexOf($columns, 'ResourceGroupName') $currIndex = [array]::IndexOf($columns, 'Currency') $report = foreach ($row in $rows) { [pscustomobject]@{ ResourceGroup = if ($row[$groupIndex]) { $row[$groupIndex] } else { '(no resource group)' } Cost = [math]::Round([double]$row[$costIndex], 2) Currency = $row[$currIndex] } } $report = @($report | Sort-Object Cost -Descending) $total = ($report | Measure-Object -Property Cost -Sum).Sum Write-Host ("Total for the last {0} days: {1:N2} {2}" -f $Days, $total, $report[0].Currency) if ($CsvPath) { $report | Export-Csv -Path $CsvPath -NoTypeInformation Write-Host "Saved to $CsvPath" } # Return real objects, so you can pipe them: | Where-Object Cost -gt 100 $report