PAYATHON 2026

如何通过 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 的计价逻辑。

备注:内容仅供参考。