Quota allocation system: - Replace monthly budget with one-time quota allocation (prepaid model) - Support both adding (+) and deducting (-) quota with underflow protection - Stepped alerts at configurable percentages (e.g., 50%/80%/90%) - Auto-disable when quota exhausted (100%), alert state resets on new allocation - Quota allocation history with operator audit trail IAM management: - Create new IAM sub-accounts directly from AirGate (auto-generates API keys) - SecretKey shown once in dialog with copy-to-clipboard - Attach/detach IAM policies via UI (ArkFullAccess, TOSFullAccess, etc.) - Sync existing users from Volcengine - Project list pulled from Volcengine API for dropdown selection Security & auth: - API Key authentication for external systems (AirDrama integration) - SECRET_KEY enforced in production (raises error if missing with DEBUG=False) - APIKeyUser with proper pk/is_staff attributes for DRF compatibility Infrastructure: - Docker + docker-compose for backend and frontend - Nginx reverse proxy for frontend with /api/ forwarding - Entrypoint with auto-migrate and default admin creation - SQLite data persisted via Docker volume at /app/data/ Bug fixes from audit: - Fix frontend referencing non-existent fields (current_month_spending, effective_budget, budget_usage_percent) - Fix scheduler using naive datetime.now() → timezone.now() - Fix scheduler reading interval from settings instead of GlobalConfig DB - Fix docker-compose SQLite volume mounting as directory - Fix CORS origin with explicit port 80 - Remove dead config (VOLC_ACCESS_KEY/SK, MONITOR_INTERVAL from settings) - Remove unused imports Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
40 KiB
火山引擎 IAM 子账号管控工具 -- 深度研究报告
研究日期:2026-03-19 目标:通过火山引擎 Open API,实现对 IAM 子账号的全面管控,包括权限隔离、消费监控、告警、自动停用等功能。
目录
- 整体架构方案
- API 认证与签名机制
- IAM 用户管理 API
- 权限策略管理
- API 密钥管理
- 计费与消费查询 API
- 预算与告警机制
- 子账号自动停用/恢复方案
- 项目管理与资源隔离
- SDK 与工具链
- 可执行实施方案
- 限制与注意事项
- 参考文档
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 签名实现
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 策略文档格式
{
"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 密钥。
{
"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 限定到指定存储桶
{
"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 访问
# 停用密钥 = 立即切断子账号的所有 API 调用能力
iam_client.call("UpdateAccessKey", {
"AccessKeyId": "AKLT****",
"Status": "inactive",
"UserName": "sub_user_1"
})
恢复子账号的 API 访问
# 恢复密钥 = 一键恢复
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 查询消费。
可行方案:
| 方案 | 实现方式 | 精确度 |
|---|---|---|
| 按项目追踪 | 为每个子账号/部门创建独立项目,资源都放在项目中 | 高 |
| 按标签追踪 | 资源打上子账号标签(如 owner=sub_user_1) |
高 |
| 按 Ark 端点追踪 | Seedance/方舟按 Endpoint 分账(ListSplitBillDetail) | 中 |
| 按 TOS 存储桶追踪 | TOS 按 Bucket 分账 | 高 |
推荐方案:项目 + 标签 双维度
1. 创建项目 "DeptA-Project"
2. 子账号的权限限定在该项目范围内 (AttachPolicyInProject)
3. 资源打标签 tag: {"department": "DeptA", "owner": "sub_user_1"}
4. 通过 ListBillDetail + ListSplitBillDetail 按项目/标签筛选消费
6.4 账户余额查询
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 预算管理 API(Billing 模块)
| 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 |
创建通知组 |
支持的通知渠道:
- 站内信(默认开启)
- SMS
- 飞书(Feishu)
- 钉钉(DingTalk)
- 企业微信
- 自定义 Webhook(HTTP POST 回调)
7.3 Ark 推理限额(Seedance 专属)
火山方舟(Ark)平台有推理限额功能:
- 可设置每个模型的最大 Token 消耗量
- 达到限额后服务自动暂停
- 最小调整间隔 2 小时
- 仅支持在线推理(不含批量)
- 目前仅支持通过控制台设置,暂无公开 API
7.4 AirGate 自建方案:额度划拨制 + 阶梯式告警
由于火山原生的预算告警仅支持站内信/邮件/短信通知,不支持自动停用。AirGate 采用额度划拨制自建:
主账号通过 AirGate 给子账号划拨额度(如 10 万元)
│
▼ 定时任务每小时查询 Billing API
累计消费不断增长,对比已划拨额度
│
├── 消费达到额度 50% → 飞书告警
├── 消费达到额度 80% → 飞书告警
├── 消费达到额度 90% → 飞书告警
└── 消费达到额度 100% → 自动停用子账号 + 飞书告警
额度用完 → 主账号在 AirGate 追加额度 → 告警状态自动重置 → 恢复子账号
额度给多了 → 主账号在 AirGate 扣减额度 → 告警状态自动重置
关键设计:
- 非月度制:额度不按月重置,是一次性划拨,用完再充
- 可追加可扣减:主账号可随时追加额度(+5万)或扣减额度(-3万),支持灵活调整
- 扣减保护:扣减后总额度不能低于已消费金额(否则会立即触发停用)
- 阶梯式告警:每个子账号可自定义告警百分比(如 [50, 80, 90]),每档只通知一次
- 额度变更即重置告警:追加或扣减额度后,已触发的告警状态自动清空,按新的使用率重新计算
- 累计消费:跨月累计,通过 Billing API 各月数据求和得出
- 操作留痕:每次划拨/扣减都记录操作人、金额、备注,可追溯
8. 子账号自动停用/恢复方案
8.1 完全停用子账号(保留账号,可恢复)
需要同时执行两个操作才能完全停用:
def get_user_access_keys(iam_client, username: str) -> list:
"""获取用户的所有 AccessKey ID"""
result = iam_client.call("ListAccessKeys", {"UserName": username})
keys = result.get("Result", {}).get("AccessKeyMetadata", [])
return [k["AccessKeyId"] for k in keys]
def disable_sub_user(iam_client, username: str, access_key_ids: list = None):
"""完全停用子账号(保留账号,可一键恢复)"""
# 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
})
print(f"用户 {username} 已完全停用(控制台 + {len(access_key_ids)} 个 API 密钥)")
8.2 一键恢复子账号
def enable_sub_user(iam_client, username: str, access_key_ids: list = None):
"""一键恢复子账号"""
# 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
})
print(f"用户 {username} 已恢复(控制台 + {len(access_key_ids)} 个 API 密钥)")
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 项目级权限授权
# 在项目范围内授权(子账号只能访问该项目下的资源)
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 使用示例
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 工具
# 安装 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:创建子账号
# 创建 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),或者如果不需要项目隔离则可以全局附加。
# 方案 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 策略
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:创建项目并分配
# 项目管理 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:消费查询脚本
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 实现的核心流程:
- 主账号通过界面给子账号划拨额度(如 10 万元)
- 定时任务每小时调用 Billing API 查询累计消费
- 消费达到额度的阶梯百分比(如 50%/80%/90%)时 → 飞书告警
- 消费达到 100% → 自动停用子账号 + 飞书告警
- 主账号可随时追加额度(告警状态自动重置)→ 恢复子账号
告警状态管理:
- 每个阶梯只通知一次,通过
triggered_alerts字段(存数据库)去重 - 追加额度时自动重置
triggered_alerts,按新使用率重新计算 - 不需要月度重置,因为是额度制而非月度制
Step 6:AirGate 已实现的 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}/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 子账号无独立计费 | 所有费用归主账号,需通过项目/标签追踪 |
| Billing API 无实时数据 | 最快 T+1 天粒度,有 1-2 天延迟 |
| 每用户最多 2 个 API 密钥 | 无法创建更多 |
| SecretKey 仅返回一次 | 创建后立即保存 |
| Billing API QPS 限制 5 | 批量查询需注意限流 |
| Ark 推理限额无公开 API | 目前仅支持控制台操作 |
| 火山原生预算告警仅通知不自动执行 | AirGate 已自建额度划拨+阶梯告警+自动停用 |
12.2 安全建议
- 主账号 AK/SK 务必安全存储,建议使用环境变量或密钥管理服务
- 定期轮换 API 密钥,利用
GetAccessKeyLastUsed检查不活跃的密钥 - 遵循最小权限原则,只授予必要的权限
- 显式 Deny 策略优先,防止权限漏洞
- 监控日志,使用 CloudTrail 审计 API 调用
12.3 消费监控的精确度问题
由于账单数据有 1-2 天延迟,消费监控存在滞后。AirGate 的应对策略:
- 额度划拨制:划拨的额度应预留 1-2 天延迟的消费余量(如实际想控制 10 万,可划拨 9 万并设阈值 [50, 80, 90])
- 阶梯式告警:在额度用尽前的多个节点提前告警,给管理员反应时间
- 高频轮询:每小时查一次,虽然数据本身有延迟,但能在数据更新后第一时间触发告警
- 结合 Ark 推理限额功能(控制台手动设置,自动暂停,无延迟)作为兜底
13. 参考文档
官方文档
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 对接。