Azure Price Sheet API 返回 404 错误,如何排查?
结论
这段代码的认证流程基本正确,Price Sheet URL 也符合订阅级 Get By Billing Period 接口的格式。HTTP 404 通常不是 Python 代码本身造成的,而是 Azure 无法在当前订阅范围内找到指定的价格表资源。
建议先检查以下三项:
billingPeriodName="202407"是否是该订阅中真实存在且允许查询的账期。- 当前订阅的计费协议是否支持订阅级 Price Sheet API。
- 服务主体是否位于正确的租户中,并拥有读取成本和计费数据的权限。
代码还有一个变量名错误。最后打印了未定义的 pricesheet_data,实际变量名是 retail_prices_data。这个错误不会导致 HTTP 404,但请求成功后会触发 NameError。
查看 Azure 返回的错误正文
当前代码直接调用 raise_for_status(),可能会漏掉 Azure 返回的具体错误代码和消息。调用前应先打印响应内容和请求跟踪标识:
response = requests.get(retail_prices_url, headers=headers, timeout=60)
print("Request URL:", response.request.url)
print("Status:", response.status_code)
print("Response:", response.text)
print("x-ms-request-id:", response.headers.get("x-ms-request-id"))
print("x-ms-correlation-request-id:",
response.headers.get("x-ms-correlation-request-id"))
response.raise_for_status()
Azure 的 JSON 响应通常采用下面的结构:
{
"error": {
"code": "...",
"message": "..."
}
}
排查时应以 error.code 和 error.message 为准。仅凭 HTTP 404,无法判断是账期不存在、计费模型不支持、订阅范围错误,还是资源被隐藏。
测试不带账期的当前价格表接口
要判断问题是否与 billingPeriodName 有关,可以先调用当前价格表接口:
GET https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Consumption/pricesheets/default?api-version=2023-05-01
对应代码如下:
current_pricesheet_url = (
f"https://management.azure.com/subscriptions/{subscription_id}"
"/providers/Microsoft.Consumption/pricesheets/default"
"?api-version=2023-05-01"
)
response = requests.get(
current_pricesheet_url,
headers=headers,
timeout=60
)
print(response.status_code)
print(response.text)
response.raise_for_status()
pricesheet_data = response.json()
可以根据结果继续判断:
- 如果当前价格表请求成功,而指定账期返回 404,
202407很可能不存在、无法查询,或者该账期没有生成价格表。 - 如果两个接口都返回 404,应重点检查订阅的计费协议、租户、订阅范围,以及该 API 是否适用于当前订阅。
- 如果返回 401,通常表示令牌无效、已过期或受众不正确。
- 如果返回 403,说明身份已经通过认证,但缺少读取成本或计费数据的权限。
不要随意把月份拼成 YYYYMM。即使格式正确,该账期也必须真实存在于对应订阅的计费上下文中。尚未关闭的月份、早于订阅创建时间的月份,或者超出服务保留范围的历史月份,都可能没有可用的价格表。
确认订阅的计费协议
Price Sheet 并非以同一种方式向所有 Azure 订阅类型提供。订阅可能属于:
- Enterprise Agreement(EA)
- Microsoft Customer Agreement(MCA)
- Microsoft Partner Agreement(MPA)
- CSP
- Web Direct 或其他在线订阅
- Sponsorship、Visual Studio、试用等特殊优惠类型
不同协议可能使用不同的计费范围,例如:
- subscription
- billing account
- billing profile
- invoice section
- enrollment
当前代码使用订阅范围:
/subscriptions/{subscriptionId}/...
如果价格表实际属于 billing account 或 billing profile,在订阅范围内发起请求时可能找不到对应资源。可以先在 Azure 门户的”成本管理 + 计费”中确认:
- 订阅所属的计费帐户类型。
- 当前账号能否看到 Billing account、Billing profile 或 Enrollment。
- 该协议对应的官方 Price Sheet API 使用哪种 scope。
- 是否应改用 billing profile、billing account 或 EA enrollment 范围的价格表下载接口。
不要只替换路径中的 provider 名称。不同计费协议不仅使用不同的 URL,所需权限、可用字段和账期表示方式也可能不同。
检查租户和服务主体权限
成功获取令牌,只能证明 client_id 和 client_secret 有效,不能证明该身份有权读取目标订阅。
至少需要确认以下内容:
subscriptionId属于tenant_id指定的租户,或者服务主体已经被正确邀请并授权。- 角色分配给服务主体对应的 Enterprise application,而不是 App registration 对象本身。
- 角色分配范围覆盖目标订阅或对应的计费范围。
- 角色允许读取成本和价格数据,例如适用范围内的
Cost Management Reader,或该计费协议要求的同等计费角色。 - 如果访问 billing account 或 billing profile,仅有订阅 RBAC 角色可能不够,还需要相应的计费范围权限。
为避免泄露资源是否存在,部分 Azure 接口会返回 404,而不是更直观的 403。因此,不能因为收到 404 就排除租户或权限问题。
修正后的诊断代码
下面的示例分别测试当前价格表和指定账期的价格表,并完整输出 Azure 返回的错误信息:
import json
import requests
tenant_id = "xxx"
client_id = "yyy"
client_secret = "zzz"
subscription_id = "www"
billing_period_name = "202407"
token_url = (
f"https://login.microsoftonline.com/{tenant_id}"
"/oauth2/v2.0/token"
)
token_data = {
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret,
"scope": "https://management.azure.com/.default",
}
token_response = requests.post(
token_url,
data=token_data,
timeout=30
)
token_response.raise_for_status()
access_token = token_response.json()["access_token"]
headers = {
"Authorization": f"Bearer {access_token}",
"Accept": "application/json",
}
current_url = (
f"https://management.azure.com/subscriptions/{subscription_id}"
"/providers/Microsoft.Consumption/pricesheets/default"
"?api-version=2023-05-01"
)
billing_period_url = (
f"https://management.azure.com/subscriptions/{subscription_id}"
"/providers/Microsoft.Billing"
f"/billingPeriods/{billing_period_name}"
"/providers/Microsoft.Consumption/pricesheets/default"
"?api-version=2023-05-01"
)
def request_pricesheet(name, url):
response = requests.get(url, headers=headers, timeout=60)
print(f"\n{name}")
print("URL:", response.request.url)
print("HTTP status:", response.status_code)
print("x-ms-request-id:", response.headers.get("x-ms-request-id"))
print(
"x-ms-correlation-request-id:",
response.headers.get("x-ms-correlation-request-id")
)
try:
print(json.dumps(response.json(), indent=2))
except ValueError:
print(response.text)
response.raise_for_status()
return response.json()
current_pricesheet = request_pricesheet(
"Current price sheet",
current_url
)
historical_pricesheet = request_pricesheet(
f"Price sheet for {billing_period_name}",
billing_period_url
)
如果当前只需要定位问题,可以先注释掉第二次调用,确认当前价格表接口能否正常使用。
关于折扣价格
Price Sheet 返回的是特定计费协议和计费范围内可用的计价信息。如果接口和计费协议支持,相关价格字段可能包含协商价格或合同价格,但这些数据不能直接视为某项资源的最终账单金额。
最终费用还可能受到以下因素影响:
- Reserved Instances
- Savings Plan
- Azure Hybrid Benefit
- 免费额度和赠金
- 阶梯计价
- 承诺消费
- 月中价格调整
- 税费
- Marketplace 费用
- 退款和人工调整
如果要核对”实际产生了多少费用”,还需要结合 Cost Details、Usage Details 或账单明细,不能只依赖 Price Sheet。
代码中的其他问题
原代码的最后一行:
print(pricesheet_data)
应改为:
print(retail_prices_data)
下面这次调用是重复的,可以删除:
response.raise_for_status()
也不要打印 token_data,其中包含 client_secret:
print(token_data)
生产环境中应把密钥存放在环境变量或密钥管理服务中,同时避免在日志里输出访问令牌、客户端密钥和完整的错误请求头。
建议按这个顺序定位问题:先读取 404 响应中的 JSON 错误正文,再测试不带账期的当前价格表接口,然后确认订阅的计费协议和正确的 scope,最后检查服务主体在订阅范围和计费范围内的权限。
备注:内容仅供参考。
