PAYATHON 2026

如何通过 Azure Billing API/SDK 获取成本分析数据

支付阿杰

结论

要获取与 Azure Portal Cost analysis 接近的按资源成本数据,建议直接使用 Azure Cost Management Query API,按 ResourceId 分组并汇总 PreTaxCost。一般不需要自行拼接 Usage Aggregate 和 RateCard 来计算费用。

Usage Aggregate 与 RateCard 主要通过 meterId 关联。理论上,可以结合用量、计价阶梯和费率计算成本。但实际账单还会受到协议价格、折扣、预留实例、Savings Plan、免费额度、舍入、税费和币种转换等因素影响。因此,直接使用下面的公式,往往无法复现 Cost analysis 的结果:

cost = quantity × rate

推荐方案:Cost Management Query API

在 Subscription 范围内,请求地址为:

POST https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.CostManagement/query?api-version=2023-11-01

下面的请求体按 ResourceId 汇总实际成本:

{
  "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": "ResourceId"
      }
    ]
  }
}

如果需要查看每个资源每天的成本,将 granularity 改为:

"granularity": "Daily"

还可以加入资源类型、资源组或服务名称等维度:

"grouping": [
  {
    "type": "Dimension",
    "name": "ResourceId"
  },
  {
    "type": "Dimension",
    "name": "ResourceType"
  },
  {
    "type": "Dimension",
    "name": "ResourceGroupName"
  }
]

可用维度取决于计费范围、订阅类型和实际数据。如果 API 提示维度无效,可以先查询当前 scope 支持的 dimensions,也可以减少分组字段,逐个测试。

Python REST API 示例

安装身份认证组件:

pip install azure-identity requests

以下代码可以使用 Azure CLI 登录身份、托管身份或服务主体获取令牌:

from datetime import datetime, timezone
import requests
from azure.identity import DefaultAzureCredential

subscription_id = "00000000-0000-0000-0000-000000000000"

credential = DefaultAzureCredential()
token = credential.get_token(
    "https://management.azure.com/.default"
).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": "ResourceId",
            },
            {
                "type": "Dimension",
                "name": "ResourceType",
            },
        ],
    },
}

response = requests.post(
    url,
    headers={
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
    },
    json=payload,
    timeout=60,
)
response.raise_for_status()

result = response.json()
properties = result["properties"]

columns = [column["name"] for column in properties["columns"]]

for row in properties["rows"]:
    record = dict(zip(columns, row))
    print(
        record.get("ResourceId"),
        record.get("ResourceType"),
        record.get("PreTaxCost"),
        record.get("Currency"),
    )

不要假设响应列的顺序固定。应先读取 properties.columns,再按位置与 properties.rows 对应。

典型的响应结构如下:

{
  "properties": {
    "columns": [
      {"name": "PreTaxCost", "type": "Number"},
      {"name": "ResourceId", "type": "String"},
      {"name": "ResourceType", "type": "String"},
      {"name": "Currency", "type": "String"}
    ],
    "rows": [
      [
        12.34,
        "/subscriptions/.../resourceGroups/rg-demo/providers/Microsoft.Compute/virtualMachines/vm01",
        "microsoft.compute/virtualmachines",
        "USD"
      ]
    ]
  }
}

实际列名和顺序由查询条件及计费数据决定,应以 API 返回的 columns 为准。

使用 Python SDK

对应的 SDK 包为:

pip install azure-identity azure-mgmt-costmanagement

示例代码:

from azure.identity import DefaultAzureCredential
from azure.mgmt.costmanagement import CostManagementClient
from azure.mgmt.costmanagement.models import (
    QueryAggregation,
    QueryDataset,
    QueryDefinition,
    QueryGrouping,
    QueryTimePeriod,
)

subscription_id = "00000000-0000-0000-0000-000000000000"
scope = f"/subscriptions/{subscription_id}"

credential = DefaultAzureCredential()
client = CostManagementClient(credential)

query = QueryDefinition(
    type="ActualCost",
    timeframe="Custom",
    time_period=QueryTimePeriod(
        from_property="2026-08-01T00:00:00Z",
        to="2026-08-29T23:59:59Z",
    ),
    dataset=QueryDataset(
        granularity="None",
        aggregation={
            "totalCost": QueryAggregation(
                name="PreTaxCost",
                function="Sum",
            )
        },
        grouping=[
            QueryGrouping(type="Dimension", name="ResourceId"),
            QueryGrouping(type="Dimension", name="ResourceType"),
        ],
    ),
)

result = client.query.usage(scope=scope, parameters=query)

column_names = [column.name for column in result.columns]

for row in result.rows or []:
    record = dict(zip(column_names, row))
    print(
        record.get("ResourceId"),
        record.get("ResourceType"),
        record.get("PreTaxCost"),
        record.get("Currency"),
    )

不同版本的 azure-mgmt-costmanagement 可能使用略有差异的模型构造参数。如果本地 SDK 提示参数名称错误,请以当前安装版本生成的类型签名为准。相比之下,REST API 请求体通常更容易排查。

如果需要逐条用量明细

如果要查看每条计费记录,而不是汇总报表,可以使用 Azure Consumption Usage Details API。记录通常包含以下信息:

  • resourceId
  • meterId
  • quantity
  • unitPrice
  • cost
  • usageStart、usageEnd
  • resourceGroup
  • instanceName
  • chargeType

获取明细后,可以直接按 resourceId 汇总返回的成本字段:

from collections import defaultdict

cost_by_resource = defaultdict(float)

for item in usage_details:
    properties = item.get("properties", {})
    resource_id = properties.get("resourceId") or "(unassigned)"
    cost = properties.get("cost") or properties.get("pretaxCost") or 0
    cost_by_resource[resource_id] += float(cost)

Usage Details 的数据量可能很大,因此需要处理 API 返回的分页链接。如果要进行周期性分析或处理大量历史数据,更适合配置 Cost Management Exports,定期把成本明细导出到 Storage Account,再通过 Python、数据库或数据仓库处理。

Usage Aggregate 与 RateCard 如何关联

如果确实需要自行估算,可以按下面的流程处理:

  1. 从 Usage Aggregate 获取每条用量记录。
  2. 读取记录中的 meterId、quantity、时间和资源信息。
  3. 在 RateCard 中找到 meterId 相同的 meter。
  4. 根据 meterRates 处理阶梯价格。
  5. 按 resourceUri 或同等的资源标识汇总计算结果。
  6. 分别处理币种、折扣、免费额度、预留资源和其他调整项。

阶梯价格不能一律按单一费率计算。例如:

meterRates:
0:   0.10
100: 0.08

当用量为 150 时,可能需要这样计算:

100 × 0.10 + 50 × 0.08

具体的阶梯含义和计价方式仍须以相应计费协议及 meter 定义为准。RateCard 给出的费率也未必是最终结算价格。因此,这种方法适合估算成本或理解 meter 的计价方式,不适合需要与账单严格对账的场景。

为什么部分成本没有 ResourceId

Cost Management 查询结果中的 ResourceId 可能为空,这不一定表示查询有误。常见原因包括:

  • Marketplace 购买项目
  • Support 或订阅级费用
  • Reservation 或 Savings Plan 购买费用
  • 税费、退款及账单调整
  • 某些共享服务或经典资源
  • 尚未关联到具体 Azure Resource Manager 资源的成本记录

展示结果时,建议将这类记录归入”未分配成本”或”订阅级成本”,不要直接丢弃。

与 Portal 数据对齐时的注意事项

要复现 Portal 中的数值,需要让以下条件保持一致:

  • 时间范围和时区。
  • Scope,例如 subscription、resource group 或 billing account。
  • 成本类型。ActualCost 与 AmortizedCost 的结果可能相差很大。
  • 筛选条件和分组维度。
  • 币种和计费周期。
  • 成本数据的入账延迟。
  • 税前成本、税后金额和发票总额的统计口径。
  • 预留实例与 Savings Plan 对实际成本和摊销成本的不同展示方式。

按资源展示成本时,建议使用 Cost Management Query API,以 ResourceId 分组并汇总 PreTaxCost。需要逐笔追溯时,再使用 Usage Details 或 Cost Management Exports,无需通过 RateCard 重算最终账单。

备注:内容仅供参考。