如何使用 Azure Billing APIs 计算 Azure 成本?
结论
可以直接计算,方法如下:
- 用
meterId匹配ResourceUsage中的用量记录和RateCard中的计价器。 - 按计费周期汇总每个计价器的
quantity。 - 从汇总用量中扣除该计价器的
includedQuantity。 - 根据
meterRates逐档计费,不能简单地使用quantity × 第一个单价。 - 将所有计价器的费用相加。
计算关系为:
EstimatedCost = Σ TierCost(BillableQuantity, meterRates)
BillableQuantity = max(0, TotalQuantity - includedQuantity)
这样算出的是基于公开费率的估算成本,不一定与 Azure 发票金额一致。企业协议折扣、预留实例、Savings Plan、Azure Hybrid Benefit、Marketplace、税费、信用额度、退款和人工调整等项目,通常无法仅靠 ResourceUsage 和 RateCard 完整还原。
计算流程
1. 使用相同的计费条件获取数据
获取 RateCard 时,以下条件必须与订阅的实际计费条件一致:
OfferDurableIdCurrencyLocaleRegionInfo
如果 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 可以使用相同的计算逻辑:
- 展开 Resource Usage 响应。
- 按
meterId和计费月份分组,对quantity求和。 - 展开 RateCard 的
Meters。 - 使用
meterId和MeterId合并查询。 - 扣除
IncludedQuantity。 - 根据
MeterRates计算阶梯费用。 - 汇总所有计价器的费用。
如果能够确认所有相关计价器都只有 "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 或相关导出数据,因为这些数据通常已经应用了实际计费价格和账单调整。
备注:内容仅供参考。
