
如果你的云上成本管理已经走到需要批量购买预留实例RI这一步那你早晚会碰上同一个问题门户里一个一个点真的太慢了。我前年第一次在 Azure China 上批量买 RI 的时候十几个 SKU、几十台配额在 portal.azure.cn 里来回切订阅点得手指发酸一个不留神还把 scope 选错到了生产订阅。后来我彻底转到 API 方式购买把整套流程固化成了内部脚本再没为买 RI 熬过夜。这篇文章就把完整方案写出来中国区专属端点怎么配、服务主体和权限怎么建、令牌怎么拿、购买订单报文的每个字段怎么填、买完如何验证最后附上我踩过的坑。适合成本管理员、运维脚本开发以及想把 RI 采购纳入自动化流程的团队。1. 为什么放着门户不用非要走 API 购买 RI预留实例1.1 三种购买方式的真实边界先说结论门户、CLI、REST API 各有适用场景但凡是量大、频繁、要落审计的需求最终都得走 API。门户是最直观的入口适合一次性、少量购买。比如测试环境临时买一两台点进去选 SKU、选区域、选期限、确认付款两分钟搞定。但它的缺点也很明显一个订单只能配一个 SKU 和一个作用域买 30 个 SKU 就要重复 30 次表单提交门户操作没有结构化输出财务对账全靠截图审计时翻聊天记录和邮件想想都头大门户购买逻辑是给人看的没法做自动化判断比如根据上月用量自动决定买多少。CLI 和 PowerShell 是脚本化的第一步。az reservation purchase和 PowerShell 的New-AzReservation确实能批量跑但终归是包在 REST API外面的一层壳。CLI 方案的问题在于它依赖本地环境和模块版本在中国区环境下的行为跟全球版不完全一致而且如果你的最终目标是把购买请求嵌进内部成本平台CLI 这个壳反而成了集成负担——你还是得读懂它背后调的到底是什么接口、什么参数。REST API 是最终的钥匙。请求你自己构造、订单号你自己控制、返回的 JSON 你可以直接落库重试逻辑、权限模型、审批流全都能按自己的规范来。这也是今天这篇文章展开的重点。1.2 先把 RI 的计费逻辑说清楚很多人第一次接触 RI 会有一个错觉以为买了 RI 就等于预购了几台虚拟机系统会给你预留出实体资源。不是的。RI 买的是计费资格可以理解成公交月票你花钱买一张月票之后一个月内只要坐符合规定的车次就不再单独刷卡。Azure 的做法是在你购买生效之后计费系统会自动把同区域、同 SKU、同代系的普通虚拟机命中原生折扣不绑定任何具体 VM 的实例 ID。这里有几个必须记住的边界RI 只覆盖计算费用。虚拟机的存储、公网 IP、磁盘、快照这些照常按量付费命中的是普通按需 VM低优先级/Spot 这类实例不在覆盖范围内折扣是自动匹配的不需要你在 VM 上做任何标记前提是 VM 的区域和规格跟 RI 一致开启实例灵活性instance flexibility之后同大小族内换规格也能命中这点后面讲字段时会再展开。把RI 是计费权益而不是资源这个底层逻辑想清楚后面所有报文字段就都好理解了你提交的订单本质上是在告诉计费系统我要买一张覆盖哪些区域、哪些规格、几台量、多长时间的优惠卡。1.3 我实际用 API 买 RI 的几个场景场景一季度批量铺量。有一次我们要在 chinaeast2 一次性买 6 个 SKU、14 台 D 系列加 8 台 B 系列的 RI。用门户逐个点不仅要来回切换订阅还要反复核对区域很容易漏。后来写成脚本先循环调 catalog 接口确认每个 SKU 在中国区的现货和价格再逐个下单20 分钟内三个订单全部落地订单号清晰可查。场景二成本平台联动。我们内部有个成本管理平台每个月读完订阅里 VM 的用量自动算出每个 SKU 的建议购买量然后生成采购单。人工审批通过之后平台直接调 API 下单。整个过程里人只做一件事审批。剩下的全部由代码完成。场景三审计需求。资金操作不能只靠门户截图。API 下单后返回的完整 JSON 会落库谁买的、买的什么规格、作用域是什么、花了多少钱全部能追溯。这个对财务年中审计特别有用。2. 中国区端点与服务主体先搭对环境2.1 端点清单中国区不是换个域名后缀那么简单Azure 中国区是由世纪互联独立运营的它有自己的一套 Azure AD、资源管理端点和门户。很多从全球版迁移过来的人第一次跑就挂在端点上因为这不是简单地把azure.com改成chinacloudapi.cn就完事。用途全球版中国区Azure China / 世纪互联门户portal.azure.comportal.azure.cn登录与获取令牌login.microsoftonline.comlogin.chinacloudapi.cnARM 资源管理management.azure.commanagement.chinacloudapi.cnPowerShell 环境名AzureCloudAzureChinaCloudAzure CLI 云环境AzureCloudAzureChinaCloud两边的账号体系是隔离的。你在中国区的 AAD 里创建的服务主体全球版的令牌服务不认识中国区 ARM 也不接受用全球版令牌发来的请求。最稳妥的第一步建议先用命令验证一遍完整链路az cloud set --name AzureChinaCloud az login --service-principal -u 应用ID -p 客户端密码 --tenant 租户ID az account show或者 PowerShell 环境Connect-AzAccount -Environment AzureChinaCloud -ServicePrincipal -ApplicationId 应用ID -Tenant 租户ID -Credential (Get-Credential)跑通这一遍你至少能确定账号、密码、租户、网络都没问题再往下切 REST API 就少一层干扰。2.2 在 portal.azure.cn 里创建服务主体服务主体就是给自动化程序用的机器人账号。为什么不直接用普通用户账号因为常规账号往往开了 MFA一到凌晨自动化跑批的时候就卡在二次验证上而且服务主体可以精确收敛权限出问题也好撤销。创建步骤打开 portal.azure.cn进入Microsoft Entra ID原 Azure Active Directory左侧选应用注册点新注册名称按用途来比如ri-purchase-svc类型选Web重定向 URI 可以临时填https://localhost因为纯 client credentials 流程不会真正跳到这个地址创建完成后在应用详情里找到应用程序(客户端) ID和目录(租户) ID这两个 ID 后面都要用进入证书和机密新建客户端密码有效期建议选 24 个月。创建后立刻复制保存——密码只显示一次关掉页面就再也看不到了。如果用 CLI 创建也可以az ad sp create-for-rbac --name ri-purchase-svc --years 2输出里的appId和password记下来tenant就是租户 ID。2.3 给自动化账号能花钱但别越权的权限购买预留实例本质上是在订阅层级创建一份计费承诺所以服务主体至少要拥有目标订阅的写权限。我见过最省事的做法是直接塞所有者但我不推荐。正确姿势是给订阅分配参与者Contributor或者如果租户里能找到预留实例购买者Reservation Purchaser这类收敛角色优先用后者。分配操作进入订阅 - 访问控制(IAM) - 添加角色分配 - 选择角色 - 搜索服务主体名称 - 保存。CLI 对应命令az role assignment create \ --assignee 应用ID \ --role Contributor \ --scope /subscriptions/订阅ID为什么 Contributor 就够因为购买 RI 会在订阅下创建一个Microsoft.Capacity/reservationOrders类型的资源参与者对订阅内资源有写权限而读者是只读角色买不了报错会是 403 AuthorizationFailed。我们后面第 6 节会专门讲这个坑。有一点要提醒自动化账号的权限越大出事故时的损失就越大。我现在的做法是单独用一个专门给 RI 采购用的订阅服务主体只授这个订阅的参与者权限跟日常运维资源完全隔离。3. 令牌这关必过client credentials 认证完整演练3.1 OAuth 2.0 client credentials 流程原理服务主体拿令牌走的是 OAuth 2.0 的 client credentials 授权模式。你可以把它理解成给机器人办一张门禁卡client_id是卡号client_secret是卡密resource是你要进的那栋楼。grant_typeclient_credentials client_id应用程序ID client_secret客户端密码 resourcehttps://management.chinacloudapi.cn这里最关键的是resource这个参数。它决定令牌颁发出来是去哪个资源的凭证。在中国区必须写成https://management.chinacloudapi.cn写成全球版的https://management.azure.com就等于拿了一张别栋楼的卡到中国区 ARM 门口刷必然 401。这一步看似简单却是无数人踩过的坑。3.2 用 curl 把令牌流程跑通在 Linux、macOS 或者 Git Bash 里先手动验证一下认证链路。注意我特意把 token 请求和后面的查询分开是为了让问题定位更干净tenantId租户ID appId应用程序ID secret客户端密码 # 第 1 步拿令牌 curl -X POST https://login.chinacloudapi.cn/${tenantId}/oauth2/token \ -d grant_typeclient_credentials \ -d client_id${appId} \ -d client_secret${secret} \ -d resourcehttps://management.chinacloudapi.cn返回的 JSON 里重点看这几个字段{ token_type: Bearer, expires_in: 3599, expires_on: 1700000000, access_token: eyJ0eXA... }expires_in通常是 3600 秒左右也就是令牌有效期 1 小时。拿到令牌后所有 ARM 请求都要在 Header 里带Authorization: Bearer access_token。第 2 步用一个只读查询验证令牌能不能真正调通 ARM。这一步强烈建议做能把认证问题和网络问题分开token上一步返回的access_token curl -s -H Authorization: Bearer ${token} \ https://management.chinacloudapi.cn/subscriptions?api-version2020-01-01如果返回里能看到订阅列表说明认证、网络、权限链路已经打通了可以放心进入购买阶段。3.3 用 PowerShell 封装令牌获取日常做自动化我习惯用 PowerShell 封装。Invoke-RestMethod的-Body传 hashtable 时会自动做表单编码比手动拼字符串省事$tenantId 租户ID $appId 应用程序ID $secret 客户端密码 $tokenBody { grant_type client_credentials client_id $appId client_secret $secret resource https://management.chinacloudapi.cn } $authResp Invoke-RestMethod -Method Post -Uri https://login.chinacloudapi.cn/$tenantId/oauth2/token -Body $tokenBody $accessToken $authResp.access_token $headers { Authorization Bearer $accessToken Content-Type application/json }把这个封装成一个函数并且在调用前检查expires_on字段令牌快过期就自动重新获取。之后所有的订单接口都复用这组 Headers跑起来就顺畅了。3.4 认证阶段常见问题对照现象大概率原因处理方式令牌接口返回 invalid_client应用 ID 或密码复制错了重新核对注意别带隐藏空格和换行令牌接口返回 unauthorized_client应用被禁用或客户端类型不对到应用注册里检查状态拿到了令牌ARM 却返回 401resource 配成了全球端点重新取令牌resource 固定用 chinaeast 的 ARM 地址ARM 返回 403错误码 AuthorizationFailed服务主体没有订阅权限到订阅 IAM 分配 Contributor 或预留购买者角色4. 购买订单报文逐字段拆解从目录查到下单4.1 先查目录确认中国区真的有这个 SKU买之前调一次 catalog 接口确认目标 SKU、区域、term、付款计划都存在。这一步不是为了走形式——中国区的 SKU 目录跟全球版并不完全一致尤其是 GPU 系列和较新的 Dv5/Esv5 系列以目录返回为准curl -s -H Authorization: Bearer $token \ https://management.chinacloudapi.cn/subscriptions/订阅ID/providers/Microsoft.Capacity/catalogs?api-version2020-10-01-previewreservedResourceTypeVirtualMachineslocationchinaeast2响应里就是可用 SKU 的列表每个 SKU 会带上规格名比如Standard_D2s_v3、可用期限P1Y、P3Y和付款计划Upfront、Monthly。如果你要买的规格不在目录里就说明这个区域当前不提供该规格的 RI硬买只会报错。这里有个区域命名的细节要注意中国区的位置代码是chinaeast、chinaeast2、chinanorth、chinanorth2以及陆续开放的新区域。不要把全球版的eastasia、eastus这类名字写进来location参数错了也是 400。4.2 购买订单的完整报文购买预留实例的 REST 操作是往 reservationOrders 集合 PUT 一个你自己生成的订单号# 订单号自己生成推荐用 UUID orderId$(uuidgen)Windows PowerShell 下生成 UUID 用[guid]::NewGuid().ToString()。curl -s -X PUT \ -H Authorization: Bearer $token \ -H Content-Type: application/json \ https://management.chinacloudapi.cn/providers/Microsoft.Capacity/reservationOrders/${orderId}?api-version2020-10-01-preview \ -d { sku: { name: Standard_D2s_v3 }, location: chinaeast2, properties: { reservedResourceType: VirtualMachines, billingScopeId: /subscriptions/订阅ID, term: P1Y, billingPlan: Monthly, quantity: 4, displayName: prod-finance-chinaeast2-d2sv3, appliedScopeType: Shared, instanceFlexibility: On } }逐字段拆开看字段说明取值建议sku.name要购买的虚拟机规格从 catalog 接口里选比如 Standard_D2s_v3location中国区区域代码chinaeast / chinaeast2 / chinanorth 等reservedResourceType预留的云资源类型买 VM 固定填 VirtualMachinesbillingScopeId用于支付这份 RI 的订阅格式 /subscriptions/订阅IDterm承诺期限P1Y 一年P3Y 三年billingPlan付款节奏Upfront 一次性预付Monthly 按月摊付quantity购买数量覆盖几台同规格 VM按实际需求填displayName订单显示名建议按用途-业务-区域-规格命名appliedScopeType折扣作用域Shared 共享 / Single 单订阅appliedScopes作用域为 Single 时填订阅数组/subscriptions/订阅IDinstanceFlexibility是否开启实例灵活性On / Off补充一句关于 API 版本的说明2020-10-01-preview是我在中国区实测过比较稳的版本写博客时保留这个是为了你跑命令能直接复现。如果官方文档已经推了更新的 GA 版本可以先在你的中国区租户里试万一返回Api version not found直接退回这个 preview 版本。4.3 billingScopeId 和 appliedScope 不是一回事这是新手最容易绕晕的一组概念。我分开讲。billingScopeId说的是这笔钱从哪个账上出。RI 本质上是一份计费承诺账单会落到你指定的这个订阅/账务实体。appliedScopeType说的是这份优惠覆盖哪些范围的虚拟机。Shared表示整个账务范围内所有订阅里只要匹配到区域和规格都能被这份 RI 命中Single表示只作用于指定的某单个订阅。举个真实例子你在订阅 A 的账上买了 4 台 D2s_v3 的 RIscope 设成Shared那么订阅 B、C 里只要开了同区域同规格的普通 VM也会被这份 RI 命中。对需要成本共享的团队来说很方便但也容易误伤——如果 A、B、C 分属不同业务部门各自要做成本核算Shared 会让账单里的抵扣变得说不清这时候就应该用Single把 scope 锁在各自的订阅上。所以在设计采购方案之前先想清楚权益归属这是一份全公司共享福利还是某一套系统的专属折扣这个决定比后面的任何参数都重要。4.4 term、billingPlan 与付款节奏选择term 只有P1Y和P3Y两个值。3 年期折扣通常更高但锁定时间更长适合长期稳定运行的常驻负载1 年期灵活一些适合还在快速演进的业务。billingPlan 分Upfront和MonthlyUpfront下单时一次性付清全款。现金流一次性支出大但总价通常有额外优惠Monthly把总费用摊到每个计费月。API 会按订单生成月度还款计划对现金流友好。但要保证订阅账户状态健康、支付方式有效否则月度计划可能被卡住。我自己的建议测试环境、弹性波动大的负载优先UpfrontP1Y别把预算锁死太久核心生产、长期稳定的服务再考虑P3Y拉低单价如果要用Monthly先确认目标订阅支持月度付款。最直接的办法是看 catalog 接口返回里该 SKU 的billingPlans字段里面没有Monthly就说明不支持。4.5 幂等重试订单号由你控制这是我特别喜欢这个 API 设计的一点reservationOrderId不是服务端生成的而是你自己生成的一个 GUID。好处非常实际网络超时、返回 UnknownError 时你可以用同一个订单号原样重发不会重复扣款服务端会通过订单号识别出这是同一个订单返回一致的结果因为是你自己占的号整个重试过程完全可控。我的落地规范是订单号在代码里生成后先和请求内容一起写入日志重试最多三次间隔按 5 秒、30 秒、120 秒退避第三次仍失败就停下来人工介入绝不无限重试。这条规则看着简单但真能救你一命——第 6 节里我写了自己遭遇过的重复下单事故就是没遵守这个规范。5. 下单之后的验证链路订单状态与折扣命中5.1 查询订单状态PUT 完不等于生效购买接口是异步落地的。PUT 请求返回 200/201 只代表订单被接收不代表权益立刻生效。紧接着要轮询订单状态curl -s -H Authorization: Bearer $token \ https://management.chinacloudapi.cn/providers/Microsoft.Capacity/reservationOrders/${orderId}?api-version2020-10-01-preview重点看properties.provisioningStatePending订单正在处理别急Provisioned已生效权益可以用Confirmed已确认Failed/Cancelled处理失败需要查订单详情里的错误信息。我的做法是每 10 秒轮询一次最多 10 次。多数订单会在几十秒内从 Pending 走到 Provisioned。如果一直卡住就去查error字段通常能看到具体原因。列出当前租户下所有订单用来做日常巡检curl -s -H Authorization: Bearer $token \ https://management.chinacloudapi.cn/providers/Microsoft.Capacity/reservationOrders?api-version2020-10-01-preview5.2 订单内部的 reservation 拆分购买订单下面会挂着具体的预留实例条目。比如你 quantity 填了 4订单返回体里通常会有多条 reservation 资源每条对应一个计费权益。做对账的时候要以reservations数组里的逐条记录为准不要只看外层的 quantity。这一点在后续做成本分摊时特别重要。多订阅共享 scope 时各业务部门分摊的额度不是按订单总量一刀切的而是要看这份订单在账单周期里实际被哪些 VM 命中、命中了多少小时。把这些数据从 API 拉下来才能算清楚每个部门的真实 RI 使用率。5.3 三条路径验证折扣真的打上了买完 RI 之后折扣不是人工绑定的而是计费系统在出账时自动匹配。我验证成果通常走三条路径第一API 层面查订单的sku.name、location、appliedScopeType跟目标 VM 是否对得上。区域必须完全一致SKU 必须同名同代系scope 必须能覆盖到那台 VM 所属的订阅。第二门户层面登录 portal.azure.cn在预留实例页面看这台 RI 的匹配 VM 列表和使用率。我的标准是匹配数量符合预期、使用率接近 100%才说明这份 RI 买得值。使用率长期低于 80%就意味着有余量浪费。第三出账层面下期账单里RI 行项目的成本应该体现为 0已预付或只体现当月摊付金额对应 VM 的计算费用被抵扣。如果还是按全额出现在账单里就说明匹配没生效要回头查 scope 和区域。尤其要记住一点RI 只抵扣计算费用。虚拟机的存储、云盘、公网 IP 都是额外按量计费的别在预算里把 RI 当成所有 VM 成本归零。5.4 常见状态码速查HTTP 状态码含义应对200 / 201请求成功购买请求还需继续轮询订单状态400请求参数不合法逐字段检查 location、SKU、term401令牌无效、过期或 resource 配错重新取令牌403权限不足或订阅未授权检查订阅 IAM 和 Microsoft.Capacity 注册状态404订单号不存在核对 URL 里的订单 ID429触发限流按退避策略重试关于 403 有一个容易忽略的补充点某些历史较久的订阅可能没有注册Microsoft.Capacity资源提供程序调用会报订阅未注册命名空间。遇到这种先执行一次注册再重试az provider register -n Microsoft.Capacity --subscription 订阅ID6. 中国区踩坑实录与自动化落地建议6.1 坑一resource 写成全球端点死活 401这是我在最早一批脚本上踩过的。当时拿着全球版脚本改token 端点换了唯独resourcehttps://management.azure.com没动。结果令牌每次都能正常取到但调到中国区 ARM 一律 401。排查了半天才意识到是门禁卡办错楼了。查看令牌返回里的 audience 字段可以验证中国区令牌的 audience 一定是https://management.chinacloudapi.cn。6.2 坑二权限给到了资源组而不是订阅RI 订单是创建在订阅层级的资源。有次我把服务主体的参与者角色授在了某个资源组上调购买接口时返回 403错误信息会直接说客户端没有对作用域的授权。后来我把采购专用的服务主体固定授在订阅级参与者再配合一个独立的采购订阅权限边界清晰排查也快。6.3 坑三中国区没有全球版的 SKU某次我想买某个 GPU 系列的 RI查 catalog 接口发现中国区目录里根本没有这个规格而全球版价格页上是有的。这件事让我彻底记住了别拿全球版的 SKU 清单直接推中国区所有采购决策先过 catalog。中国区 VM 规格的更新节奏跟全球版不完全同步尤其新系列和高端 GPU 系列差距会更明显。6.4 坑四重复下单与幂等设计有一年批量操作时遇到一次网络超时我以为下单没成功于是换了个新的订单号重发。结果同一个订阅、同一个 SKU 下了两单多买了一倍量。事后分析根因就是没有用同一个订单号重试的幂等机制。从那次起我强制规定每次购买请求必须先落库订单号重试时原号重发失败超过三次人工复核。这条规则现在写进了团队的自动化规范里。6.5 自动化落地把购买请求封装成内部服务如果你们团队打算把 RI 采购做成常规流程我建议按下面这个结构组织而不是让每个人直接对着 API 调下单服务封装成内部 API入参是 SKU、区域、数量、term、billingPlan、scope出参是订单号和状态。业务方不接触裸 API权限和审计都好控制。策略引擎每月读一次 VM 用量计算每个 SKU 的建议购买量。简单版本可以用过去 30 天同规格同时在线最大数量作为基线激进一点按 90 天峰值但要预留容量缓冲。审批门禁机器算出建议量人工在审批单上确认后才允许调用下单服务。所有购买动作留痕。对账巡检每天拉一次订单列表跟 CMDB 里的采购单比对发现多买、漏买、使用率异常就告警。最后分享一个我现在一直在用的细节所有新采购 RI 的displayName都按用途-业务单元-区域-规格的格式生成比如prod-finance-chinaeast2-d2sv3。这个命名规则看起来无关紧要但当你对着几十个订单做对账、排障、甚至在调整权益时能省下大量猜谜时间。买 RI 这件事量大了之后拼的已经不是会不会调接口而是流程是否闭环、命名是否规范、重试是否安全。我个人在踩过重复下单的坑之后对先落库、再下单、同号重试这条纪律的执行要比任何技术参数都上心。