用 Terraform 给 AI Agent 上云(六):LLM 网关与密钥管理
把所有 LLM 访问收敛到一个网关:按 Agent 限流、请求落 SLS 日志、KMS 之外不留密钥。Terraform 配 API Gateway + ECS 上自托管 LiteLLM,DashScope/OpenAI/Anthropic 的 key 通过 KMS Secrets Manager 自动轮转。
CK
Chen Kai
· 6 min read · 2997 words
不成熟的 Agent stack 有个常见模式:每个 Agent 自己 .env 文件里有一份 OPENAI_API_KEY。有时是同一份,有时不是,有时是同事原型阶段留下的个人 key。账单到了没人能说清是哪个 Agent 烧的 token,key 泄露的时候(一定会泄露)你在十几个 .env 文件之间打地鼠。
本篇结束这个状态。我们建一个 LLM 网关:
- 所有 provider key 在 KMS Secrets Manager 里
- Agent 通过短期 RAM token 认证
- 按 Agent 强制 QPM 和每日 token 上限
- 每次请求落 SLS 用于审计和成本归因
- 轮转 key 不重启任何 Agent
两天搭,永久赢。
形状

左边 Agent,右边 provider,中间网关。每个 Agent 调"一个 LLM"的 HTTP 请求实际打到网关,网关决定派给哪个 provider、附上对的 key、强制配额、记结果。
两个合理实现:
- 阿里云 API Gateway 加自定义后端——最托管、加配额最容易、和 RAM 集成
- ECS 上自托管 LiteLLM(或自研)放 ALB 后——最灵活、支持长尾 provider、加成本追踪更容易
我两个都用,看路由逻辑多自定义。纯代理 + 配额,API Gateway 单独够。多 provider 路由 + fallback + 预算守门,LiteLLM on ECS 赢。
第 1 步:所有 key 进 KMS Secrets Manager
第一条规则:provider key 不出现在 .env、provider {} 块、Agent 代码、tfstate 明文里。它们住在 KMS Secrets Manager,网关启动时通过 STS 拉取。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| locals {
llm_secrets = {
"dashscope-prod" = "DashScope (百炼) API key"
"openai-prod" = "OpenAI API key"
"anthropic-prod" = "Anthropic API key"
"deepseek-prod" = "DeepSeek API key"
}
}
resource "alicloud_kms_secret" "llm" {
for_each = local.llm_secrets
secret_name = each.key
secret_data = var.llm_keys[each.key] # 通过 -var 或环境注入
version_id = "v1"
description = each.value
encryption_key_id = module.vpc.kms_keys["secrets"]
rotation_interval = "30d"
enable_automatic_rotation = false # 我们靠改 secret_data 轮转
recovery_window_in_days = 7
}
|
key 本身通过 var.llm_keys 进来——-var-file=secrets.auto.tfvars(gitignore)或者 CI secret 里 TF_VAR_llm_keys='{...}'。永远不进仓库。
实操提示: 轮转 provider key 时,改 secret_data 并 bump version_id。KMS 在 recovery window 内保持老版本可用,进行中的请求不会失败;新的网关拉取拿到新版本。在 PR 里规划这件事,让它可审计。
第 2 步:网关能 assume 的 RAM role
网关 ECS 或函数需要权限读这些 secret——而且只读这些:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| resource "alicloud_ram_role" "gateway" {
name = "agent-gateway-${terraform.workspace}"
assume_role_policy_document = jsonencode({
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = {
Service = ["ecs.aliyuncs.com"]
}
}]
Version = "1"
})
}
resource "alicloud_ram_policy" "gateway_kms" {
policy_name = "agent-gateway-kms-${terraform.workspace}"
policy_document = jsonencode({
Version = "1"
Statement = [
{
Effect = "Allow"
Action = [
"kms:GetSecretValue",
"kms:Decrypt"
]
Resource = [for s in alicloud_kms_secret.llm : s.arn]
}
]
})
}
resource "alicloud_ram_role_policy_attachment" "gateway_kms" {
policy_name = alicloud_ram_policy.gateway_kms.policy_name
policy_type = "Custom"
role_name = alicloud_ram_role.gateway.name
}
|
三处刻意:
- Resource 级 policy。 只这些 secret,不是
kms:GetSecretValue on *。网关被攻陷时攻击者无法横向扩展到其他 KMS secret。 - 没有长期 AK。 Role 由 ECS 实例通过 metadata service assume。零静态凭证。
kms:Decrypt 也要给,仅仅读 secret 也需要,因为 secret 静态加密。
第 3 步:ECS 上部署 LiteLLM
LiteLLM 是我所知最简单的开源 LLM 代理。前端讲 OpenAI API 格式,后端翻译成各 provider 的方言。ECS 自托管保留灵活性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| resource "alicloud_instance" "gateway" {
count = 2 # 双机 HA,前面挂 ALB
instance_name = "llm-gateway-${terraform.workspace}-${count.index + 1}"
image_id = data.alicloud_images.ubuntu.images[0].id
instance_type = "ecs.c7.large"
availability_zone = "cn-shanghai-${count.index == 0 ? "l" : "m"}"
vswitch_id = module.vpc.private_vswitch_ids[count.index]
security_groups = [module.vpc.agent_runtime_sg_id] # 同 SG;网关也是 runtime tier
role_name = alicloud_ram_role.gateway.name # 网关 assume 这个 role
system_disk_category = "cloud_essd"
system_disk_size = 40
user_data = base64encode(templatefile("${path.module}/gateway-init.sh", {
config_b64 = base64encode(local.litellm_config)
sls_project = alicloud_log_project.agents.name
sls_logstore = alicloud_log_store.gateway_requests.name
}))
tags = { Role = "llm-gateway" }
}
locals {
litellm_config = yamlencode({
model_list = [
{
model_name = "qwen-max"
litellm_params = {
model = "dashscope/qwen-max-2026-01-15"
api_key = "os.environ/DASHSCOPE_API_KEY"
}
},
{
model_name = "claude-opus"
litellm_params = {
model = "anthropic/claude-opus-4.7"
api_key = "os.environ/ANTHROPIC_API_KEY"
}
},
{
model_name = "gpt-4o"
litellm_params = {
model = "openai/gpt-4o-2026-01-15"
api_key = "os.environ/OPENAI_API_KEY"
}
}
]
general_settings = {
master_key = "os.environ/LITELLM_MASTER_KEY"
database_url = "os.environ/DATABASE_URL"
}
})
}
|
gateway-init.sh 启动:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| #!/bin/bash
set -euxo pipefail
apt-get update -y
apt-get install -y python3.11 python3.11-venv git curl jq
# 通过实例 role 从 KMS 拉 provider key(不需要 AK)
TOKEN=$(curl -s http://100.100.100.200/latest/meta-data/ram/security-credentials/agent-gateway-${ENV})
ACCESS_KEY_ID=$(echo $TOKEN | jq -r .AccessKeyId)
ACCESS_KEY_SECRET=$(echo $TOKEN | jq -r .AccessKeySecret)
SECURITY_TOKEN=$(echo $TOKEN | jq -r .SecurityToken)
# 用阿里云 KMS CLI(或 Python SDK)拿每个 key
pip install alibabacloud-kms20160120
export DASHSCOPE_API_KEY=$(python3 -c "import kms_helper; print(kms_helper.get('dashscope-prod'))")
export OPENAI_API_KEY=$(python3 -c "import kms_helper; print(kms_helper.get('openai-prod'))")
export ANTHROPIC_API_KEY=$(python3 -c "import kms_helper; print(kms_helper.get('anthropic-prod'))")
# 写 LiteLLM 配置
mkdir -p /etc/litellm
echo "${config_b64}" | base64 -d > /etc/litellm/config.yaml
# 装 + 用 pm2 跑 LiteLLM
pip install 'litellm[proxy]'
npm install -g pm2
pm2 start --name llm-gateway -- litellm --config /etc/litellm/config.yaml --port 4000
pm2 save
pm2 startup systemd -u root --hp /root
|
每台实例上网关现在跑起来了,监听 4000,所有 provider key 已加载。前面 ALB 分流:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| resource "alicloud_alb_load_balancer" "gateway" {
vpc_id = module.vpc.vpc_id
address_type = "Intranet"
load_balancer_name = "llm-gateway-${terraform.workspace}"
load_balancer_edition = "Standard"
zone_mappings {
vswitch_id = module.vpc.private_vswitch_ids[0]
zone_id = "cn-shanghai-l"
}
zone_mappings {
vswitch_id = module.vpc.private_vswitch_ids[1]
zone_id = "cn-shanghai-m"
}
}
resource "alicloud_alb_server_group" "gateway" {
vpc_id = module.vpc.vpc_id
server_group_name = "llm-gateway"
protocol = "HTTP"
health_check_config {
health_check_enabled = true
health_check_path = "/health"
health_check_protocol = "HTTP"
}
servers = [
for inst in alicloud_instance.gateway : {
server_id = inst.id
port = 4000
weight = 100
}
]
}
resource "alicloud_alb_listener" "gateway" {
load_balancer_id = alicloud_alb_load_balancer.gateway.id
listener_port = 80
listener_protocol = "HTTP"
default_actions {
type = "ForwardGroup"
forward_group_config {
server_group_tuples {
server_group_id = alicloud_alb_server_group.gateway.id
}
}
}
}
|
Agent 现在通过 http://<alb-id>.cn-shanghai.alb.aliyuncs.com/v1/chat/completions 访问网关,永远见不到 provider key。
第 4 步:按 Agent 配额
LiteLLM 原生支持按 key 配额。最干净的 Terraform 接法是给每个 Agent 建一个 LiteLLM “virtual key”,各自带 QPM 和 token 预算。LiteLLM 把这些存在自己数据库里,所以你 apply 时通过它的 API 用 null_resource 配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| locals {
agent_quotas = {
"research-agent" = { qpm = 120, daily_tokens = 2000000, max_budget = 800 }
"code-agent" = { qpm = 60, daily_tokens = 1000000, max_budget = 500 }
"support-agent" = { qpm = 300, daily_tokens = 3000000, max_budget = 600 }
"schedule-agent" = { qpm = 10, daily_tokens = 100000, max_budget = 40 }
}
}
resource "null_resource" "agent_keys" {
for_each = local.agent_quotas
triggers = {
config_hash = sha256(jsonencode(each.value))
}
provisioner "local-exec" {
command = <<-EOT
curl -X POST http://${alicloud_alb_load_balancer.gateway.dns_name}/key/generate \
-H "Authorization: Bearer ${var.litellm_master_key}" \
-H "Content-Type: application/json" \
-d '{
"key_alias": "${each.key}",
"rpm_limit": ${each.value.qpm},
"max_budget": ${each.value.max_budget},
"tpm_limit": ${each.value.daily_tokens / 1440}
}'
EOT
}
}
|
我不太喜欢 null_resource + local-exec——它是"provider 里还没这个资源"的逃生口。但能用,替代方案(写一个 LiteLLM 的 Terraform provider)一个团队的代价大于回报。
输出是每个 Agent 拿到一个独立的 LITELLM_API_KEY 环境变量,第四篇的 cloud-init 脚本读取。配额超限返回 429 Too Many Requests,Agent 应该用指数退避处理。
第 5 步:密钥轮转流
把 key 放 KMS Secrets Manager 的全部意义就是轮转:

生命周期:
- 在 Terraform(或 KMS API)里改
secret_data,version_id bump 到 v2 - KMS 在 rotation window 内(默认 30 天)保持
v1 可用 - 网关实例冷启动时重拉;现有实例继续用缓存值直到下次刷新(每 15 分钟,
gateway-init.sh 配置) - 30 天后
v1 禁用——还在用的人拿到 InvalidSecretVersion - 通过 SLS 确认
v1 零调用,提升 v2、退役 v1
团队来说,把这个写成 runbook,季度执行一次,哪怕没泄露。超过一个季度的 key 按定义就是陈旧;把陈旧当低烈度事故对待。
百炼 / DashScope 具体怎么办?
DashScope 在 LiteLLM 眼里就是另一个 OpenAI 兼容 endpoint。模型名是 dashscope/qwen-max、dashscope/qwen-plus 等。API key 是你从 DashScope 控制台生成的。
如果你想要一等公民阿里云原生待遇(用 STS 替代 API key),DashScope 在某些 endpoint 上支持 STS 鉴权——但 2026 年 API key 路径仍是标准,按上面通过 KMS 轮转 key 是正确的运维模式。
实操提示: 给 LiteLLM 设 master_key(LITELLM_MASTER_KEY 环境变量)。不设的话,任何能访问到网关的人都能给自己签发 API key。设了之后只有 master 能签发下属 key——而 master 永远不出 Terraform 变量空间。
这一篇带给你
读完本篇你拥有:
- 一个 URL,所有 Agent 都从这里调"LLM"
- 一个加新 model provider 的地方(改
litellm_config,terraform apply) - 一个轮转任何 provider key 的地方(改
var.llm_keys,terraform apply) - 一个日志流(下一篇),显示每次请求的延迟、token、模型、Agent
- 每个 Agent 硬性 QPM 和预算上限——失控循环最多花 ¥800/天,不是你整月预算
网关是战略资产。每个我交付过的团队都在一个月内感谢我——通常是某人 API key 不小心提交进 git、他们意识到轮转是一个一行 PR 而不是一场救火的那一刻。
下一篇
第七篇是观测和成本控制:SLS 收日志、ARMS 收 trace、CloudMonitor 收指标,每日 LLM 花费过阈值时 ping 钉钉的预算告警,以及 SLS 驱动的成本看板让你看到"哪个 Agent 在烧我的预算"。
第八篇是端到端 walkthrough,第二到七篇所有东西落成一次 terraform apply。
Liked this piece?
Follow on GitHub for the next one — usually one a week.
GitHub →