先说我为什么折腾这件事。前年我接了一个 Azure China 账号群的成本治理项目,几十个订阅里跑着上百台虚拟机,利用率参差不齐,账单却每个月都很好看地超预算。最标准的省钱手段就是买预留实例(RI),也就是提前承诺一年或三年的用量,换取最高72%左右的折扣。问题是门户里一台一台手动买,几十个 SKU、多个订阅,手点不仅慢,还容易把承诺范围和计费订阅选错,月底对账的时候更是一笔糊涂账。后来我花了一个周末把 RI 采购全部切成 API 方式,用脚本批量下单,整个流程从“点半天”变成“跑一次”。这篇文章就把 Azure China 上用 API 购买预留实例的完整玩法盘一遍,包括认证、下单参数、轮询状态和一大堆实践里才踩得到的坑,适合做云成本管理、FinOps、或者正在搞云资源自动化的朋友参考。
1. 为什么非得用 API 买预留实例
1.1 预留实例是什么,能省多少钱
先给没接触过的朋友补个底。预留实例本质是云厂商的“批发折扣”,你承诺用一段时间,换取更低的单价。逻辑跟办健身年卡一样:一次性预付或绑定承诺,换来每次使用成本下降。
在 Azure China 里,RI 通常针对虚拟机的计算部分打折,也可以覆盖 SQL Database、Cosmos DB、App Service 等资源。以虚拟机为例,你买了一个Standard_D8s_v3的一年期预留实例,只要你在承诺的区域和 SKU 上开机,账单里的计算费用就会按折扣价计算,即便你中途删除并重建虚拟机,只要规格匹配,折扣一样生效。我以前实测过,一年期 RI 大约能省 30%-40%,三年期能到 50%-72%,具体看 SKU 和地区的定价策略。这个比例在 Azure China 也一样适用。
真正要强调的是,RI 买的是“容量承诺”,不是“分配给你一台机器”。买完之后你不需要关联到任何具体的虚拟机,系统会在你对应区域的匹配资源运行时自动应用折扣。这个机制决定了它适合两类场景:一是长期稳定运行的基线负载,比如核心数据库、生产中间件;二是有明确扩容计划的工作负载,比如明年要新增一批计算节点,现在先锁定折扣。
1.2 门户购买和 API 购买的差别在哪
门户购买其实不复杂,在 Cost Management + Billing 里找到“购买预留实例”,选 SKU、区域、期限、数量,点击购买就完事。但一旦规模上来,问题就暴露了:
- 不可批量:门户没有“导入清单”功能,50 个 SKU 就意味着 50 次完整的手动操作。
- 不可审计:谁在什么时候买了什么、为什么买,门户操作很难沉淀成结构化的审计记录。
- 不可幂等重试:网络卡一下,到底下单成功没有,需要人工确认,很容易重复购买。
- 不可集成:企业做 FinOps 时通常希望采购流程走审批平台,审批通过之后自动下单,而不是由运维同学手工去点“确认购买”。
API 方式解决的核心就是:可编程、可重复、可审计。你可以把购买行为变成代码仓库里的一次 PR,评审通过后由 CI/CD 管道执行,订单会自动落库,再通过回调或轮询写入成本平台。这套玩法在几十个订阅的规模下,收益远超那点开发成本。
2. 动手前必须搞定的认证体系
2.1 中国区的认证端点跟全球版不一样
这是最容易踩的第一个坑。很多人拿着全球版 Azure 的认证地址直接往 Azure China 上套,结果拿到的 token 要么无效,要么后续请求跳到错误的 management 端点。
Azure China(由世纪互联运营)与全球版的关键区别在于两组 endpoint:
- 登录认证:
https://login.partner.microsoftonline.cn或https://login.chinacloudapi.cn - 资源管理:
https://management.chinacloudapi.cn
国际版对应的是https://login.microsoftonline.com和https://management.azure.com。这两个 URL 在拿 token 时就必须配对使用,否则即便你通过认证,后续 API 请求依然无法命中目标租户。
我建议把这两个地址写进团队的基础配置仓库,不要散落在脚本里。China 环境的另一特点是 Azure PowerShell、Azure CLI 都需要显式指定云环境,比如az cloud set --name AzureChinaCloud,否则连订阅列表都拉不出来。记住一个原则:凡是访问中国区资源,所有 SDK 和命令行都别用默认环境。
2.2 创建服务主体并配置密钥
要用 API 完成自动化,不能每次拿用户名密码登录,必须创建服务主体(Service Principal),本质是一个“机器人账号”。
操作路径是在 Microsoft Entra ID(Azure AD)里完成应用注册:
- 进入 Microsoft Entra ID,选择“应用注册”,点击“新建注册”。
- 名称随意,比如
RI-Purchase-Automation。 - 支持的账户类型选择“仅此组织目录中的账户”。
- 注册完成后,记录“应用程序(客户端) ID”和“目录(租户) ID”,这两个 ID 后面都要用。
- 在“证书和密码”中新建一个客户端密码,选择一年有效期。生成的密码值只展示一次,立刻保存到密钥管理服务里。
这里有个实操细节:很多人会把客户端密码直接写死在脚本里,我强烈不推荐。Azure China 的自动化任务建议配合 Key Vault 的托管密钥或自家 CI/CD 平台的 secret 管理,把密码当成高危资产对待。你想想,这个服务主体如果被赋予了订阅级 Contributor 权限,泄露出去的破坏力不亚于一个管理员的密码。
2.3 给服务主体分配购买预留实例的权限
创建完服务主体,它还是一个“没有权限的账号”。要让它能购买 RI,需要把权限作用到目标订阅或管理组上。
在目标订阅的“访问控制(IAM)”里添加角色分配,角色建议按从简到繁选择:
| 角色 | 能力 | 适用建议 |
|---|---|---|
| Reservation Purchaser(预留实例购买者) | 可以购买预留实例,但不能管理其他资源 | 最小权限方案,推荐 |
| Contributor | 可以管理订阅内所有资源 | 如果该服务主体还有其他自动化用途 |
| Owner | 完全控制订阅 | 不推荐给自动化账号 |
为什么我推荐 Reservation Purchaser 而不是 Contributor?因为自动化账号大多数时候只负责下订单,没必要给它整个订阅的写权限。一旦这个账号被攻破,Contributor 至少还能创建、删除虚拟机,而 Reservation Purchaser 只能做预留实例相关的操作。别嫌麻烦,安全上额外花十分钟是值得的。
另外,角色分配还有个延迟问题:分配完成后,服务主体真正拥有权限通常需要几十秒到几分钟。下单脚本在执行之前,先加一个简单的轮询检查权限,避免刚分配完角色就立刻执行导致 403。
2.4 获取 Access Token 的两种写法
认证方式选择客户端凭据流(client_credentials),适合无人值守场景。先看 curl 版本:
tenant_id="your-tenant-id" client_id="your-client-id" client_secret="your-client-secret" access_token=$(curl -s -X POST "https://login.partner.microsoftonline.cn/${tenant_id}/oauth2/v2.0/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "grant_type=client_credentials" \ --data-urlencode "client_id=${client_id}" \ --data-urlencode "client_secret=${client_secret}" \ --data-urlencode "scope=https://management.chinacloudapi.cn/.default" \ | jq -r '.access_token')注意scope里的资源地址必须是中国区的 management endpoint,写成.default表示使用应用注册时配置的默认权限。如果返回的 JSON 里没有access_token,先检查返回的error_description,绝大多数情况是 client_secret 复制多了空格,或者有效期已经过了。
PowerShell 版本就简洁多了:
Connect-AzAccount -Environment AzureChinaCloud -ServicePrincipal ` -TenantId "your-tenant-id" ` -ApplicationId "your-client-id" ` -CertificateThumbprint "your-cert-thumbprint" # 或用 -ClientSecret 传密码 $token = (Get-AzAccessToken -ResourceUrl "https://management.chinacloudapi.cn").Token这里顺便说一句,能用证书就尽量不要用客户端密码。证书的轮换可以半自动化,而客户端密码一旦在日志里被打印出来,就是长期后门。我见过不止一次团队把 secret 打到 CI 日志里,最后只能紧急轮换密钥,非常狼狈。
3. 购买预留实例的 API 核心玩法
3.1 购买前必须想清楚的四个参数
调用下单 API 之前,参数如果错了,轻则 400 报错,重则买了不匹配的承诺,白白浪费钱。我每次落地都会把参数整理成 JSON 配置文件,团队评审后再执行。
四个关键参数分别是:
SKU 名称(sku.name):如Standard_D8s_v3,必须与目标虚拟机规格完全一致,大小写敏感。买错 SKU 的话,折扣不会自动套用到别的规格上。
期限(term):P1Y表示一年,P3Y表示三年。这里要算一笔账:三年折扣更高,但锁定时间也长;如果业务计划不够明确,宁可选一年期,将灵活性握在自己手里。
区域(location):如chinanorth2(中国北部2)、chinaeast2(中国东部2)。RI 的折扣只作用于购买时指定的区域,不可能用北京区域的 RI 去覆盖上海区域的开销,下单前要反复核对资源组所在的可用区。
作用域(appliedScopeType):Shared表示共享作用于订阅内所有匹配的虚拟机,Single表示只应用于指定订阅或资源组。绝大多数场景用Shared更省心,因为你不用精确指认哪台机器,系统自动挑最合适的。只有你需要跨订阅隔离时才用Single。
另外还有个容易被忽略的instanceFlexibility参数。如果设置为On,意味着你买的 2 核 RI 可以部分匹配到 4 核或 8 核机器上,按比例抵扣,灵活性大幅提升。我强烈建议开启,除非你确认自己的负载规格永远不会变化。
3.2 创建预留订单的两种接口姿势
Azure 的预留实例购买走的是Microsoft.Capacity资源提供程序,下单接口本质上是“创建预留订单”。
在 Azure China 上,基础端点长这样:
https://management.chinacloudapi.cn/providers/Microsoft.Capacity/reservationOrders/{reservationOrderId}?api-version=2022-11-01为什么推荐 PUT 而不是 POST?因为 PUT 允许我们自己指定一个 GUID 作为reservationOrderId,天然具备幂等性。如果网络超时重试,同一个订单 ID 不会有两条订单被创建;而 POST 由服务端生成 ID,重试时无法从请求层面避免重复下单。
如果你不想管 GUID,也可以用 POST 到/providers/Microsoft.Capacity/reservationOrders,服务端会返回包含新订单 ID 的 Location 头,但你需要额外处理重复请求的问题。
这里特别提醒一下 API 版本。Azure China 的某些功能上线会滞后于国际版,所以你去看文档时如果发现国际版的 api-version 特别新,未必在中国区可用。稳妥的做法是在 API 请求中先试2022-11-01,如果返回错误再去服务文档确认最新支持的版本。
3.3 一次完整的 curl 下单实操
下面这个例子,我把购买请求完整写出来,你复制后替换占位符就可以用。
#!/usr/bin/env bash subscription_id="your-billing-subscription-id" reservation_order_id=$(uuidgen) # 生成一个全局唯一 ID,macOS 用 uuidgen,Linux 可用 /proc/sys/kernel/random/uuid access_token="PASTE_YOUR_ACCESS_TOKEN" curl -s -X PUT \ "https://management.chinacloudapi.cn/providers/Microsoft.Capacity/reservationOrders/${reservation_order_id}?api-version=2022-11-01" \ -H "Authorization: Bearer ${access_token}" \ -H "Content-Type: application/json" \ -d '{ "sku": { "name": "Standard_D8s_v3" }, "location": "chinanorth2", "reservedResourceType": "VirtualMachines", "billingScopeId": "/subscriptions/'${subscription_id}'", "term": "P1Y", "quantity": 2, "displayName": "prod-d8s-v3-one-year", "appliedScopeType": "Shared", "appliedScopes": [], "instanceFlexibility": "On" }'我解释一下里面的几个blind spot:
reservedResourceType: VirtualMachines表示这是虚拟机预留实例。如果是 SQL 数据库或 Cosmos DB,需要换成对应类型。billingScopeId是付费的订阅,这个订阅必须与后续实际的用量订阅有正确的合约关系,通常是企业协议或同一计费主体的订阅。quantity是针对同一 SKU、同一区域的一次性购买数量,比如买 2 台Standard_D8s_v3的一年承诺。displayName要起得规范,我习惯按环境-SKU-期限命名,方便月底对账时一眼看出哪笔订单对应什么用途。
请求发出后正常会返回202 Accepted,而不是200 OK。因为下订单是一个异步操作,服务端需要时间在后台创建预留资源。响应头里有Location和Retry-After,前者指向待查询的订单状态地址,后者建议你等待的秒数。
3.4 轮询订单状态直到成功
下单只是第一步,真正的结算动作发生在后台。订单状态常见的有Pending、Purchased、Failed、Cancelled等。你需要按下面这种方式轮询:
order_status=$(curl -s \ "https://management.chinacloudapi.cn/providers/Microsoft.Capacity/reservationOrders/${reservation_order_id}?api-version=2022-11-01" \ -H "Authorization: Bearer ${access_token}" \ | jq -r '.properties.state') echo "当前订单状态: ${order_status}"如果返回Purchased,恭喜,RI 已经生效,通常几分钟内就会体现在账单折扣上。如果一直是Pending,不要无脑高频轮询,我建议按 5 秒、10 秒、30 秒的退避策略重试,最多等两分钟;长时间Pending就要查看是否有未完成的条件,比如计费合约有问题、订阅额度不足等。
有一点必须提醒:不要看到 202 就认为购买成功了。在异步系统里,202 只代表“请求已受理”,不保证一定会成功。我接手过一个事故,脚本只处理了 202 响应没有检查最终状态,结果订单后台报错,那部分本应享有的折扣整整晚了一个月才被发现,损失不可谓不小。所有下单脚本必须以最终状态为准,加一个显眼的失败告警。
4. 常见问题与排查技巧实录
4.1 401/403 认证类错误
调用过程中最常见的错误就是401 Unauthorized和403 Forbidden,这两种报错虽然都跟权限有关,含义差别很大:
| 错误码 | 可能原因 | 排查手段 |
|---|---|---|
| 401 Unauthorized | token 无效、过期、格式错误 | 检查 token 是否真的以Bearer前缀传递;重新获取 token;确认认证 URL 是否是中国区 |
| 403 Forbidden | 身份有效但没有权限 | 检查服务主体是否被分配了 Reservation Purchaser 或 Contributor 角色;确认角色分配作用在正确的订阅/管理组 |
401 Unauthorized里我还碰到过一次很奇怪的情况:token 明明是在中国区拿的,但请求时把 management 地址写成了management.azure.com,导致资源提供程序校验失败,也报 401。这种纯属环境串台,把 URL 改回management.chinacloudapi.cn就正常了。
至于403,最常见的是角色分配没有生效。因为 Azure 的 RBAC 权限传播有延迟,刚分配完角色立刻调用就会 403。我的做法是在脚本前部加一段权限探测,比如先调用一个轻量的读取接口,确认能访问之后再真正下单。
4.2 400 参数校验错误
400 Bad Request说明请求体不合法,给了服务端无法理解的参数。我整理了几个高频原因:
- SKU 名称错误:比如
Standard_D8S_v3把s写成了大写,立刻 400。 - location 无效:Azure China 有自己的区域代号,不要把全球版的
eastus直接搬过来,中国区常见的是chinanorth2、chinanorth3、chinaeast2。 - term 格式错误:必须是大写
P1Y或P3Y,写小写p1y就挂了。 - billingScopeId 指向的订阅不对:RI 购买计费作用域必须是有效的订阅路径,指向管理组有时会报错。
遇到 400 不要慌,错误响应体里面通常有error.message字段,里面会提示具体是哪个字段不合法。我用 jq 解析出来后再对照请求体排查,比瞎猜快得多。
4.3 订单一直 Pending,最后变 Failed
我的订单曾经卡在 Pending 状态超过五分钟,最后变 Failed,查了半天才发现问题出在计费订阅上。Azure China 的企业客户往往有多个计费关系,某些订阅并不具备购买预留实例的合约资格。你可以检查订阅所在合约是否允许购买 RI,尤其是促销订阅和额度受限的订阅。
另一个原因是数量超限。同一个订阅下同一 SKU 的 RI 数量有配额约束,如果你已经买了很多,再下单就可能失败。这类问题无法通过简单重试解决,需要先查看现有预留实例列表,确认配额情况,或者把部分订单分摊到其他订阅。
4.4 中国区特有的本地化坑
Azure China 因为是本地运营,很多问题带有“本地特色”。
第一是接口文档滞后。你在中国区控制台或者文档站上看到的 API 示例有时比实际支持的版本旧,如果你照搬了某个全新的 api-version,容易收到不兼容的报错。我会在环境中预先跑一个探测脚本,把可用版本范围摸清,再固化成配置。
第二是区域命名习惯。中国区的数据中心分为北部和东部,但同一个区域在不同产品下叫法可能略有差异,比如虚拟机页面显示“中国北部2”,API 里却是chinanorth2。反正统一以 API 文档为准,别用中文显示名写代码。
第三是账单生效时间。中国区的账单系统是每日汇聚的,RI 购买成功后折扣通常不会实时体现在当天的成本视图里,一般 24 到 48 小时后才能对账。这点我在刚开始做成本分析时被坑过:看到第一天的账单没有折扣就开始怀疑下单失败,其实只是延迟。
5. 自动化落地的进阶经验
5.1 幂等、重试与并发控制
购买 RI 本质是花钱的接口,任何异常都可能造成重复下单或漏单。所以自动化脚本必须考虑幂等和重试。
我在设计脚本时坚持几个原则:每次下单使用固定生成的reservationOrderId,这个 ID 会成为订单的一部分,重试时用同一个 ID,服务端会识别为同一笔请求;重试采用指数退避,比如第一次等 5 秒,第二次 10 秒,第三次 30 秒,最多三次;所有请求都写审计日志,记录请求体、响应码、订单状态,方便事后复盘。
并发控制也要重视。如果你同时发多个下单请求,Azure 的 API 不是无限扩容的,瞬间并发可能触发 429 Throttling 错误。我的经验是每个订阅内的下单请求控制在每秒 1 到 2 个,批量购买时排成一个队列,逐条执行,而不是一口气全部发出。
5.2 把采购流程集成到 FinOps 平台
对于中大型企业,RI 购买通常不是一个“运维直接跑脚本”的动作,而是要经过审批。你可以把 API 购买脚本包装成一个内部工具,只暴露一个参数化的入口:传入 SKU、数量、期限、区域,生成预检报告,由财务确认后执行。
我曾经用这种方式搭过一个简单流程:研发提交资源需求 -> 脚本自动估算对应 SKU 和数量 -> 生成购买草案 -> 财务在审批平台确认 -> CI 流水线执行下单 -> 订单结果自动同步到成本台账。整个过程不需要人工登录门户,而且每一笔订单都能追溯到某个需求工单,月底对账再也不用靠瞎猜。
5.3 异常兜底与成本预算预警
API 自动化提高了效率,同时也提高了出错的“速度”。脚本一旦写错,可能在几分钟内创建大量不必要的预留实例,造成长期成本锁定。
我建议在自动化流程上加两个保护层:一是购买前检查预算额度,比如本月该 SKU 的预估花费是否低于某个阈值;二是购买后立刻设置成本告警,万一订单状态异常,第一时间通知到人。另外,预留实例虽然可以取消或调整,但可能会有退款或违约限制,不要把希望全寄托在“买错了再退”这件事上。
6. 实操中的几条碎片经验
写到这里,再掏几个不算核心但很实用的碎片经验。
其一,token 千万别在脚本里反复获取。一个 access token 的默认有效期通常是一小时,批量下单时一次拿好、到处使用,不会过期。如果每个请求都重新认证,不仅慢,还可能在短时间内把认证限流给打出来。我习惯先取 token,再传入所有下单函数。
其二,所有时间相关的判断都用 UTC 或中国标准时间之一,不要混。Azure API 返回的时间戳总是带Z后缀的 UTC 格式,而中国区账单里往往显示北京时间。对账脚本里如果不统一转换,你会看到两个小时甚至一天的偏差。
其三,如果团队里多个项目要共享 RI,最好提前约定一个命名规范。我在多个账号里见过test、aaa、111这种毫无意义的订单名,半年后再看完全不知道当初买了干什么用。命名的价值在长期运维中会被无限放大,这点怎么强调都不过分。
我个人现在所有的 RI 采购都走 API,不只是因为效率,更因为每次购买都能留下一份结构化的记录。如果有一天财务问我“这个月成本为什么下降了”,我不用靠记忆回答,直接把订单日志拉出来,哪笔订单、对应哪些资源、折扣比例是多少,一目了然。这套方案的投入成本并不高,一个周末基本能跑通,希望对正在关注 Azure China 成本优化的朋友有所启发。