Series · Terraform Agents · Chapter 6

用 Terraform 给 AI Agent 上云(六):LLM 网关与密钥管理

把所有 LLM 访问收敛到一个网关:按 Agent 限流、请求落 SLS 日志、KMS 之外不留密钥。Terraform 配 API Gateway + ECS 上自托管 LiteLLM,DashScope/OpenAI/Anthropic 的 key 通过 KMS Secrets Manager 自动轮转。

不成熟的 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

两天搭,永久赢。

形状

集中式 LLM 网关:一个出口、一个配额、一个审计日志

左边 Agent,右边 provider,中间网关。每个 Agent 调"一个 LLM"的 HTTP 请求实际打到网关,网关决定派给哪个 provider、附上对的 key、强制配额、记结果。

两个合理实现:

  1. 阿里云 API Gateway 加自定义后端——最托管、加配额最容易、和 RAM 集成
  2. ECS 上自托管 LiteLLM(或自研)放 ALB 后——最灵活、支持长尾 provider、加成本追踪更容易

我两个都用,看路由逻辑多自定义。纯代理 + 配额,API Gateway 单独够。多 provider 路由 + fallback + 预算守门,LiteLLM on ECS 赢。

第 1 步:所有 key 进 KMS Secrets Manager

第一条规则:provider key 不出现在 .envprovider {} 块、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 配:

按 Agent 配额策略

 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 的全部意义就是轮转:

密钥轮转流——KMS 作为单一真源

生命周期:

  1. 在 Terraform(或 KMS API)里改 secret_dataversion_id bump 到 v2
  2. KMS 在 rotation window 内(默认 30 天)保持 v1 可用
  3. 网关实例冷启动时重拉;现有实例继续用缓存值直到下次刷新(每 15 分钟,gateway-init.sh 配置)
  4. 30 天后 v1 禁用——还在用的人拿到 InvalidSecretVersion
  5. 通过 SLS 确认 v1 零调用,提升 v2、退役 v1

团队来说,把这个写成 runbook,季度执行一次,哪怕没泄露。超过一个季度的 key 按定义就是陈旧;把陈旧当低烈度事故对待。

百炼 / DashScope 具体怎么办?

DashScope 在 LiteLLM 眼里就是另一个 OpenAI 兼容 endpoint。模型名是 dashscope/qwen-maxdashscope/qwen-plus 等。API key 是你从 DashScope 控制台生成的。

如果你想要一等公民阿里云原生待遇(用 STS 替代 API key),DashScope 在某些 endpoint 上支持 STS 鉴权——但 2026 年 API key 路径仍是标准,按上面通过 KMS 轮转 key 是正确的运维模式。

实操提示: 给 LiteLLM 设 master_keyLITELLM_MASTER_KEY 环境变量)。不设的话,任何能访问到网关的人都能给自己签发 API key。设了之后只有 master 能签发下属 key——而 master 永远不出 Terraform 变量空间。

这一篇带给你

读完本篇你拥有:

  • 一个 URL,所有 Agent 都从这里调"LLM"
  • 一个加新 model provider 的地方(改 litellm_configterraform apply
  • 一个轮转任何 provider key 的地方(改 var.llm_keysterraform 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