PAYATHON 2026

使用 Terraform 和 Github Actions 创建 Azure subscription 时出现 401

技术老齐

结论

这个 401 UserNotAuthorized 通常不表示 GitHub Actions 登录失败,而是执行 Terraform 的 Service Principal 没有在目标 Enrollment Account 的计费范围内取得有效的订阅创建权限。

Tenant、Management Group 或现有 Subscription 上的 Owner 都属于 Azure RBAC 权限,无法替代 Microsoft.Billing 下的计费角色。即使 SPN 能正常创建其他 Azure 资源,也不能说明它有权在指定的 Enrollment Account 下创建新订阅。

建议先检查以下几项:

  1. principalId 使用的是 Service Principal Object ID,而不是 Application/Client ID 或 App Registration Object ID。
  2. principalTenantId 与 SPN 所在租户一致。
  3. 角色分配在 Terraform 实际使用的 Enrollment Account 上。
  4. Terraform 的 billing_scope_id 指向同一个 Enrollment Account。
  5. 当前计费协议确实是 Enterprise Agreement。MCA 使用不同的权限模型和作用域。
  6. GitHub Actions 中 Terraform 实际使用的身份,就是获得计费角色的 SPN。

为什么 Tenant Owner 仍然不够

这里涉及两套彼此独立的 Azure 授权体系:

  • Azure RBAC:用于管理现有订阅和资源,包括 Owner、Contributor 等角色。
  • Azure Billing RBAC:用于管理 Billing Account、Enrollment Account、Billing Profile、Invoice Section,以及创建订阅。

即使把 Owner 分配到 Tenant Root Management Group,也不会自动获得 Enrollment Account 的计费权限。创建 EA 订阅时,Azure 会按照请求中的 billingScope,单独检查调用者是否具有相应的 Billing role。

因此,下面两种情况可以同时出现:

  • SPN 能登录 Azure,也能创建 Resource Group、Storage Account 等资源。
  • 同一个 SPN 调用 Microsoft.Subscription/aliases 创建订阅时被拒绝。

虽然 HTTP 状态码是 401,但更值得关注的是以下错误内容:

Code="UserNotAuthorized"
Message="User is not authorized to create subscriptions on this enrollment account"

这表示目标 Enrollment Account 的授权检查失败,不能只按客户端密钥错误来处理。

第一步:确认 GitHub Actions 实际使用的身份

使用 Client Secret 时,Terraform 常见的环境变量如下:

env:
  ARM_CLIENT_ID: $
  ARM_CLIENT_SECRET: $
  ARM_TENANT_ID: $

如果还要访问已有订阅,可以同时提供:

env:
  ARM_SUBSCRIPTION_ID: $

可以在工作流中输出非敏感身份信息,确认当前登录身份:

- name: Verify Azure identity
  run: |
    az account show \
      --query '{tenantId:tenantId, subscriptionId:id, user:user}' \
      --output json

不要输出 Client Secret、OIDC Token 或完整 Access Token。

还要检查 Terraform 是否因为环境变量、Provider 配置或 azure/login 设置而选用了另一个身份。仓库级 Secret、Environment Secret 和组织级 Secret 之间可能存在同名覆盖。

第二步:核对 Service Principal Object ID

计费角色分配中的 principalId 必须是 Enterprise Application 对应的 Service Principal Object ID,不能使用以下 ID:

  • Application/Client ID
  • App Registration 的 Object ID
  • 某个用户的 Object ID

可以通过 Client ID 查询 Service Principal Object ID:

az ad sp show \
  --id "<ARM_CLIENT_ID>" \
  --query id \
  --output tsv

命令返回的值才是角色分配请求中应填写的 principalId。

App Registration 页面和 Enterprise Applications 页面可能会显示不同的 Object ID。Billing role assignment 需要绑定的是租户内的 Service Principal 对象。

第三步:检查 Enrollment Account 上的角色分配

请让具有相应 EA 计费管理权限的账号查询目标 Enrollment Account 的角色分配:

az rest \
  --method get \
  --url "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/<billingAccountName>/enrollmentAccounts/<enrollmentAccountName>/billingRoleAssignments?api-version=2019-10-01-preview"

查看返回结果中是否存在对应记录,并核对以下字段:

{
  "properties": {
    "principalId": "<service-principal-object-id>",
    "principalTenantId": "<tenant-id>",
    "roleDefinitionId": "<subscription-creator-role-definition-id>"
  }
}

如果当前 SPN 没有列出 Billing role assignment 的权限,这个查询也可能被拒绝。此时需要由 Enterprise Administrator 或具有该 Enrollment Account 管理权限的人员执行检查。

不能只确认“某处有一条 Subscription Creator 分配”,还要逐项核对:

  • principalId 与 GitHub Actions 使用的 SPN 完全一致。
  • principalTenantId 正确。
  • roleDefinitionId 对应 Subscription Creator。
  • 作用域中的 billingAccountName 正确。
  • 作用域中的 enrollmentAccountName 正确。
  • 角色分配仍然有效,且没有绑定到另一个 Enrollment Account。

billingAccountName 和 enrollmentAccountName 通常是 Azure API 使用的内部标识,不要只根据门户中的显示名称手工拼接。

第四步:核对 Terraform 的 Billing Scope

EA Enrollment Account 通常使用以下作用域:

/providers/Microsoft.Billing/billingAccounts/<billingAccountName>/enrollmentAccounts/<enrollmentAccountName>

Terraform 示例:

variable "billing_account_name" {
  type = string
}

variable "enrollment_account_name" {
  type = string
}

data "azurerm_billing_enrollment_account_scope" "target" {
  billing_account_name    = var.billing_account_name
  enrollment_account_name = var.enrollment_account_name
}

resource "azurerm_subscription" "target" {
  subscription_name = "example-production"
  billing_scope_id  = data.azurerm_billing_enrollment_account_scope.target.id
  workload          = "Production"
}

也可以临时输出作用域进行核对:

output "enrollment_account_scope_id" {
  value = data.azurerm_billing_enrollment_account_scope.target.id
}

不要只检查角色名称。常见的问题是角色分配在 Enrollment Account A,而 Terraform 的 billing_scope_id 指向 Enrollment Account B。

第五步:确认计费协议类型

上面的 Enrollment Account 和 Subscription Creator 流程适用于 Enterprise Agreement。

如果 Billing Account 实际采用 Microsoft Customer Agreement,通常会使用类似下面的作用域:

/providers/Microsoft.Billing/billingAccounts/<billingAccountName>/billingProfiles/<billingProfileName>/invoiceSections/<invoiceSectionName>

MCA 需要在 Billing Profile 或 Invoice Section 范围内配置相应权限,不能直接套用 EA Enrollment Account 的角色分配方式。

可以通过以下命令检查 Billing Account 的协议类型:

az rest \
  --method get \
  --url "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/<billingAccountName>?api-version=2024-04-01"

可用的 API 版本取决于当前 Azure CLI、Azure API 和租户环境。如果该版本不可用,需要按照 Microsoft Billing API 当前支持的版本进行调整。

权限不正确时如何修复

如果角色绑定了错误的 Object ID,建议删除错误的 Billing role assignment,再由有权管理该 Enrollment Account 的账号重新创建。

请求结构如下:

{
  "properties": {
    "principalId": "<service-principal-object-id>",
    "principalTenantId": "<tenant-id>",
    "roleDefinitionId": "<full-subscription-creator-role-definition-resource-id>"
  }
}

roleDefinitionId 应使用目标 Billing Account 和 Enrollment Account 返回的完整角色定义资源 ID。不要凭记忆手工填写角色 GUID。最好先查询该作用域下的 Billing role definitions,再选择 Subscription Creator 对应的定义。

重新分配后,计费授权可能需要一段时间才能生效。权限传播完成后,应让 GitHub Actions 重新进行一次完整的身份验证并再次运行,不要复用角色分配前取得的登录会话。

仍然返回 401 时的排查顺序

可以按以下顺序检查:

  1. 确认工作流中的 ARM_CLIENT_ID。
  2. 将 Client ID 转换为 Service Principal Object ID。
  3. 检查角色分配记录中的 principalId。
  4. 检查 principalTenantId。
  5. 检查 Terraform 最终使用的 billing_scope_id。
  6. 确认 Enrollment Account 处于可用状态,并且仍允许创建订阅。
  7. 确认 Billing Account 的协议类型是 EA,而不是 MCA。
  8. 等待权限传播,然后重新登录。
  9. 使用同一个 SPN 直接调用 Subscription Alias API,以判断问题来自 Terraform 配置还是 Azure Billing 授权。

最后一步会真实创建订阅并产生计费影响。执行时应使用受控的测试名称和正确的计费范围,不要在生产环境中随意操作。

其他注意事项

  • Microsoft.Subscription Provider 的注册问题通常不会触发这条明确指向 Enrollment Account 的授权错误,注册 Provider 不能替代 Billing role assignment。
  • Subscription Creator 权限必须授予实际发起 Alias 创建请求的身份。
  • 使用 GitHub OIDC 时,角色仍然授予 Service Principal Object ID。Federated Credential 只改变登录方式,不会改变 Azure 中的主体对象。
  • 不要直接开启 Terraform TRACE 日志来排查生产工作流。详细日志可能包含敏感的认证信息。
  • 新订阅创建成功后,将其放入 Management Group、分配 Azure RBAC 或部署资源,还会触发其他授权检查。Subscription Creator 不会自动解决这些后续权限问题。

排查时应优先核对 Service Principal Object ID、Enrollment Account 作用域和 Terraform billing_scope_id。其中任何一项不匹配,都可能导致当前的 UserNotAuthorized,即使该 SPN 已经拥有很高的 Azure RBAC 权限。

备注:内容仅供参考。