FinOpsToolkitExt.BenefitRecommendations.psm1
|
function Get-BenefitRecommendation { <# .SYNOPSIS Queries the Azure Cost Management Benefit Recommendations API. .DESCRIPTION Retrieves savings plan purchase recommendations for a billing account. Uses Invoke-AzRestMethod for authentication (requires Connect-AzAccount). .PARAMETER BillingAccountId The billing account ID (e.g., "90846601"). .PARAMETER LookBackPeriod The look-back period for usage analysis. Default: Last60Days. .PARAMETER Term The commitment term. Default: P3Y. .PARAMETER Scope Recommendation scope. Default: Shared. .PARAMETER ExpandUsage Include hourly usage data in the response. .PARAMETER ExpandAllRecommendationDetails Include all recommendation detail tiers in the response. .EXAMPLE Get-BenefitRecommendation -BillingAccountId "90846601" -LookBackPeriod Last30Days -Term P3Y -ExpandUsage -ExpandAllRecommendationDetails #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$BillingAccountId, [ValidateSet('Last7Days', 'Last30Days', 'Last60Days')] [string]$LookBackPeriod = 'Last60Days', [ValidateSet('P1Y', 'P3Y')] [string]$Term = 'P3Y', [ValidateSet('Single', 'Shared')] [string]$Scope = 'Shared', [switch]$ExpandUsage, [switch]$ExpandAllRecommendationDetails ) $path = "/providers/Microsoft.Billing/billingAccounts/$BillingAccountId/providers/Microsoft.CostManagement/benefitRecommendations" $filter = "properties/lookBackPeriod eq '$LookBackPeriod' AND properties/term eq '$Term' AND properties/scope eq '$Scope'" $expandParts = @() if ($ExpandUsage) { $expandParts += 'properties/usage' } if ($ExpandAllRecommendationDetails) { $expandParts += 'properties/allRecommendationDetails' } $queryString = "api-version=2025-03-01&`$filter=$filter" if ($expandParts.Count -gt 0) { $queryString += "&`$expand=$($expandParts -join ',')" } $fullPath = "${path}?${queryString}" Write-Verbose "Calling: $fullPath" $result = Invoke-AzRestMethod -Method GET -Path $fullPath -ErrorAction Stop if ($result.StatusCode -ne 200) { throw "API returned $($result.StatusCode): $($result.Content)" } $response = $result.Content | ConvertFrom-Json $count = ($response.value | Measure-Object).Count Write-Host "Retrieved $count benefit recommendation(s) for billing account $BillingAccountId" -ForegroundColor Green return $response.value } function Publish-BenefitRecommendationToKusto { <# .SYNOPSIS Ingests benefit recommendations into an Azure Data Explorer table. .DESCRIPTION Takes benefit recommendation objects (from Get-BenefitRecommendation) and ingests them into the SavingsPlanRecommendations_raw table as a single row containing the full array in the Recommendation column. .PARAMETER Recommendations The recommendation array from Get-BenefitRecommendation. Accepts pipeline input. .PARAMETER ClusterUri The ADX cluster URI (e.g., "https://str-finops-hub.israelcentral.kusto.windows.net"). .PARAMETER Database The database name. Default: Hub. .PARAMETER Table The target table name. Default: SavingsPlanRecommendations_raw. .EXAMPLE Get-BenefitRecommendation -BillingAccountId "90846601" | Publish-BenefitRecommendationToKusto -ClusterUri "https://str-finops-hub.israelcentral.kusto.windows.net" #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [object]$Recommendations, [Parameter(Mandatory)] [string]$ClusterUri, [string]$Database = 'Hub', [string]$Table = 'SavingsPlanRecommendations_raw' ) begin { $collected = [System.Collections.Generic.List[object]]::new() } process { if ($Recommendations -is [System.Collections.IEnumerable] -and $Recommendations -isnot [string]) { foreach ($item in $Recommendations) { $collected.Add($item) } } else { $collected.Add($Recommendations) } } end { if ($collected.Count -eq 0) { Write-Warning "No recommendations to publish." return } $token = az account get-access-token --resource "https://kusto.kusto.windows.net" --query accessToken -o tsv if (-not $token) { throw "Failed to get Kusto access token. Run 'az login' first." } $jsonArray = $collected | ConvertTo-Json -Depth 100 -Compress $csl = ".set-or-append $Table <| print Recommendation=dynamic($jsonArray)" $body = @{ db = $Database csl = $csl } | ConvertTo-Json -Compress $mgmtUri = "$($ClusterUri.TrimEnd('/'))/v1/rest/mgmt" Write-Verbose "Ingesting $($collected.Count) recommendation(s) into $Database.$Table" Write-Verbose "URI: $mgmtUri" $response = Invoke-RestMethod -Uri $mgmtUri -Method Post -Headers @{ Authorization = "Bearer $token" 'Content-Type' = 'application/json; charset=utf-8' } -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) Write-Host "Published $($collected.Count) recommendation(s) to $Database.$Table" -ForegroundColor Green return $response } } |