使用 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 下创建新订阅。
建议先检查以下几项:
principalId使用的是 Service Principal Object ID,而不是 Application/Client ID 或 App Registration Object ID。principalTenantId与 SPN 所在租户一致。- 角色分配在 Terraform 实际使用的 Enrollment Account 上。
- Terraform 的
billing_scope_id指向同一个 Enrollment Account。 - 当前计费协议确实是 Enterprise Agreement。MCA 使用不同的权限模型和作用域。
- 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 时的排查顺序
可以按以下顺序检查:
- 确认工作流中的
ARM_CLIENT_ID。 - 将 Client ID 转换为 Service Principal Object ID。
- 检查角色分配记录中的
principalId。 - 检查
principalTenantId。 - 检查 Terraform 最终使用的
billing_scope_id。 - 确认 Enrollment Account 处于可用状态,并且仍允许创建订阅。
- 确认 Billing Account 的协议类型是 EA,而不是 MCA。
- 等待权限传播,然后重新登录。
- 使用同一个 SPN 直接调用 Subscription Alias API,以判断问题来自 Terraform 配置还是 Azure Billing 授权。
最后一步会真实创建订阅并产生计费影响。执行时应使用受控的测试名称和正确的计费范围,不要在生产环境中随意操作。
其他注意事项
Microsoft.SubscriptionProvider 的注册问题通常不会触发这条明确指向 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 权限。
备注:内容仅供参考。