PAYATHON 2026

如何使用 Azure Billing APIs 计算 Azure 成本?

支付老李

结论

可以直接计算,方法如下:

  1. 用 meterId 匹配 ResourceUsage 中的用量记录和 RateCard 中的计价器。
  2. 按计费周期汇总每个计价器的 quantity。
  3. 从汇总用量中扣除该计价器的 includedQuantity。
  4. 根据 meterRates 逐档计费,不能简单地使用 quantity × 第一个单价。
  5. 将所有计价器的费用相加。

计算关系为:

EstimatedCost = Σ TierCost(BillableQuantity, meterRates)

BillableQuantity = max(0, TotalQuantity - includedQuantity)

这样算出的是基于公开费率的估算成本,不一定与 Azure 发票金额一致。企业协议折扣、预留实例、Savings Plan、Azure Hybrid Benefit、Marketplace、税费、信用额度、退款和人工调整等项目,通常无法仅靠 ResourceUsage 和 RateCard 完整还原。

计算流程

1. 使用相同的计费条件获取数据

获取 RateCard 时,以下条件必须与订阅的实际计费条件一致:

  • OfferDurableId
  • Currency
  • Locale
  • RegionInfo

如果 Azure Offer、币种或区域有误,即使 API 正常返回数据,计算结果也可能与实际账单相差很大。

Resource Usage 查询还需要覆盖完整且一致的计费周期。不要直接混算跨月数据,因为价格、包含额度和商业条款可能随计费周期变化。

2. 使用 meterId 关联用量和费率

Resource Usage 记录通常包含以下字段:

{
  "meterId": "00000000-0000-0000-0000-000000000000",
  "quantity": 12.5,
  "usageStartTime": "2026-08-01T00:00:00Z",
  "usageEndTime": "2026-08-01T01:00:00Z"
}

RateCard 中对应的计价器可能是:

{
  "MeterId": "00000000-0000-0000-0000-000000000000",
  "MeterName": "Example Meter",
  "Unit": "Hours",
  "IncludedQuantity": 0,
  "MeterRates": {
    "0": 0.12,
    "100": 0.10,
    "1000": 0.08
  }
}

两者通过以下字段关联:

ResourceUsage.meterId = RateCard.MeterId

字段大小写可能因 API 版本、SDK 或 Power BI 的转换方式而不同,实现时应以实际收到的 JSON 为准。

3. 先按计价器汇总用量

不要为每条小时级用量记录分别扣除 includedQuantity。包含额度通常只应在适用的计费周期和计价器范围内扣除一次。

例如:

Meter A 的总用量 = 60 + 80 + 100 = 240
IncludedQuantity = 40
BillableQuantity = 240 - 40 = 200

需要注意的是,某些权益并不是简单地按“每个 meterId 每月扣除一次”处理。如果订阅报价中的包含额度由多个计价器共享,只看单个 RateCard 条目可能无法准确分摊。这种情况应以实际 Offer 的计费规则为准。

4. 按阶梯费率计算

meterRates 的键代表该费率档位的起始用量,而不是结束用量。

例如:

{
  "0": 0.12,
  "100": 0.10,
  "1000": 0.08
}

计费用量为 1200 时,费用计算如下:

前 100 单位:       100 × 0.12
第 100~1000 单位:900 × 0.10
超过 1000 的部分: 200 × 0.08

因此:

Cost = 100 × 0.12 + 900 × 0.10 + 200 × 0.08
     = 118

只有当 meterRates 仅包含 "0" 这一个档位时,才可以直接计算:

Cost = BillableQuantity × meterRates["0"]

Python 示例

下面的示例不依赖 Azure SDK,只处理从两个 API 获取的 JSON 数据:

from collections import defaultdict
from decimal import Decimal


def get_value(obj, *names, default=None):
    """兼容不同 API 输出中的字段大小写。"""
    for name in names:
        if name in obj:
            return obj[name]
    return default


def calculate_tiered_cost(quantity, meter_rates):
    """
    quantity: 扣除 includedQuantity 后的计费用量
    meter_rates: 例如 {"0": 0.12, "100": 0.10, "1000": 0.08}
    """
    quantity = Decimal(str(quantity))

    tiers = sorted(
        (
            Decimal(str(start_quantity)),
            Decimal(str(rate))
        )
        for start_quantity, rate in meter_rates.items()
    )

    if quantity <= 0 or not tiers:
        return Decimal("0")

    cost = Decimal("0")

    for index, (tier_start, rate) in enumerate(tiers):
        if quantity <= tier_start:
            break

        if index + 1 < len(tiers):
            next_tier_start = tiers[index + 1][0]
            units_in_tier = min(quantity, next_tier_start) - tier_start
        else:
            units_in_tier = quantity - tier_start

        if units_in_tier > 0:
            cost += units_in_tier * rate

    return cost


def calculate_cost(resource_usage, rate_card_meters):
    # 建立 MeterId -> RateCard meter 的索引
    rate_by_meter = {}

    for meter in rate_card_meters:
        meter_id = get_value(meter, "MeterId", "meterId")
        if meter_id:
            rate_by_meter[meter_id.lower()] = meter

    # 按 meterId 汇总整个计费周期的用量
    usage_by_meter = defaultdict(Decimal)

    for usage in resource_usage:
        meter_id = get_value(usage, "meterId", "MeterId")
        quantity = get_value(usage, "quantity", "Quantity", default=0)

        if meter_id:
            usage_by_meter[meter_id.lower()] += Decimal(str(quantity))

    details = []
    total_cost = Decimal("0")

    for meter_id, total_quantity in usage_by_meter.items():
        meter = rate_by_meter.get(meter_id)

        if meter is None:
            details.append({
                "meterId": meter_id,
                "quantity": total_quantity,
                "status": "RateCard 中未找到对应 MeterId"
            })
            continue

        included_quantity = Decimal(str(
            get_value(
                meter,
                "IncludedQuantity",
                "includedQuantity",
                default=0
            )
        ))

        meter_rates = get_value(
            meter,
            "MeterRates",
            "meterRates",
            default={}
        )

        billable_quantity = max(
            Decimal("0"),
            total_quantity - included_quantity
        )

        cost = calculate_tiered_cost(
            billable_quantity,
            meter_rates
        )

        total_cost += cost

        details.append({
            "meterId": meter_id,
            "meterName": get_value(meter, "MeterName", "meterName"),
            "unit": get_value(meter, "Unit", "unit"),
            "totalQuantity": total_quantity,
            "includedQuantity": included_quantity,
            "billableQuantity": billable_quantity,
            "cost": cost,
            "status": "calculated"
        })

    return total_cost, details

调用方式:

total_cost, details = calculate_cost(
    resource_usage=usage_response["value"],
    rate_card_meters=rate_card_response["Meters"]
)

print(f"Estimated cost: {total_cost}")

for item in details:
    print(item)

实际响应中的集合字段不一定是 value 或 Meters,需要根据返回的 JSON 调整。

Power BI 中的处理思路

Power BI 可以使用相同的计算逻辑:

  1. 展开 Resource Usage 响应。
  2. 按 meterId 和计费月份分组,对 quantity 求和。
  3. 展开 RateCard 的 Meters。
  4. 使用 meterId 和 MeterId 合并查询。
  5. 扣除 IncludedQuantity。
  6. 根据 MeterRates 计算阶梯费用。
  7. 汇总所有计价器的费用。

如果能够确认所有相关计价器都只有 "0" 这一个费率档位,DAX 可以简化为:

BillableQuantity =
MAX (
    0,
    [TotalQuantity] - [IncludedQuantity]
)

EstimatedCost =
[BillableQuantity] * [BaseRate]

如果存在多个费率档位,就不能只提取 MeterRates["0"]。可以先在 Power Query 中把 MeterRates 转换成包含档位起点和单价的明细表,再按区间计算。

为什么结果可能与 Cost Analysis 不一致

即使关联方式和计算公式都没有问题,结果仍可能与 Azure Portal 的 Cost Analysis 或最终发票不同。常见原因有:

  • RateCard 返回的是与 Offer 相关的费率,不一定包含组织最终协商的价格。
  • Resource Usage 可能存在延迟、补记或后续更正。
  • 预留实例和 Savings Plan 的费用、抵扣及摊销方式,不能简化为用量乘以单价。
  • Azure Hybrid Benefit 等权益可能改变实际收费。
  • Marketplace 项目可能使用单独的计费记录。
  • 税费、信用额度、退款、舍入和账单调整不会体现在这套简单公式中。
  • 查询时间范围没有与 Azure 的计费周期严格对齐。
  • 用量单位与价格单位不一致,或需要按服务规则换算。
  • 计费周期内价格发生了变化,但计算时只使用了一份 RateCard 快照。

因此,这套方法适合估算“基于 ResourceUsage 和 RateCard 的费用”。如果需要获得与账单一致的实际成本,应优先使用 Azure Cost Management 的 Cost Details、Usage Details 或相关导出数据,因为这些数据通常已经应用了实际计费价格和账单调整。

备注:内容仅供参考。