如何通过 Azure billing API 按 Resource Group 统计费用?
结论
可以按 Resource Group 统计 Azure 费用,但不建议再使用旧版 Resource RateCard 和 Resource Usage 自行计算。
更稳妥的做法是调用 Azure Cost Management Query API,按 ResourceGroupName 分组并汇总 PreTaxCost。该接口返回 Azure 完成计价后的成本数据,通常更接近 Azure Portal 中 Cost Management/Billing 页面显示的结果。
旧接口可以通过 MeterId 关联:
Resource Usage中的meterId表示使用了哪种计量器。Resource RateCard中相同的MeterId提供该计量器的费率。- RateCard 不包含 Resource Group 和资源名称,这些信息要从 usage record 的资源 URI、实例数据或资源标识字段中获取。
不过,简单计算 usage × rate 往往无法准确还原账单。实际计费还涉及阶梯价格、免费额度、折扣、预留实例、Savings Plan、市场项目、舍入和账期调整等因素。
推荐方案:Cost Management Query API
下面的请求按订阅查询,并汇总各 Resource Group 的费用:
POST https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.CostManagement/query?api-version=2023-11-01
Authorization: Bearer {access-token}
Content-Type: application/json
请求体示例:
{
"type": "ActualCost",
"timeframe": "Custom",
"timePeriod": {
"from": "2026-08-01T00:00:00Z",
"to": "2026-08-29T23:59:59Z"
},
"dataset": {
"granularity": "None",
"aggregation": {
"totalCost": {
"name": "PreTaxCost",
"function": "Sum"
}
},
"grouping": [
{
"type": "Dimension",
"name": "ResourceGroupName"
}
]
}
}
典型响应由列定义和行数据组成:
{
"properties": {
"columns": [
{
"name": "PreTaxCost",
"type": "Number"
},
{
"name": "ResourceGroupName",
"type": "String"
},
{
"name": "Currency",
"type": "String"
}
],
"rows": [
[
128.42,
"customer-a-prod",
"USD"
],
[
37.18,
"customer-a-shared",
"USD"
]
]
}
}
列顺序不一定固定,应根据 columns 动态解析 rows:
import requests
subscription_id = "00000000-0000-0000-0000-000000000000"
access_token = "<access-token>"
url = (
f"https://management.azure.com/subscriptions/{subscription_id}"
"/providers/Microsoft.CostManagement/query"
"?api-version=2023-11-01"
)
payload = {
"type": "ActualCost",
"timeframe": "Custom",
"timePeriod": {
"from": "2026-08-01T00:00:00Z",
"to": "2026-08-29T23:59:59Z"
},
"dataset": {
"granularity": "None",
"aggregation": {
"totalCost": {
"name": "PreTaxCost",
"function": "Sum"
}
},
"grouping": [
{
"type": "Dimension",
"name": "ResourceGroupName"
}
]
}
}
response = requests.post(
url,
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
},
json=payload,
timeout=60
)
response.raise_for_status()
properties = response.json()["properties"]
column_names = [column["name"] for column in properties["columns"]]
records = [
dict(zip(column_names, row))
for row in properties["rows"]
]
for record in records:
print(
record.get("ResourceGroupName"),
record.get("PreTaxCost"),
record.get("Currency")
)
如果还要查看每天的费用趋势,可将:
"granularity": "None"
改为:
"granularity": "Daily"
响应中通常会多出日期列。
查询某个 Resource Group 的费用
可以添加维度过滤器:
{
"type": "ActualCost",
"timeframe": "Custom",
"timePeriod": {
"from": "2026-08-01T00:00:00Z",
"to": "2026-08-29T23:59:59Z"
},
"dataset": {
"granularity": "Daily",
"aggregation": {
"totalCost": {
"name": "PreTaxCost",
"function": "Sum"
}
},
"filter": {
"dimensions": {
"name": "ResourceGroupName",
"operator": "In",
"values": [
"customer-a-prod"
]
}
}
}
}
可用的维度名称和成本字段可能受订阅计费协议及查询范围影响。如果接口拒绝某个维度,应先查询该 scope 支持的 Cost Management dimensions,不要自行猜测字段名称。
获取 Resource Group 内每个资源的费用
除了 Resource Group,还可以按资源 ID 分组:
"grouping": [
{
"type": "Dimension",
"name": "ResourceGroupName"
},
{
"type": "Dimension",
"name": "ResourceId"
}
]
完整的 dataset 示例:
{
"granularity": "None",
"aggregation": {
"totalCost": {
"name": "PreTaxCost",
"function": "Sum"
}
},
"grouping": [
{
"type": "Dimension",
"name": "ResourceGroupName"
},
{
"type": "Dimension",
"name": "ResourceId"
}
]
}
ResourceId 通常类似:
/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/{provider}/{type}/{name}
通过它可以直接识别 Resource Group,也可以解析资源类型和资源名称。
旧版 RateCard 与 Usage 的关联方式
如果遗留系统必须使用旧接口,可以按下面的关系关联:
UsageRecord.meterId == RateCard.MeterId
RateCard 描述的是计量器价格,而不是具体资源。例如,同一个虚拟机计量器可能由多个虚拟机、多个 Resource Group 共同使用。因此,只凭 MeterId 无法确定:
- 资源名称
- Resource Group
- 完整资源 ID
- 哪条价格对应哪个资源实例
这些信息必须从 usage record 中获取。旧版 Usage API 的不同响应版本可能把它们放在 instanceData、resourceUri、resourceGroup 或类似字段中。有些旧格式中的 instanceData 还是经过转义的 JSON 字符串,需要再次解析。
处理流程大致如下:
from decimal import Decimal
rates_by_meter = {
rate["MeterId"]: rate
for rate in rate_card["Meters"]
}
for usage in usage_records:
meter_id = usage["meterId"]
rate = rates_by_meter.get(meter_id)
if rate is None:
continue
quantity = Decimal(str(usage["quantity"]))
unit_price = Decimal(str(rate["MeterRates"]["0"]))
estimated_cost = quantity * unit_price
print(meter_id, quantity, unit_price, estimated_cost)
这段代码只适用于单一费率。实际的 MeterRates 可能采用阶梯费率,例如:
{
"0": 0.10,
"100": 0.08,
"1000": 0.06
}
此时不能把全部用量乘以某个单价,而要按区间累计:
cost =
quantity_in_0_to_100 × 0.10
+ quantity_in_100_to_1000 × 0.08
+ quantity_above_1000 × 0.06
阶梯计价还可能按订阅、计量器和账期累计,而不是针对每条 usage record 单独计算。先逐条计算再求和,结果可能不准确。
为什么手工计算与 Portal 可能不一致
常见原因有:
- RateCard 价格不是客户的最终成交价。
- 企业协议、Microsoft Customer Agreement 或合作伙伴协议可能包含折扣。
- Azure Reservations 和 Savings Plan 会改变成本归属。
- 免费额度或计量器包含量需要按账期累计。
- 有些费用属于 subscription 级别,不能归属到 Resource Group。
- Marketplace、支持计划、税费、退款和账单调整可能单独记录。
- Portal 使用的日期范围、时区或成本类型可能不同。
- Azure 成本数据存在入账延迟,当天费用通常还不是最终值。
ActualCost与摊销后的AmortizedCost含义不同。- 已删除或移动的资源仍可能产生迟到费用。
如果要查看预留实例等采购费用按使用周期摊分后的成本,可以在计费协议支持的情况下尝试:
"type": "AmortizedCost"
与 Portal 对账时,需要保持以下条件一致:
- 相同 scope
- 相同日期范围和时区
- 相同货币
- 相同成本类型
- 相同过滤条件
- 相同的税费显示方式
- 相同的数据更新时间
权限与数据范围
调用 Cost Management API 的身份需要满足以下条件:
- 能获取 Azure Resource Manager access token;
- 对查询 scope 具有成本数据读取权限;
- 订阅或计费账户允许该身份查看成本。
拥有资源管理权限,不代表一定能读取费用。例如,某个服务主体即使能管理 Resource Group,也可能因 Cost Management 权限不足而收到 403。
并非所有费用都能归属到 Resource Group。对于没有 Resource Group 的费用,应保留一个类似 “未分配” 的分组,避免统计总额低于订阅总费用。
MeterId 可以把使用量记录关联到计量器费率,但不能识别具体资源。如果目标是获得接近 Azure Portal 的 Resource Group 成本数据,应使用 Cost Management Query API 返回的 ActualCost 或 AmortizedCost,不必重新实现 Azure 的计价逻辑。
备注:内容仅供参考。
