
先说我为什么折腾这件事。前年我接了一个 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 IDAzure 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_idyour-tenant-id client_idyour-client-id client_secretyour-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_typeclient_credentials \ --data-urlencode client_id${client_id} \ --data-urlencode client_secret${client_secret} \ --data-urlencode scopehttps://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 的话折扣不会自动套用到别的规格上。期限termP1Y表示一年P3Y表示三年。这里要算一笔账三年折扣更高但锁定时间也长如果业务计划不够明确宁可选一年期将灵活性握在自己手里。区域location如chinanorth2中国北部2、chinaeast2中国东部2。RI 的折扣只作用于购买时指定的区域不可能用北京区域的 RI 去覆盖上海区域的开销下单前要反复核对资源组所在的可用区。作用域appliedScopeTypeShared表示共享作用于订阅内所有匹配的虚拟机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-version2022-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_idyour-billing-subscription-id reservation_order_id$(uuidgen) # 生成一个全局唯一 IDmacOS 用 uuidgenLinux 可用 /proc/sys/kernel/random/uuid access_tokenPASTE_YOUR_ACCESS_TOKEN curl -s -X PUT \ https://management.chinacloudapi.cn/providers/Microsoft.Capacity/reservationOrders/${reservation_order_id}?api-version2022-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 spotreservedResourceType: 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-version2022-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 Unauthorizedtoken 无效、过期、格式错误检查 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 成本优化的朋友有所启发。