如何通过 Azure Billing API 获取资源成本明细
结论
可以获取,但不应从 Billing Invoices API 入手。Microsoft.Billing/invoices 返回的主要是账单周期、账单标识和下载信息,不提供逐资源费用,因此列表中没有金额很正常。
制作资源成本 dashboard 时,可以优先考虑:
- Azure Cost Management Query API:按资源、资源组、服务、日期等维度聚合成本,适合制作图表和报表。
- Azure Consumption Usage Details API:提供更接近原始消费记录的明细,适合核算或导出。
- 新项目建议使用
@azure/identity、@azure/arm-costmanagement和@azure/arm-consumption。ms-rest-azure、azure-arm-billing、azure-arm-consumption都是旧版 SDK,不建议在新代码中继续使用。
自动化认证应使用 Microsoft Entra ID service principal、托管身份或工作负载身份,无需通过 Postman 或 interactiveLogin() 获取令牌。
为什么 Invoices API 没有资源金额
账单、成本和消费记录是三类不同的数据:
- Billing API:提供账单周期、发票元数据和账单文档等信息。
- Cost Management API:提供聚合后的实际成本、摊销成本和预测成本。
- Consumption API:提供用量和计费明细。
发票是结算文档,不是资源成本数据集。即使成功调用:
/subscriptions/{subscriptionId}/providers/Microsoft.Billing/invoices
返回结果通常也不会逐项列出虚拟机、磁盘、数据库等资源的成本。
URL 中的 api-version=2016-09-01 只是该 REST 操作使用的版本,并不表示查询的是 2016 年的数据。不过,旧示例使用的版本和 SDK 确实可能已经停止维护。通过官方 SDK 调用时,通常无需手动拼接 api-version。
推荐方案:Cost Management Query API
该 API 可以按 ResourceId 聚合成本,也能同时按资源组、资源类型、服务名称等维度分组。
对应的 REST 请求如下:
POST https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.CostManagement/query?api-version={supported-api-version}
Authorization: Bearer {access-token}
Content-Type: application/json
请求体示例:
{
"type": "ActualCost",
"timeframe": "Custom",
"timePeriod": {
"from": "2026-09-01T00:00:00Z",
"to": "2026-09-10T00:00:00Z"
},
"dataset": {
"granularity": "Daily",
"aggregation": {
"totalCost": {
"name": "Cost",
"function": "Sum"
}
},
"grouping": [
{
"type": "Dimension",
"name": "ResourceId"
},
{
"type": "Dimension",
"name": "ResourceType"
}
]
}
}
返回结果使用 columns 加 rows 的表格结构。字段顺序不能假定为固定值,应根据 columns 建立索引。
使用现代 Node.js SDK
安装依赖:
npm install @azure/identity @azure/arm-costmanagement
使用 service principal 查询订阅成本:
const { ClientSecretCredential } = require("@azure/identity");
const { CostManagementClient } = require("@azure/arm-costmanagement");
const tenantId = process.env.AZURE_TENANT_ID;
const clientId = process.env.AZURE_CLIENT_ID;
const clientSecret = process.env.AZURE_CLIENT_SECRET;
const subscriptionId = process.env.AZURE_SUBSCRIPTION_ID;
async function main() {
const credential = new ClientSecretCredential(
tenantId,
clientId,
clientSecret
);
const client = new CostManagementClient(credential);
const scope = `/subscriptions/${subscriptionId}`;
const result = await client.query.usage(scope, {
type: "ActualCost",
timeframe: "Custom",
timePeriod: {
from: new Date("2026-09-01T00:00:00Z"),
to: new Date("2026-09-10T00:00:00Z")
},
dataset: {
granularity: "Daily",
aggregation: {
totalCost: {
name: "Cost",
function: "Sum"
}
},
grouping: [
{
type: "Dimension",
name: "ResourceId"
},
{
type: "Dimension",
name: "ResourceType"
}
]
}
});
const columnNames = (result.columns || []).map(column => column.name);
const rows = (result.rows || []).map(row =>
Object.fromEntries(
columnNames.map((name, index) => [name, row[index]])
)
);
console.dir(rows, { depth: null });
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
如果程序运行在 Azure VM、App Service、Functions 或 Container Apps 中,建议启用托管身份,以免保存 client secret:
const { DefaultAzureCredential } = require("@azure/identity");
const { CostManagementClient } = require("@azure/arm-costmanagement");
const subscriptionId = process.env.AZURE_SUBSCRIPTION_ID;
async function main() {
const credential = new DefaultAzureCredential();
const client = new CostManagementClient(credential);
const result = await client.query.usage(
`/subscriptions/${subscriptionId}`,
{
type: "ActualCost",
timeframe: "MonthToDate",
dataset: {
granularity: "Daily",
aggregation: {
totalCost: {
name: "Cost",
function: "Sum"
}
},
grouping: [
{
type: "Dimension",
name: "ResourceId"
}
]
}
}
);
console.dir(result, { depth: null });
}
main().catch(console.error);
权限应该配置在哪里
所需权限不在 App registration 的 API permissions 页面,而是在目标订阅或计费范围的 Access control (IAM) 中。
调用 Azure Resource Manager 成本 API 时,需要给 service principal 分配 Azure RBAC 角色。如果只需读取成本,通常应在订阅范围分配:
Cost Management Reader
该角色包含读取 Cost Management 和 Consumption 数据所需的权限。仅在 App registration 中添加 Microsoft Graph 或其他 delegated permissions,并不会自动获得订阅成本的访问权。
可以使用 Azure CLI 分配角色:
az role assignment create \
--assignee-object-id "<service-principal-object-id>" \
--assignee-principal-type ServicePrincipal \
--role "Cost Management Reader" \
--scope "/subscriptions/<subscription-id>"
这里要填写 service principal 在 Enterprise applications 中对应的 object ID,而不是以下任一 ID:
- Application (client) ID
- App registration 自身的 object ID
- Subscription ID
- Tenant ID
如果程序还要读取普通 Azure 资源的属性,例如资源名称、标签、位置和资源类型,可以额外分配:
Reader
Reader 和 Cost Management Reader 的用途不同:
Reader:读取 Azure 资源配置。Cost Management Reader:读取成本和消费数据。
如果 dashboard 同时展示资源属性和成本,通常需要同时使用这两个角色。
401 错误的判断方法
缺少 Azure RBAC 权限时,更常见的响应是 403 Forbidden。如果返回 401 Unauthorized,应先检查认证信息本身。
需要核对以下内容:
-
domain是否为订阅所属 Microsoft Entra tenant 的 tenant ID。 -
clientId是否为 Application (client) ID。 -
secret是否为创建 secret 后显示的 Value,而不是 Secret ID。 -
service principal 是否确实存在于该 tenant 的 Enterprise applications 中。
-
subscriptionId是否属于同一个 tenant。 -
access token 的 audience 是否为 Azure Resource Manager,即:
https://management.azure.com/ -
是否混用了旧 SDK、旧认证库和不兼容的令牌类型。
使用 ClientSecretCredential 或 DefaultAzureCredential 时,SDK 会为 Azure Resource Manager 获取正确的令牌,通常不需要自行调用 getToken()。
角色分配完成后可能会有短暂的传播延迟。此时应重新获取令牌并重试,不要继续使用角色分配前缓存的令牌。
使用 Usage Details API 获取更细明细
如果需要逐条计费记录,而不是按资源聚合的数据,可以使用:
@azure/arm-consumption
安装:
npm install @azure/identity @azure/arm-consumption
示例:
const { ClientSecretCredential } = require("@azure/identity");
const {
ConsumptionManagementClient
} = require("@azure/arm-consumption");
const tenantId = process.env.AZURE_TENANT_ID;
const clientId = process.env.AZURE_CLIENT_ID;
const clientSecret = process.env.AZURE_CLIENT_SECRET;
const subscriptionId = process.env.AZURE_SUBSCRIPTION_ID;
async function main() {
const credential = new ClientSecretCredential(
tenantId,
clientId,
clientSecret
);
const client = new ConsumptionManagementClient(
credential,
subscriptionId
);
const scope = `/subscriptions/${subscriptionId}`;
for await (const detail of client.usageDetails.list(scope, {
filter:
"properties/usageStart ge '2026-09-01' and " +
"properties/usageEnd le '2026-09-10'"
})) {
console.dir(detail, { depth: null });
}
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
不同账户协议和 SDK 版本支持的筛选字段可能不同。如果服务端拒绝某个 $filter,可以先去掉筛选条件调用,再根据实际返回模型和当前 SDK 文档调整。
Usage Details 的数据量通常很大,因此必须处理分页。使用 SDK 的异步迭代器,可以避免只读取第一页。
订阅类型和计费策略也会影响结果
即使 Azure RBAC 配置正确,以下情况仍可能造成无权访问或数据不完整:
- Enterprise Agreement 管理员关闭了订阅用户查看费用的策略。
- 在 Microsoft Customer Agreement 下,身份没有对应 billing profile 或 invoice section 的权限。
- CSP 订阅的部分成本只能从合作伙伴侧查看。
- 资源尚未产生可计费使用量。
- 当天成本仍在处理中,尚未进入 Cost Management。
- 某些费用只能分配到订阅、资源组或服务,无法对应具体
ResourceId。 - 预留实例、Savings Plan、支持计划、税费、Marketplace 采购和抵扣项不一定能映射到单个资源。
如果已经在订阅范围分配 Cost Management Reader,但仍无法读取成本,需要检查订阅的计费协议,以及组织级的“允许订阅用户查看费用”等策略。
ActualCost 与 AmortizedCost
dashboard 还要明确使用哪种成本口径:
ActualCost
表示账单实际记账成本,更接近本期结算金额。
AmortizedCost
会将预留实例、Savings Plan 等预付费用按使用期间摊销,更适合分析资源每天的实际使用成本。
如果只展示其中一种,图表总额可能与发票金额不一致。生产环境中的 dashboard 最好注明成本口径、币种、查询时间范围和数据更新时间。
实际落地建议
资源成本 dashboard 可以按以下方式实现:
- 使用 Cost Management Query API 生成按天、资源组和
ResourceId聚合的数据。 - 使用 Azure Resource Manager 查询资源名称、标签、位置等属性。
- 需要审计或核算时,再通过 Usage Details API 获取底层计费记录。
- 定期将查询结果写入数据库,避免用户每次打开 dashboard 时都实时扫描大量消费数据。
- 根据返回的
Currency字段处理币种,不要假定所有记录使用同一种货币。 - 将没有
ResourceId的费用单独归为共享费用、采购费用或订阅级费用。
原程序的认证思路基本正确,但不应继续使用 interactiveLogin(),也不能依靠 App registration 的 API permissions 获得成本权限。应改用现代 Azure Identity SDK,并在订阅 IAM 中将 Cost Management Reader 分配给对应的 service principal,才能获取逐资源成本数据。
备注:内容仅供参考。
