Azure成本管理API PowerShell脚本获取Location及BlobLink失败排查
问题描述
我尝试通过Azure Cost Management API提取使用成本,先在Postman中测试了API的POST请求,流程正常:
POST https://management.azure.com/subscriptions/0000000-0000-0000-0000-00000000/providers/Microsoft.CostManagement/generateCostDetailsReport?api-version=2022-10-01 { "metric": "ActualCost", "timePeriod": { "start": "2020-03-01", "end": "2020-03-15" } }
根据文档,POST响应的Location头包含报表查询URI,GET该URI可获取包含Blob下载链接的响应体。但我写的PowerShell脚本批量遍历订阅时,$usageGet1Uri和$blobLink为空,导致Invoke-RestMethod和Invoke-WebRequest因Uri参数为空报错。
脚本内容:
# Step 1: Get mandatory parameters from the user $startDate = Read-Host "Enter the start date (YYYY-MM-DD):" $endDate = Read-Host "Enter the end date (YYYY-MM-DD):" # Step 2: Connect to Azure and get subscriptions Connect-AzAccount $subscriptions = Get-AzSubscription | Select-Object -ExpandProperty SubscriptionId # Step 3: Process each subscription foreach ($subscriptionId in $subscriptions) { # Generate cost details report $generateReportUri = "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.CostManagement/generateCostDetailsReport?api-version=2022-10-01" $reportRequest = @{ metric = "ActualCost" timePeriod = @{ start = $startDate end = $endDate } } $reportResponse = Invoke-RestMethod -Uri $generateReportUri -Method Post -Body ($reportRequest | ConvertTo-Json) -Headers @{ "Authorization" = "Bearer $((Get-AzAccessToken).Token)" } # Get usage report URI $usageGet1Uri = $reportResponse.Headers.Location # Download and rename the usage report file $usageReport = Invoke-RestMethod -Uri $usageGet1Uri -Method Get -Headers @{ "Authorization" = "Bearer $((Get-AzAccessToken).Token)" } $blobLink = $usageReport.Manifest.blobs.blobLink $subscriptionName = (Get-AzSubscription -SubscriptionId $subscriptionId).Name $fileName = "$subscriptionName.csv" Invoke-WebRequest -Uri $blobLink -OutFile $fileName Write-Host "Downloaded and renamed usage report for subscription '$subscriptionName' to '$fileName'." }
报错信息:
Invoke-RestMethod : Cannot validate argument on parameter 'Uri'. The argument is null or empty. Provide an argument that is not null or empty, and then try the command again. At C:\Users\XXXXXX\VScode\POST_method_try.ps1:29 char:43
- $usageReport = Invoke-RestMethod -Uri $usageGet1Uri -Method Get - ...
~~~~~~~~~~~~~
CategoryInfo : InvalidData: (:) [Invoke-RestMethod], ParameterBindingValidationException
FullyQualifiedErrorId : ParameterArgumentValidationError,Microsoft.PowerShell.Commands.InvokeRestMethodCommand
Invoke-WebRequest : Cannot validate argument on parameter 'Uri'. The argument is null or empty. Provide an argument that is not null or empty, and then try the command again. At C:\Users\XXXXXX\VScode\POST_method_try.ps1:34 char:28- Invoke-WebRequest -Uri $blobLink -OutFile $fileName
~~~~~~~~~
CategoryInfo : InvalidData: (:) [Invoke-WebRequest], ParameterBindingValidationException
FullyQualifiedErrorId : ParameterArgumentValidationError,Microsoft.PowerShell.Commands.InvokeWebRequestCommand
错误原因分析
Invoke-RestMethod不返回响应头:默认情况下Invoke-RestMethod只返回API的响应体,不会包含响应头信息,所以$reportResponse.Headers.Location必然为空,需要改用Invoke-WebRequest获取完整响应对象。- 异步操作未等待:生成成本报表是异步操作,POST请求返回
Location后不能立刻发起GET请求,必须轮询等待操作状态变为Succeeded,否则GET响应不会包含Blob链接。 - Blob链接路径错误:响应中
blobs是数组类型,直接取$usageReport.Manifest.blobs.blobLink无法正确获取值,需要指定数组索引。
修复后的脚本
# Step 1: Get mandatory parameters from the user $startDate = Read-Host "Enter the start date (YYYY-MM-DD):" $endDate = Read-Host "Enter the end date (YYYY-MM-DD):" # Step 2: Connect to Azure and get subscriptions Connect-AzAccount $subscriptions = Get-AzSubscription | Select-Object SubscriptionId, Name # Step 3: Reuse access token to avoid repeated calls $accessToken = (Get-AzAccessToken).Token $authHeader = @{ "Authorization" = "Bearer $accessToken" } # Step 4: Process each subscription foreach ($sub in $subscriptions) { $subscriptionId = $sub.SubscriptionId $subscriptionName = $sub.Name Write-Host "Processing subscription: $subscriptionName ($subscriptionId)" # Generate cost details report $generateReportUri = "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.CostManagement/generateCostDetailsReport?api-version=2022-10-01" $reportRequest = @{ metric = "ActualCost" timePeriod = @{ start = $startDate end = $endDate } } | ConvertTo-Json # Use Invoke-WebRequest to get full response including headers $postResponse = Invoke-WebRequest -Uri $generateReportUri -Method Post -Body $reportRequest -Headers $authHeader -ContentType "application/json" $usageGetUri = $postResponse.Headers["Location"] if (-not $usageGetUri) { Write-Host "Failed to get report status URI for $subscriptionName" continue } # Wait for the report generation to complete (poll every 10 seconds, max 30 attempts) $maxAttempts = 30 $attemptCount = 0 $reportStatus = "Running" while ($reportStatus -ne "Succeeded" -and $attemptCount -lt $maxAttempts) { Start-Sleep -Seconds 10 $attemptCount++ $statusResponse = Invoke-RestMethod -Uri $usageGetUri -Method Get -Headers $authHeader $reportStatus = $statusResponse.status Write-Host "Attempt $attemptCount/$maxAttempts - Report status: $reportStatus" if ($reportStatus -eq "Failed") { Write-Host "Report generation failed for $subscriptionName : $($statusResponse.error.message)" break } } if ($reportStatus -ne "Succeeded") { Write-Host "Report generation timed out for $subscriptionName" continue } # Get the final report data with blob link $finalReport = Invoke-RestMethod -Uri $usageGetUri -Method Get -Headers $authHeader # Blobs are stored in an array, take the first blob (adjust if multiple blobs exist) $blobLink = $finalReport.manifest.blobs[0].blobLink if (-not $blobLink) { Write-Host "No blob link found for $subscriptionName" continue } # Download the report $fileName = "$subscriptionName.csv" Invoke-WebRequest -Uri $blobLink -OutFile $fileName Write-Host "Successfully downloaded report for $subscriptionName to $fileName`n" }
修复说明
- 改用
Invoke-WebRequest发送POST请求,获取包含Location头的完整响应。 - 增加轮询逻辑,等待报表生成操作完成(状态变为
Succeeded),避免过早发起GET请求。 - 修正Blob链接提取路径,直接访问数组第一个元素(若有多个Blob可按需遍历)。
- 复用AccessToken,减少重复调用
Get-AzAccessToken的次数,提升效率。 - 增加错误判断逻辑,避免中间步骤失败导致后续报错。
内容的提问来源于stack exchange,提问作者Mantra Reborn
相关产品推荐
相关产品推荐

