AirGate/火山引擎IAM子账号管控工具_深度研究报告.md
seaislee1209 a7e030dc57 feat: auto-authorize policies when adding projects to sub-accounts
- Disable now removes all policies (saved to DB) + Enable restores them
- Project add: policies are now user-selected (checkbox), not auto-attached
- Fix serializer allow_blank for optional fields (email/phone/password)
- Better error reporting for policy detach/attach failures
- Handle duplicate user creation with clear error message

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 15:01:18 +08:00

1214 lines
44 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 火山引擎 IAM 子账号管控工具 -- 深度研究报告
> 研究日期2026-03-19
> 目标:通过火山引擎 Open API实现对 IAM 子账号的全面管控,包括权限隔离、消费监控、告警、自动停用等功能。
---
## 目录
1. [整体架构方案](#1-整体架构方案)
2. [API 认证与签名机制](#2-api-认证与签名机制)
3. [IAM 用户管理 API](#3-iam-用户管理-api)
4. [权限策略管理](#4-权限策略管理)
5. [API 密钥管理](#5-api-密钥管理)
6. [计费与消费查询 API](#6-计费与消费查询-api)
7. [预算与告警机制](#7-预算与告警机制)
8. [子账号自动停用/恢复方案](#8-子账号自动停用恢复方案)
9. [项目管理与资源隔离](#9-项目管理与资源隔离)
10. [SDK 与工具链](#10-sdk-与工具链)
11. [可执行实施方案](#11-可执行实施方案)
12. [限制与注意事项](#12-限制与注意事项)
13. [参考文档](#13-参考文档)
---
## 1. 整体架构方案
### 1.1 核心需求
| 需求 | 实现方式 | 可行性 |
|------|----------|--------|
| 子账号不能看到主账号信息 | IAM 默认零权限 + 显式 Deny 策略 | **完全可行** |
| 子账号仅有 Seedance 2.0 + TOS 权限 | 仅附加 ArkFullAccess + TOSFullAccess 策略 | **完全可行** |
| 子账号能看到自己的账单 | 通过 AirGate 按多项目聚合查询,主账号代查展示,可按项目查看明细 | **完全可行** |
| 子账号不能看到其他账号消费/余额 | 不授予 billing/bss 权限 + 显式 Deny | **完全可行** |
| 消费达到阈值发告警 | 额度划拨制 + 阶梯式告警50%/80%/90%+ 飞书通知 | **完全可行** |
| 消费达到阈值自动停用 | 消费达到已划拨额度 100% 时自动停用 | **完全可行** |
| 一键恢复子账号 | 调用 IAM API 重新启用 | **完全可行** |
### 1.2 架构图
```
┌──────────────────────────────────────────────────────┐
│ 管控工具 (后端服务) │
│ │
│ ┌─────────┐ ┌──────────┐ ┌────────────┐ │
│ │ IAM管理 │ │ 消费监控 │ │ 告警引擎 │ │
│ │ 模块 │ │ 模块 │ │ 模块 │ │
│ └────┬─────┘ └────┬─────┘ └────┬───────┘ │
│ │ │ │ │
│ ┌────▼─────────────▼─────────────▼───────┐ │
│ │ 火山引擎 Open API 调用层 │ │
│ │ (HMAC-SHA256 签名认证) │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
│ │ │
┌────▼────┐ ┌─────▼─────┐ ┌────▼─────┐
│ IAM API │ │Billing API│ │CloudMonitor│
│iam.vol..│ │billing.vol│ │open.vol.. │
└─────────┘ └───────────┘ └────────────┘
```
### 1.3 关键发现
> **重要**:火山引擎的 IAM 子用户**没有独立的计费账户**。所有费用归属主账号。子账号的消费追踪需要通过**项目Project**或**标签Tag**维度来实现,由主账号通过 Billing API 查询后聚合展示。
---
## 2. API 认证与签名机制
### 2.1 签名算法
火山引擎使用 **HMAC-SHA256** 签名(类似 AWS Signature V4
**签名流程:**
```
1. 构造规范请求 (Canonical Request)
= HTTP方法 + 路径 + 排序后的查询参数 + 规范头部 + 签名头列表 + Body哈希
2. 构造待签名字符串 (String to Sign)
= "HMAC-SHA256" + 时间戳 + 凭证范围 + SHA256(规范请求)
3. 派生签名密钥
kDate = HMAC-SHA256(SecretKey, 日期)
kRegion = HMAC-SHA256(kDate, "cn-north-1")
kService = HMAC-SHA256(kRegion, 服务名)
kSigning = HMAC-SHA256(kService, "request")
4. 计算签名
Signature = Hex(HMAC-SHA256(kSigning, 待签名字符串))
```
### 2.2 完整 Python 签名实现
```python
import datetime
import hashlib
import hmac
import requests
from urllib.parse import quote
class VolcengineClient:
"""火山引擎 API 客户端,处理 HMAC-SHA256 签名认证"""
def __init__(self, ak: str, sk: str, service: str, host: str,
region: str = "cn-north-1", version: str = "2018-01-01"):
self.ak = ak
self.sk = sk
self.service = service
self.host = host
self.region = region
self.version = version
def _norm_query(self, params: dict) -> str:
query = ""
for key in sorted(params.keys()):
if isinstance(params[key], list):
for v in params[key]:
query += quote(key, safe="-_.~") + "=" + quote(str(v), safe="-_.~") + "&"
else:
query += quote(key, safe="-_.~") + "=" + quote(str(params[key]), safe="-_.~") + "&"
return query[:-1].replace("+", "%20") if query else ""
def _hmac_sha256(self, key: bytes, content: str) -> bytes:
return hmac.new(key, content.encode("utf-8"), hashlib.sha256).digest()
def _hash_sha256(self, content: str) -> str:
return hashlib.sha256(content.encode("utf-8")).hexdigest()
def call(self, action: str, params: dict = None, body: str = "") -> dict:
"""调用火山引擎 API
Returns:
dict: API 响应。如果 ResponseMetadata 中包含 Error会抛出 RuntimeError。
"""
params = params or {}
now = datetime.datetime.now(datetime.timezone.utc)
x_date = now.strftime("%Y%m%dT%H%M%SZ")
short_date = x_date[:8]
x_content_sha256 = self._hash_sha256(body)
all_params = {"Action": action, "Version": self.version, **params}
signed_headers_str = "content-type;host;x-content-sha256;x-date"
canonical_headers = (
f"content-type:application/x-www-form-urlencoded\n"
f"host:{self.host}\n"
f"x-content-sha256:{x_content_sha256}\n"
f"x-date:{x_date}"
)
query_string = self._norm_query(all_params)
canonical_request = "\n".join([
"GET", "/", query_string,
canonical_headers, "", signed_headers_str, x_content_sha256
])
credential_scope = f"{short_date}/{self.region}/{self.service}/request"
string_to_sign = "\n".join([
"HMAC-SHA256", x_date, credential_scope,
self._hash_sha256(canonical_request)
])
k_date = self._hmac_sha256(self.sk.encode("utf-8"), short_date)
k_region = self._hmac_sha256(k_date, self.region)
k_service = self._hmac_sha256(k_region, self.service)
k_signing = self._hmac_sha256(k_service, "request")
signature = self._hmac_sha256(k_signing, string_to_sign).hex()
headers = {
"Host": self.host,
"X-Date": x_date,
"X-Content-Sha256": x_content_sha256,
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": (
f"HMAC-SHA256 Credential={self.ak}/{credential_scope}, "
f"SignedHeaders={signed_headers_str}, Signature={signature}"
)
}
# 重要:不能使用 requests.get(url, params=...),因为 requests 库会自行
# URL 编码参数,其编码方式可能与签名时使用的 _norm_query 不一致,
# 导致签名校验失败401 错误)。必须手动拼接 query string。
url = f"https://{self.host}/?{query_string}"
r = requests.get(url, headers=headers)
resp = r.json()
# 检查 API 错误
error = resp.get("ResponseMetadata", {}).get("Error")
if error:
raise RuntimeError(
f"Volcengine API Error [{action}]: "
f"{error.get('Code', 'Unknown')} - {error.get('Message', '')}"
)
return resp
```
---
## 3. IAM 用户管理 API
**服务端点:** `https://iam.volcengineapi.com/`
**API 版本:** `2018-01-01`
**服务代码:** `iam`
### 3.1 用户生命周期 API
| Action | 说明 | 关键参数 |
|--------|------|----------|
| `CreateUser` | 创建子用户 | `UserName`(必填), `DisplayName`, `Email`, `MobilePhone` |
| `GetUser` | 查询用户详情 | `UserName` |
| `UpdateUser` | 更新用户信息 | `UserName`, `NewUserName`, `NewDisplayName` 等 |
| `ListUsers` | 列出所有用户 | `Limit`, `Offset`(分页) |
| `DeleteUser` | 删除用户 | `UserName` |
### 3.2 登录管理 API控制台访问开关
| Action | 说明 | 关键参数 |
|--------|------|----------|
| `CreateLoginProfile` | 开通控制台登录 | `UserName`, `Password`, `LoginAllowed`, `PasswordResetRequired` |
| `GetLoginProfile` | 查询登录状态 | `UserName` |
| `UpdateLoginProfile` | **启用/停用用户** | `UserName`, `LoginAllowed`(true/false) |
| `DeleteLoginProfile` | 删除登录能力 | `UserName` |
> **关键能力**`UpdateLoginProfile` + `LoginAllowed=false` 可以**停用子账号的控制台访问**,设为 `true` 即可**一键恢复**。
### 3.3 GetLoginProfile 响应字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `LoginAllowed` | Boolean | 是否允许登录 |
| `LastLoginDate` | String | 最后登录时间 |
| `LastLoginIp` | String | 最后登录 IP |
| `LoginLocked` | Boolean | 是否被锁定 |
| `SafeAuthFlag` | Boolean | 是否开启 MFA |
### 3.4 用户组管理 API
| Action | 说明 |
|--------|------|
| `CreateGroup` | 创建用户组 |
| `AddUserToGroup` | 添加用户到组 |
| `RemoveUserFromGroup` | 移出用户组 |
| `ListGroupsForUser` | 查询用户所在组 |
| `ListUsersForGroup` | 查询组内用户 |
---
## 4. 权限策略管理
### 4.1 策略操作 API
| Action | 说明 |
|--------|------|
| `CreatePolicy` | 创建自定义策略 |
| `GetPolicy` | 查询策略详情 |
| `UpdatePolicy` | 更新策略 |
| `DeletePolicy` | 删除策略 |
| `ListPolicies` | 列出所有策略 |
| `AttachUserPolicy` | 将策略附加到用户 |
| `DetachUserPolicy` | 从用户分离策略 |
| `ListAttachedUserPolicies` | 查询用户已附加的策略 |
| `AttachPolicyInProject` | 在项目范围内附加策略 |
| `DetachPolicyInProject` | 在项目范围内分离策略 |
### 4.2 系统预置策略(关键)
| 策略名 | 适用场景 | 说明 |
|--------|----------|------|
| `ArkFullAccess` | Seedance 2.0 | 方舟平台完整管理权限(含模型、端点、微调) |
| `ArkStandardGlobalAccess` | Seedance 2.0 | 标准使用权限(不含模型上线) |
| `ArkReadOnlyAccess` | Seedance 2.0 | 只读权限 |
| `TOSFullAccess` | 对象存储 | TOS 完整管理权限 |
| `TOSReadOnlyAccess` | 对象存储 | TOS 只读权限 |
| `AccessKeySelfManageAccess` | API 密钥 | 用户仅能管理自己的 API 密钥 |
### 4.3 策略文档格式
```json
{
"Statement": [
{
"Effect": "Allow | Deny",
"Action": ["服务代码:操作名称"],
"Resource": ["trn:服务代码:区域:账号ID:资源路径"],
"Condition": {
"条件运算符": {
"条件键": ["值"]
}
}
}
]
}
```
**TRN资源名称格式** `trn:${ServiceCode}:${Region}:${AccountId}:${ResourcePath}`
示例:
- IAM 用户:`trn:iam::2100000000001:user/Bob`
- TOS 存储桶:`trn:tos:::my-bucket`
- TOS 对象:`trn:tos:::my-bucket/path/*`
### 4.4 推荐的子账号策略配置
#### 策略一:允许 Seedance 2.0 + TOS使用系统策略
**方案 A -- 全局授权**(不需要项目隔离时):
```
AttachUserPolicy: PolicyName=ArkFullAccess, PolicyType=System
AttachUserPolicy: PolicyName=TOSFullAccess, PolicyType=System
AttachUserPolicy: PolicyName=AccessKeySelfManageAccess, PolicyType=System
```
**方案 B -- 项目级授权**(推荐,需要隔离不同子账号的资源):
```
AttachUserPolicy: PolicyName=AccessKeySelfManageAccess, PolicyType=System # 全局
AttachPolicyInProject: PolicyName=ArkFullAccess, ProjectName=DeptA-Project # 限定在项目内
AttachPolicyInProject: PolicyName=TOSFullAccess, ProjectName=DeptA-Project # 限定在项目内
```
> **注意**:方案 A 和方案 B 不能混用。如果同时全局附加和项目级附加同一策略,全局策略会使项目限制失效。
#### 策略二:禁止查看主账号信息(自定义 Deny 策略)
> **注意**:此策略故意**排除了** `iam:CreateAccessKey`、`iam:UpdateAccessKey`、`iam:DeleteAccessKey`、`iam:ListAccessKeys`、`iam:GetAccessKeyLastUsed` 等密钥自管理操作,以避免与 `AccessKeySelfManageAccess` 策略冲突。因为 **Deny 优先于 Allow**,如果这里 deny 了 `iam:*`,子账号将无法管理自己的 API 密钥。
```json
{
"Statement": [
{
"Effect": "Deny",
"Action": [
"iam:ListUsers",
"iam:GetUser",
"iam:ListGroups",
"iam:GetGroup",
"iam:ListRoles",
"iam:GetRole",
"iam:ListPolicies",
"iam:GetPolicy",
"iam:ListAttachedUserPolicies",
"iam:ListAttachedRolePolicies",
"iam:ListEntitiesForPolicy",
"iam:GetLoginProfile",
"iam:GetSecurityConfig",
"iam:CreateUser",
"iam:UpdateUser",
"iam:DeleteUser",
"iam:CreateGroup",
"iam:UpdateGroup",
"iam:DeleteGroup",
"iam:CreateRole",
"iam:UpdateRole",
"iam:DeleteRole",
"iam:CreatePolicy",
"iam:UpdatePolicy",
"iam:DeletePolicy",
"iam:AttachUserPolicy",
"iam:DetachUserPolicy",
"iam:AttachRolePolicy",
"iam:DetachRolePolicy",
"iam:CreateLoginProfile",
"iam:UpdateLoginProfile",
"iam:DeleteLoginProfile",
"iam:AddUserToGroup",
"iam:RemoveUserFromGroup",
"iam:ListUsersForGroup",
"iam:ListGroupsForUser",
"iam:SetSecurityConfig",
"iam:CreateServiceLinkedRole",
"iam:DeleteServiceLinkedRole",
"iam:AttachUserGroupPolicy",
"iam:DetachUserGroupPolicy",
"iam:ListAttachedUserGroupPolicies",
"iam:AssumeRole"
],
"Resource": ["*"]
},
{
"Effect": "Deny",
"Action": [
"billing:*",
"bss:*"
],
"Resource": ["*"]
},
{
"Effect": "Deny",
"Action": [
"organization:*"
],
"Resource": ["*"]
}
]
}
```
> **原理**IAM 子用户**默认没有任何权限**。即使不加 Deny 策略,子用户也看不到主账号信息。但显式 Deny 可以防止其他策略意外授权。Deny 优先级始终高于 Allow。
>
> **关键设计**Deny 策略中明确列出了要禁止的 IAM 操作,而**没有使用 `iam:*` 通配符**。这样不会阻断 `AccessKeySelfManageAccess` 授予的密钥自管理能力(`iam:CreateAccessKey`、`iam:UpdateAccessKey`、`iam:DeleteAccessKey`、`iam:ListAccessKeys`)。
#### 策略三:允许用户管理自己的 API 密钥
已有系统预置策略 `AccessKeySelfManageAccess`,直接附加即可。
#### 策略四TOS 限定到指定存储桶
```json
{
"Statement": [
{
"Effect": "Allow",
"Action": ["tos:*"],
"Resource": [
"trn:tos:::department-bucket",
"trn:tos:::department-bucket/*"
]
},
{
"Effect": "Allow",
"Action": ["tos:ListBuckets"],
"Resource": ["*"]
}
]
}
```
---
## 5. API 密钥管理
| Action | 说明 | 关键参数 |
|--------|------|----------|
| `CreateAccessKey` | 创建 API 密钥对 | `UserName`(可选,不填=为自己创建) |
| `ListAccessKeys` | 列出用户的密钥 | `UserName` |
| `UpdateAccessKey` | **启用/停用密钥** | `AccessKeyId`, `Status`(active/inactive) |
| `DeleteAccessKey` | 删除密钥 | `AccessKeyId` |
| `GetAccessKeyLastUsed` | 查询密钥最后使用 | `AccessKeyId` |
### 重要限制
- **每个用户最多 2 个 API 密钥**
- **SecretAccessKey 仅在创建时返回一次**,之后无法再获取
- 停用密钥后,使用该密钥的所有 API 调用将立即失败
### 停用子账号的 API 访问
```python
# 停用密钥 = 立即切断子账号的所有 API 调用能力
iam_client.call("UpdateAccessKey", {
"AccessKeyId": "AKLT****",
"Status": "inactive",
"UserName": "sub_user_1"
})
```
### 恢复子账号的 API 访问
```python
# 恢复密钥 = 一键恢复
iam_client.call("UpdateAccessKey", {
"AccessKeyId": "AKLT****",
"Status": "active",
"UserName": "sub_user_1"
})
```
---
## 6. 计费与消费查询 API
**服务端点:** `https://billing.volcengineapi.com`
**API 版本:** `2022-01-01`
**服务代码:** `billing`
**QPS 限制:** 5 QPS
### 6.1 账单查询 API
| Action | 说明 | 粒度 |
|--------|------|------|
| `ListBillOverviewByCategory` | 按类别汇总 | 月 |
| `ListBillOverviewByProd` | 按产品汇总 | 月 |
| `ListBill` | 账单流水 | 月 |
| `ListBillDetail` | **明细账单**(最细粒度) | 日/月 |
| `ListSplitBillDetail` | 分账账单(按资源拆分) | 月 |
| `ListAmortizedCostBillDaily` | 每日摊销成本 | 日 |
### 6.2 ListBillDetail 关键参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `BillPeriod` | String | 是 | 格式 YYYY-MM近 24 个月 |
| `Limit` | Integer | 是 | 每页数量 1-300 |
| `Offset` | Integer | 否 | 分页偏移 |
| `OwnerID` | Array[Long] | 否 | 按资源拥有者筛选 |
| `Product` | Array[String] | 否 | 按产品筛选 |
| `GroupTerm` | Integer | 否 | 0=明细, 1=实例, 2=产品, 3=账号 |
| `GroupPeriod` | Integer | 否 | 0=账期, 1=日, 2=详情 |
| `ExpenseDate` | String | 否 | 特定日期(需 GroupPeriod=1 |
| `InstanceNo` | String | 否 | 实例 ID 筛选 |
### 6.3 按子账号追踪消费的方法
> **核心问题**IAM 子账号没有独立的计费维度。不能直接按 IAM UserName 查询消费。
**AirGate 采用的方案:多项目聚合追踪**
一个子账号可以关联多个火山项目,每个项目有独立的监测开关。消费追踪按**所有开启监测的项目消费之和**计算。
```
子账号 (seaislee)
├── 项目A: Seedance-团队1 ← 开启监测 → 消费 ¥30,000
├── 项目B: Seedance-团队2 ← 开启监测 → 消费 ¥20,000
├── 项目C: Seedance-团队3 ← 开启监测 → 消费 ¥15,000
├── 项目D: 测试项目 ← 关闭监测(不计入)
└── 项目E: 内部工具 ← 关闭监测(不计入)
累计消费 = 项目A + 项目B + 项目C = ¥65,000仅算开启监测的
已划拨额度: ¥100,000
使用率: 65% → 50% 告警已触发
```
**典型使用场景:**
- 一个部门子账号下,每个团队各创建一个火山项目
- 每个项目下各有一个 Seedance 2.0 API 端点
- 管理员可按需开关某些项目的监测(如测试项目不计费)
- 告警和自动停用基于所有开启项目的消费总和 vs 划拨额度
**消费查询方式:** 对每个开启监测的项目分别调用 `ListBillDetail`(按 Project 字段筛选),累加得出总消费。同时记录每个项目的独立消费,前端可展开查看明细。
### 6.4 账户余额查询
```python
billing_client = VolcengineClient(AK, SK, "billing", "billing.volcengineapi.com",
version="2022-01-01")
# 查询账户余额
balance = billing_client.call("QueryBalanceAcct")
# 返回:可用余额、冻结金额等
```
### 6.5 数据时效性
| 数据类型 | 可用时间 |
|----------|----------|
| 上月完整账单 | 每月 2 日 12:00 |
| 日粒度账单 | T+1 ~ T+2 天 |
| 分账账单 | ~2 天延迟 |
| 摊销成本 | ~2 天延迟 |
| **实时账单** | **不支持** |
---
## 7. 预算与告警机制
### 7.1 预算管理 APIBilling 模块)
| Action | 说明 |
|--------|------|
| `CreateBudget` | 创建预算 |
| `UpdateBudget` | 更新预算 |
| `ListBudget` | 查询预算列表 |
| `QueryBudgetDetail` | 查询预算详情 |
| `DeleteBudget` | 删除预算 |
| `ListBudgetAmountByBudgetID` | 查询预算金额 |
| `ListRecipientInformation` | 查询告警接收人 |
**预算可按以下维度筛选:**
- 区域、产品、标签、项目、账号 OwnerID、付款人 PayerID、可用区、计费模式
### 7.2 CloudMonitor Webhook 告警
**端点:** `https://open.volcengineapi.com?Action={Action}&Version=2018-01-01`
| Action | 说明 |
|--------|------|
| `CreateRule` | 创建告警规则(支持 Webhook |
| `CreateWebhook` | 配置 Webhook 回调地址 |
| `CreateContacts` | 添加告警联系人 |
| `CreateContactGroup` | 创建通知组 |
**支持的通知渠道:**
- 站内信(默认开启)
- Email
- SMS
- 飞书Feishu
- 钉钉DingTalk
- 企业微信
- 自定义 WebhookHTTP POST 回调)
### 7.3 Ark 推理限额Seedance 专属)
火山方舟Ark平台有**推理限额**功能:
- 可设置每个模型的最大 Token 消耗量
- **达到限额后服务自动暂停**
- 最小调整间隔 2 小时
- 仅支持在线推理(不含批量)
- 目前仅支持通过控制台设置,**暂无公开 API**
### 7.4 AirGate 自建方案:额度划拨制 + 阶梯式告警
由于火山原生的预算告警仅支持站内信/邮件/短信通知不支持自动停用。AirGate 采用**额度划拨制**自建:
```
主账号通过 AirGate 给子账号划拨额度(如 10 万元)
▼ 定时任务每小时查询 Billing API
遍历子账号下所有开启监测的项目 → 分别查询消费 → 累加得出总消费
▼ 总消费对比已划拨额度
├── 消费达到额度 50% → 飞书告警
├── 消费达到额度 80% → 飞书告警
├── 消费达到额度 90% → 飞书告警
└── 消费达到额度 100% → 自动停用子账号 + 飞书告警
额度用完 → 主账号在 AirGate 追加额度 → 告警状态自动重置 → 恢复子账号
额度给多了 → 主账号在 AirGate 扣减额度 → 告警状态自动重置
```
**关键设计:**
- **多项目聚合**:一个子账号可关联多个火山项目,每个项目有独立监测开关。消费 = 所有开启监测的项目消费之和
- **项目即权限**:添加项目时自动调用 `AttachPolicyInProject` 在项目范围内授权,移除项目时自动回收权限。子账号只能操作被授权的项目,碰不到其他人的资源。**添加项目时授权哪些策略由管理员在弹窗中手动选择**(从下拉列表选,支持多选,默认不预选任何策略),避免系统自动附加不需要的权限
- **项目明细可查**:前端可展开查看每个项目的独立消费,便于分析哪个团队/项目花得多
- **非月度制**:额度不按月重置,是一次性划拨,用完再充
- **可追加可扣减**:主账号可随时追加额度(+5万或扣减额度-3万支持灵活调整
- **扣减保护**:扣减后总额度不能低于已消费金额(否则会立即触发停用)
- **阶梯式告警**:每个子账号可自定义告警百分比(如 [50, 80, 90]),每档只通知一次
- **额度变更即重置告警**:追加或扣减额度后,已触发的告警状态自动清空,按新的使用率重新计算
- **累计消费**:跨月累计,通过 Billing API 各月数据求和得出
- **操作留痕**:每次划拨/扣减都记录操作人、金额、备注,可追溯
---
## 8. 子账号自动停用/恢复方案
### 8.1 完全停用子账号(保留账号,可恢复)
需要**同时**执行三个操作才能完全停用:
> **重要发现2026-03-20 实测验证)**:仅停用控制台登录 + 停用 API 密钥不够。如果子账号已经登录了火山控制台(浏览器会话未过期),他仍然可以继续操作(如在体验中心生成视频)。**必须同时移除所有权限策略**,这样即使会话未过期,刷新页面后任何操作都会返回"权限不足"。
```python
def disable_sub_user(iam_client, username: str, access_key_ids: list = None):
"""完全停用子账号(保留账号,可一键恢复)
三步停用:
1. 停用控制台登录(阻止新登录)
2. 停用所有 API 密钥(阻止 API 调用)
3. 移除所有权限策略(已登录的会话也无法操作)
移除的策略列表保存到本地数据库,恢复时自动加回。
"""
# 0. 如果未传入 access_key_ids自动查询
if access_key_ids is None:
access_key_ids = get_user_access_keys(iam_client, username)
# 1. 停用控制台登录
iam_client.call("UpdateLoginProfile", {
"UserName": username,
"LoginAllowed": "false"
})
# 2. 停用所有 API 密钥
for ak_id in access_key_ids:
iam_client.call("UpdateAccessKey", {
"AccessKeyId": ak_id,
"Status": "inactive",
"UserName": username
})
# 3. 移除所有权限策略(保存到 DB 以便恢复)
saved_policies = []
resp = iam_client.call("ListAttachedUserPolicies", {"UserName": username})
policies = resp.get("Result", {}).get("AttachedPolicyMetadata", [])
for p in policies:
policy_name = p["PolicyName"]
policy_type = p["PolicyType"]
iam_client.call("DetachUserPolicy", {
"UserName": username,
"PolicyName": policy_name,
"PolicyType": policy_type,
})
saved_policies.append({"name": policy_name, "type": policy_type})
# saved_policies 存入本地 DB 的 IAMUser.saved_policies_on_disable 字段JSONField
# 恢复时从此字段读取并重新附加
print(f"用户 {username} 已完全停用(控制台 + {len(access_key_ids)} 个密钥 + {len(saved_policies)} 个策略已移除)")
```
### 8.2 一键恢复子账号
```python
def enable_sub_user(iam_client, username: str, access_key_ids: list = None,
saved_policies: list = None):
"""一键恢复子账号
三步恢复(与停用操作完全对称):
1. 恢复控制台登录
2. 恢复所有 API 密钥
3. 重新附加停用时保存的权限策略
"""
# 0. 如果未传入 access_key_ids自动查询
if access_key_ids is None:
access_key_ids = get_user_access_keys(iam_client, username)
# 1. 恢复控制台登录
iam_client.call("UpdateLoginProfile", {
"UserName": username,
"LoginAllowed": "true"
})
# 2. 恢复所有 API 密钥
for ak_id in access_key_ids:
iam_client.call("UpdateAccessKey", {
"AccessKeyId": ak_id,
"Status": "active",
"UserName": username
})
# 3. 重新附加停用时保存的策略
if saved_policies:
for p in saved_policies:
iam_client.call("AttachUserPolicy", {
"UserName": username,
"PolicyName": p["name"],
"PolicyType": p["type"],
})
# 恢复后清空 saved_policies_on_disable 字段
print(f"用户 {username} 已恢复(控制台 + {len(access_key_ids)} 个密钥 + {len(saved_policies or [])} 个策略已恢复)")
```
### 8.3 停用 vs 删除的区别
| 操作 | 效果 | 可恢复 |
|------|------|--------|
| `UpdateLoginProfile(LoginAllowed=false)` | 停用控制台登录 | 一键恢复 |
| `UpdateAccessKey(Status=inactive)` | 停用 API 访问 | 一键恢复 |
| `DetachUserPolicy` | 移除权限但保留用户 | 重新附加即可 |
| `DeleteUser` | **永久删除用户** | **不可恢复** |
---
## 9. 项目管理与资源隔离
### 9.1 项目管理 API
**端点:** `https://open.volcengineapi.com`
| Action | 说明 |
|--------|------|
| `CreateProject` | 创建项目 |
| `ListProjects` | 列出项目 |
| `GetProject` | 获取项目详情 |
| `UpdateProject` | 更新项目 |
| `DeleteProject` | 删除项目 |
| `ListProjectResources` | 列出项目中的资源 |
| `MoveProjectResource` | 在项目间移动资源 |
| `ListProjectIdentities` | 列出项目中的用户/角色 |
### 9.2 项目级权限授权
```python
# 在项目范围内授权(子账号只能访问该项目下的资源)
iam_client.call("AttachPolicyInProject", {
"UserName": "sub_user_1",
"PolicyName": "ArkFullAccess",
"PolicyType": "System",
"ProjectName": "DeptA-Project"
})
```
**效果:** 子账号仅能操作 `DeptA-Project` 项目下的 Ark/Seedance 资源,无法看到其他项目的内容。
### 9.3 标签管理 API
| Action | 说明 |
|--------|------|
| `TagResources` | 给资源打标签 |
| `UntagResources` | 移除标签 |
| `ListTagsForResources` | 查询资源标签 |
**标签用于:**
- 资源分组与管理
- 按标签筛选账单(在 ListBillDetail 响应中的 `Tag` 字段)
- IAM 条件策略(基于标签的访问控制)
---
## 10. SDK 与工具链
### 10.1 推荐 SDK
| 语言 | 包名 | 安装 | 覆盖 IAM/Billing |
|------|------|------|-------------------|
| **Python**(推荐) | `volcengine-python-sdk` | `pip install volcengine-python-sdk` | 是 |
| Go | `volcengine-go-sdk` | `go get github.com/volcengine/volcengine-go-sdk` | 是 |
| Node.js | `@volcengine/openapi` | `npm install @volcengine/openapi` | 是 |
| Java | `volcengine-java-sdk` | Maven | 是 |
### 10.2 Python SDK 使用示例
```python
import volcenginesdkcore
import volcenginesdkiam
# 配置
configuration = volcenginesdkcore.Configuration()
configuration.ak = "YOUR_AK"
configuration.sk = "YOUR_SK"
configuration.region = "cn-beijing"
volcenginesdkcore.Configuration.set_default(configuration)
# IAM 操作
iam_api = volcenginesdkiam.IAMApi(
volcenginesdkcore.ApiClient(configuration)
)
# 列出用户
users = iam_api.list_users(volcenginesdkiam.ListUsersRequest(
limit=100,
offset=0
))
```
### 10.3 CLI 工具
```bash
# 安装 Volcengine CLI
# 从 https://github.com/volcengine/volcengine-cli/releases 下载
# 配置
ve configure set --profile default --region cn-beijing \
--access-key YOUR_AK --secret-key YOUR_SK
# 使用
ve iam ListUsers
ve iam CreateUser --UserName "sub_user_1" --DisplayName "Sub User 1"
ve billing ListBillDetail --BillPeriod "2026-03" --Limit 100
```
---
## 11. 可执行实施方案
### 第一阶段:基础搭建
#### Step 1创建子账号
```python
# 创建 IAM 客户端
iam = VolcengineClient(AK, SK, "iam", "iam.volcengineapi.com")
# 创建子用户
iam.call("CreateUser", {
"UserName": "dept_a_user",
"DisplayName": "部门A用户",
"Email": "dept_a@company.com",
"MobilePhone": "+8618000000000"
})
# 开通控制台登录
iam.call("CreateLoginProfile", {
"UserName": "dept_a_user",
"Password": "Initial@Pass123",
"LoginAllowed": "true",
"PasswordResetRequired": "true"
})
# 创建 API 密钥(记录返回的 SecretAccessKey
result = iam.call("CreateAccessKey", {"UserName": "dept_a_user"})
# result["Result"]["AccessKey"]["SecretAccessKey"] -- 仅此一次!
```
#### Step 2配置权限
> **重要**如果要通过项目隔离资源Step 4**不要**在此处全局附加 `ArkFullAccess` / `TOSFullAccess`
> 否则全局策略会覆盖项目级限制,子账号将能访问所有项目的资源。
> 应当仅在项目范围内授权(见 Step 4或者如果不需要项目隔离则可以全局附加。
```python
# 方案 A不需要项目隔离时全局授权
# iam.call("AttachUserPolicy", {
# "UserName": "dept_a_user",
# "PolicyName": "ArkFullAccess",
# "PolicyType": "System"
# })
# iam.call("AttachUserPolicy", {
# "UserName": "dept_a_user",
# "PolicyName": "TOSFullAccess",
# "PolicyType": "System"
# })
# 方案 B推荐需要项目隔离时此处只附加密钥自管理策略
# Ark 和 TOS 的权限在 Step 4 中通过 AttachPolicyInProject 在项目范围内授权
# 允许自行管理 API 密钥(此策略需全局附加,不受项目限制)
iam.call("AttachUserPolicy", {
"UserName": "dept_a_user",
"PolicyName": "AccessKeySelfManageAccess",
"PolicyType": "System"
})
```
#### Step 3创建并附加 Deny 策略
```python
import json
deny_policy = {
"Statement": [
{
"Effect": "Deny",
"Action": [
"iam:ListUsers", "iam:GetUser",
"iam:ListGroups", "iam:GetGroup",
"iam:ListRoles", "iam:GetRole",
"iam:ListPolicies", "iam:GetPolicy",
"iam:ListAttachedUserPolicies", "iam:ListAttachedRolePolicies",
"iam:ListEntitiesForPolicy", "iam:GetLoginProfile",
"iam:GetSecurityConfig",
"iam:CreateUser", "iam:UpdateUser", "iam:DeleteUser",
"iam:CreateGroup", "iam:UpdateGroup", "iam:DeleteGroup",
"iam:CreateRole", "iam:UpdateRole", "iam:DeleteRole",
"iam:CreatePolicy", "iam:UpdatePolicy", "iam:DeletePolicy",
"iam:AttachUserPolicy", "iam:DetachUserPolicy",
"iam:AttachRolePolicy", "iam:DetachRolePolicy",
"iam:CreateLoginProfile", "iam:UpdateLoginProfile",
"iam:DeleteLoginProfile",
"iam:AddUserToGroup", "iam:RemoveUserFromGroup",
"iam:ListUsersForGroup", "iam:ListGroupsForUser",
"iam:SetSecurityConfig",
"iam:CreateServiceLinkedRole", "iam:DeleteServiceLinkedRole",
"iam:AttachUserGroupPolicy", "iam:DetachUserGroupPolicy",
"iam:ListAttachedUserGroupPolicies",
"iam:AssumeRole"
],
"Resource": ["*"]
},
{
"Effect": "Deny",
"Action": ["billing:*", "bss:*"],
"Resource": ["*"]
},
{
"Effect": "Deny",
"Action": ["organization:*"],
"Resource": ["*"]
}
]
}
# 注意PolicyDocument 不要额外 URL 编码VolcengineClient._norm_query 会自动编码
iam.call("CreatePolicy", {
"PolicyName": "DenyAdminAndBilling",
"Description": "禁止访问 IAM 管理和计费信息",
"PolicyDocument": json.dumps(deny_policy)
})
iam.call("AttachUserPolicy", {
"UserName": "dept_a_user",
"PolicyName": "DenyAdminAndBilling",
"PolicyType": "Custom"
})
```
### 第二阶段:消费监控
#### Step 4创建项目并分配
```python
# 项目管理 API与 IAM 共用端点,但 Version 不同)
project_client = VolcengineClient(AK, SK, "iam", "iam.volcengineapi.com",
version="2021-08-01")
# 创建项目
project_client.call("CreateProject", {
"ProjectName": "DeptA-Project",
"Description": "部门A专属项目"
})
# 在项目范围内授权(子账号只能操作此项目下的 Ark 和 TOS 资源)
iam.call("AttachPolicyInProject", {
"UserName": "dept_a_user",
"PolicyName": "ArkFullAccess",
"PolicyType": "System",
"ProjectName": "DeptA-Project"
})
iam.call("AttachPolicyInProject", {
"UserName": "dept_a_user",
"PolicyName": "TOSFullAccess",
"PolicyType": "System",
"ProjectName": "DeptA-Project"
})
```
#### Step 5消费查询脚本
```python
billing = VolcengineClient(AK, SK, "billing", "billing.volcengineapi.com",
version="2022-01-01")
def get_user_spending(bill_period: str, project_name: str = None) -> float:
"""查询指定项目/用户的消费金额(带分页处理)"""
total = 0.0
offset = 0
page_size = 300
while True:
params = {
"BillPeriod": bill_period,
"Limit": str(page_size),
"Offset": str(offset),
"GroupTerm": "0", # 明细级别(非聚合),确保 Project 字段可用
"GroupPeriod": "0", # 按账期
"NeedRecordNum": "1",
}
result = billing.call("ListBillDetail", params)
items = result.get("Result", {}).get("List", [])
record_num = int(result.get("Result", {}).get("Total", 0))
for item in items:
# 按项目筛选
if project_name and item.get("Project") != project_name:
continue
total += float(item.get("PayableAmount", "0"))
# 分页:如果还有更多数据,继续查询
offset += page_size
if offset >= record_num or not items:
break
return total
```
### 第三阶段:告警与自动停用(已在 AirGate 中实现)
> 以下逻辑已通过 AirGate 管理平台实现,不再需要手写脚本。
> 详见 `backend/utils/scheduler.py` 和 `backend/apps/monitor/views.py`。
**AirGate 实现的核心流程:**
1. 主账号通过界面给子账号**划拨额度**(如 10 万元)
2. 子账号下挂多个火山项目,每个项目有独立监测开关
3. 定时任务每小时遍历所有开启监测的项目,分别调用 Billing API 查询消费,累加得出总消费
4. 总消费达到额度的阶梯百分比(如 50%/80%/90%)时 → 飞书告警
5. 总消费达到 100% → 自动停用整个子账号 + 飞书告警
6. 主账号可随时追加额度(告警状态自动重置)→ 恢复子账号
**多项目管理:**
- 每个子账号可关联 N 个火山项目
- 每个项目有独立的监测开关(开/关)
- 可通过"全选"一键开启/关闭所有项目的监测
- 消费明细可按项目展开查看,但告警和停用看的是**所有开启项目的消费总和**
**告警状态管理:**
- 每个阶梯只通知一次,通过 `triggered_alerts` 字段(存数据库)去重
- 追加额度时自动重置 `triggered_alerts`,按新使用率重新计算
- 不需要月度重置,因为是额度制而非月度制
#### Step 6AirGate 已实现的 API 接口
```
# 仪表盘
GET /api/v1/dashboard/ # 总览(用户数/消费/告警)
# 火山主账号管理
GET /api/v1/volc-accounts/ # 列出主账号
POST /api/v1/volc-accounts/ # 添加主账号AK/SK 加密存储)
PUT /api/v1/volc-accounts/{id}/ # 更新主账号
DELETE /api/v1/volc-accounts/{id}/ # 删除主账号
POST /api/v1/volc-accounts/{id}/test/ # 测试密钥有效性
# IAM 子账号管理
GET /api/v1/iam-users/ # 列出所有子账号
POST /api/v1/iam-users/sync/ # 从火山同步全部子账号
POST /api/v1/iam-users/import/ # 导入指定子账号
GET /api/v1/iam-users/{id}/ # 查询子账号详情
PUT /api/v1/iam-users/{id}/update/ # 更新配置(告警阈值/开关)
POST /api/v1/iam-users/{id}/disable/ # 停用子账号
POST /api/v1/iam-users/{id}/enable/ # 恢复子账号
GET /api/v1/iam-users/{id}/policies/ # 查看权限策略
POST /api/v1/iam-users/{id}/policies/attach/ # 附加权限策略
POST /api/v1/iam-users/{id}/policies/detach/ # 移除权限策略
# 子账号项目管理(多项目关联 + 自动授权/回收)
GET /api/v1/iam-users/{id}/projects/ # 查看子账号关联的项目列表
POST /api/v1/iam-users/{id}/projects/add/ # 添加关联项目(自动在项目范围内授权默认策略)
PUT /api/v1/iam-users/{id}/projects/{pid}/ # 更新项目监测开关
DELETE /api/v1/iam-users/{id}/projects/{pid}/delete/ # 移除关联项目(自动回收项目范围内的策略)
POST /api/v1/iam-users/{id}/projects/toggle-all/ # 全选/全不选监测开关
# 额度管理
POST /api/v1/iam-users/{id}/allocate/ # 追加额度(正数)或扣减额度(负数)
GET /api/v1/iam-users/{id}/quota-history/ # 查看额度变更记录(含追加和扣减)
# 消费查询
GET /api/v1/billing/overview/ # 消费总览
POST /api/v1/billing/refresh/ # 手动刷新消费数据
GET /api/v1/billing/balance/ # 主账号余额
# 全局配置
GET /api/v1/config/ # 查看全局配置
PUT /api/v1/config/ # 更新全局配置
# 告警记录
GET /api/v1/alerts/ # 告警历史(支持类型筛选)
# 项目列表
GET /api/v1/projects/ # 从火山拉取项目列表
```
---
## 12. 限制与注意事项
### 12.1 关键限制
| 限制项 | 说明 |
|--------|------|
| IAM 子账号无独立计费 | 所有费用归主账号,通过多项目聚合追踪(子账号关联 N 个项目,消费=开启监测的项目之和) |
| Billing API 无实时数据 | 最快 T+1 天粒度,有 1-2 天延迟 |
| 每用户最多 2 个 API 密钥 | 无法创建更多 |
| SecretKey 仅返回一次 | 创建后立即保存 |
| Billing API QPS 限制 5 | 批量查询需注意限流 |
| Ark 推理限额无公开 API | 目前仅支持控制台操作 |
| 火山原生预算告警仅通知不自动执行 | AirGate 已自建额度划拨+阶梯告警+自动停用 |
### 12.2 安全建议
1. **主账号 AK/SK 务必安全存储**,建议使用环境变量或密钥管理服务
2. **定期轮换 API 密钥**,利用 `GetAccessKeyLastUsed` 检查不活跃的密钥
3. **遵循最小权限原则**,只授予必要的权限
4. **显式 Deny 策略优先**,防止权限漏洞
5. **监控日志**,使用 CloudTrail 审计 API 调用
### 12.3 消费监控的精确度问题
由于账单数据有 1-2 天延迟消费监控存在滞后。AirGate 的应对策略:
- **额度划拨制**:划拨的额度应预留 1-2 天延迟的消费余量(如实际想控制 10 万,可划拨 9 万并设阈值 [50, 80, 90]
- **阶梯式告警**:在额度用尽前的多个节点提前告警,给管理员反应时间
- **高频轮询**:每小时查一次,虽然数据本身有延迟,但能在数据更新后第一时间触发告警
- 结合 Ark 推理限额功能(控制台手动设置,自动暂停,无延迟)作为兜底
---
## 13. 参考文档
### 官方文档
| 文档 | URL |
|------|-----|
| IAM API 概览 | https://www.volcengine.com/docs/6257/65842 |
| IAM 基本概念 | https://www.volcengine.com/docs/6257/64963 |
| 创建用户并授权 | https://www.volcengine.com/docs/6257/94013 |
| CreateAccessKey | https://www.volcengine.com/docs/6257/65000 |
| AttachUserPolicy | https://www.volcengine.com/docs/6257/65029 |
| LoginProfile 管理 | https://www.volcengine.com/docs/6257/65013 |
| 策略概述 | https://www.volcengine.com/docs/6257/65058 |
| 策略基本结构 | https://www.volcengine.com/docs/6257/65059 |
| 系统预置策略 | https://www.volcengine.com/docs/6257/1253730 |
| 自定义策略 | https://www.volcengine.com/docs/6257/1158323 |
| Billing API 概览 | https://www.volcengine.com/docs/6269/1165275 |
| ListBillDetail | https://www.volcengine.com/docs/6269/1127842 |
| 预算管理 | https://www.volcengine.com/docs/6269/1165274 |
| 分账账单 | https://www.volcengine.com/docs/6269/177196 |
| 计费权限管理 | https://www.volcengine.com/docs/6269/1186807 |
| 项目管理 | https://www.volcengine.com/docs/6649/166155 |
| TOS IAM 策略 | https://www.volcengine.com/docs/6349/102133 |
| Ark IAM 教程 | https://www.volcengine.com/docs/82379/1263493 |
| Ark 推理限额 | https://www.volcengine.com/docs/82379/1159200 |
| CloudMonitor API | https://www.volcengine.com/docs/6408/78940 |
| API 签名方法 | https://www.volcengine.com/docs/6369/67269 |
### GitHub 资源
| 资源 | URL |
|------|-----|
| Volcengine GitHub | https://github.com/volcengine |
| Python SDK | https://github.com/volcengine/volcengine-python-sdk |
| Go SDK | https://github.com/volcengine/volcengine-go-sdk |
| Node.js SDK | https://github.com/volcengine/volc-sdk-nodejs |
| OpenAPI Demos | https://github.com/volcengine/volc-openapi-demos |
| CLI 工具 | https://github.com/volcengine/volcengine-cli |
| TOS Python SDK | https://github.com/volcengine/ve-tos-python-sdk |
### 其他资源
| 资源 | URL |
|------|-----|
| API Explorer | https://api.volcengine.com/api-docs |
| SDK Center | https://api.volcengine.com/api-sdk |
| PyPI (新 SDK) | https://pypi.org/project/volcengine-python-sdk/ |
| npm | https://www.npmjs.com/package/@volcengine/openapi |
---
> **当前进度**AirGate 管控工具已完成核心功能开发Django 4.2 + DRF + Vue 3 + Element Plus
> 包括 IAM 子账号管理、额度划拨、阶梯式告警、消费监控、飞书通知。
> 项目仓库https://gitea.airlabs.art/seaislee/AirGate.git
>
> **待完成**创建子账号功能、权限策略配置界面、Docker/K8s 部署配置、飞书联调、AirDrama API 对接。