Series · Terraform Agents · Chapter 8

用 Terraform 给 AI Agent 上云(八):端到端——一次 apply 起整个 research-agent-stack

把七个 module 拼到一个仓库,跑一次 terraform apply,看一个完整的 Agent runtime——VPC、ECS、RDS、OpenSearch、OSS、LLM 网关、SLS 观测、成本告警——七分钟内起来。真实 apply 输出、module DAG、可 fork 的起手仓库。

这是第二到第七篇所有东西落到一处的文章。读完之后你会跑过一次 terraform apply,在阿里云上产出一个完整、可观测、有预算的 Agent runtime stack。约 31 个资源,~7 分钟实际时间。

我们要建的 stack:

research-agent-stack:每一个盒子,一次 terraform apply

五层——edge、compute、memory、platform、ops——由本系列做出的 module 组合。

项目结构

research-agent-stack/
├── README.md
├── versions.tf                  # Terraform + provider 版本
├── backend.tf                   # OSS + Tablestore 远程 state
├── providers.tf                 # alicloud + alicloud.beijing 别名
├── variables.tf                 # ��层输入
├── locals.tf                    # 看 workspace 计算的 local
├── main.tf                      # module 组合
├── outputs.tf                   # endpoint + 连接串
├── env/
│   ├── dev.tfvars
│   ├── staging.tfvars
│   └── prod.tfvars
├── secrets/
│   └── secrets.auto.tfvars      # gitignore——provider key
├── modules/
│   ├── vpc-baseline/            # 第三篇
│   ├── storage/                 # 第五篇
│   ├── compute/                 # 第四篇
│   ├── llm-gateway/             # 第六篇
│   └── observability/           # 第七篇
└── scripts/
    ├── cloud-init/
    │   ├── agent.sh
    │   └── gateway.sh
    └── restore-drill.sh

顶层八个 *.tfmodules/ 下五个 module,env/*.tfvars 装环境特定值,secrets/secrets.auto.tfvars 装 git 之外的密钥。这是我每个项目都用的布局——无聊就是好。

main.tf——组合

 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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
locals {
  is_prod   = terraform.workspace == "prod"
  name      = "agents-${terraform.workspace}"
  zones     = ["cn-shanghai-l", "cn-shanghai-m", "cn-shanghai-n"]

  common_tags = {
    Project     = "research-agent-stack"
    Environment = terraform.workspace
    ManagedBy   = "terraform"
    Owner       = "ai-platform"
  }
}

module "vpc" {
  source = "./modules/vpc-baseline"

  name       = local.name
  cidr_block = "10.20.0.0/16"
  zones      = local.zones
  tags       = local.common_tags
}

module "storage" {
  source = "./modules/storage"

  name              = local.name
  vpc               = module.vpc
  is_prod           = local.is_prod
  enable_dr         = local.is_prod   # 跨区 OSS 复制只在 prod 开
  tags              = local.common_tags

  providers = {
    alicloud         = alicloud
    alicloud.beijing = alicloud.beijing
  }
}

module "observability" {
  source = "./modules/observability"

  name             = local.name
  vpc              = module.vpc
  dingtalk_webhook = var.dingtalk_webhook
  cost_ceiling_cny = local.is_prod ? 800 : 100
  tags             = local.common_tags
}

module "gateway" {
  source = "./modules/llm-gateway"

  name           = local.name
  vpc            = module.vpc
  observability  = module.observability
  llm_keys       = var.llm_keys
  agent_quotas   = var.agent_quotas
  instance_count = local.is_prod ? 2 : 1
  tags           = local.common_tags
}

module "compute" {
  source = "./modules/compute"

  name           = local.name
  vpc            = module.vpc
  storage        = module.storage
  gateway        = module.gateway
  observability  = module.observability
  agent_repo_url = var.agent_repo_url
  agent_branch   = var.agent_branch
  ecs_count      = local.is_prod ? 3 : 1
  tags           = local.common_tags
}

五个 module 调用。注意每个 module 把前一个 module 的输出当输入——module.computemodule.vpcmodule.storagemodule.gatewaymodule.observability。这种依赖接线就是 Terraform 用来构建 apply DAG 的:

Terraform module 依赖 DAG

network 和 KMS 在最上——它们没有依赖。storage、compute、gateway 依赖 network + KMS 但相互独立,所以 Terraform 并行建。compute 还依赖 storage 和 gateway 因为 cloud-init 模板需要它们的 endpoint。observability 和 alarm 依赖 compute 因为它们引用 SG ID。

variables.tf

 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
variable "agent_repo_url" {
  description = "要部署的 Agent runtime 的 git URL"
  type        = string
  default     = "https://github.com/example/research-agent.git"
}

variable "agent_branch" {
  description = "要部署的 git 分支 / tag"
  type        = string
  default     = "main"
}

variable "dingtalk_webhook" {
  description = "告警钉钉 webhook URL"
  type        = string
  sensitive   = true
}

variable "llm_keys" {
  description = "provider 名 → API key 的 map,通过 secrets.auto.tfvars 设"
  type        = map(string)
  sensitive   = true
}

variable "agent_quotas" {
  description = "按 Agent 的 QPM 和预算上限"
  type = map(object({
    qpm          = number
    daily_tokens = number
    max_budget   = number
  }))
  default = {
    "research-agent" = { qpm = 120, daily_tokens = 2000000, max_budget = 800 }
  }
}

sensitive = true 让 Terraform 不在 plan/apply 输出里打值。值还是会进 tfstate(这就是为什么第二篇我们给 OSS bucket 开了加密)。

env/dev.tfvars

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
agent_repo_url   = "https://github.com/example/research-agent.git"
agent_branch     = "develop"
dingtalk_webhook = "https://oapi.dingtalk.com/robot/send?access_token=DEV_TOKEN"

agent_quotas = {
  "research-agent" = {
    qpm          = 30
    daily_tokens = 200000
    max_budget   = 50
  }
}

secrets/secrets.auto.tfvars(gitignore)

1
2
3
4
5
6
llm_keys = {
  "dashscope-prod" = "sk-DS-XXXXXXXXXXXXXXXXX"
  "openai-prod"    = "sk-XX-XXXXXXXXXXXXXXXXX"
  "anthropic-prod" = "sk-ant-XXXXXXXXXXXXXXXXX"
  "deepseek-prod"  = "sk-DEEPSEEK-XXXXXXXXX"
}

*.auto.tfvars 文件不加 -var-file 自动加载。从第一次 commit 起 secrets/ 就要在 .gitignore 里。

Apply

1
2
3
4
5
6
cd research-agent-stack
terraform workspace select dev
terraform init
terraform plan -var-file=env/dev.tfvars -out=tfplan
# 看 plan 输出:~31 个资源待创建
terraform apply tfplan

新建 apply 的真实耗时:

真实 apply 时间线——RDS/OpenSearch 主导,其余并行

时钟分解:

  • 0-60s: VPC、vSwitch、NAT、EIP、KMS 密钥——快资源
  • 60-380s: RDS(5 分钟)、OpenSearch(5.5 分钟)、ECS(~2 分钟)、网关(~1.5 分钟)——全部并行,被最慢的卡住
  • 380-460s: Agent 应用部署、observability 资源、告警

总共约 7 分钟,被 RDS 和 OpenSearch 主导。无变更的重复 apply 30 秒内结束因为 Terraform 只 diff。

精简后的 apply 文字记录:

Terraform will perform the following actions:

  # module.vpc.alicloud_vpc.this will be created
  + resource "alicloud_vpc" "this" {
      + cidr_block = "10.20.0.0/16"
      + vpc_name   = "agents-dev"
      ...
    }

  ...(再 29 个资源)...

Plan: 31 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + agent_endpoints       = (known after apply)
  + gateway_url           = (known after apply)
  + sls_dashboard_url     = (known after apply)
  + total_estimated_cost  = "~¥1450/月(dev 规格)"

Do you want to perform these actions in workspace "dev"?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

module.vpc.alicloud_vpc.this: Creating...
module.vpc.alicloud_kms_key.this["memory"]: Creating...
module.vpc.alicloud_kms_key.this["secrets"]: Creating...
module.vpc.alicloud_kms_key.this["logs"]: Creating...
module.vpc.alicloud_vpc.this: Creation complete after 4s [id=vpc-uf6abc123]
module.vpc.alicloud_vswitch.private["0"]: Creating...
module.vpc.alicloud_vswitch.private["1"]: Creating...
module.vpc.alicloud_vswitch.private["2"]: Creating...
module.vpc.alicloud_vswitch.public["0"]: Creating...
...
module.storage.alicloud_db_instance.memory: Still creating... [4m 30s elapsed]
module.storage.alicloud_opensearch_app_group.vector: Still creating... [5m 10s elapsed]
module.storage.alicloud_db_instance.memory: Creation complete after 4m 38s [id=pgm-uf6def456]
module.storage.alicloud_opensearch_app_group.vector: Creation complete after 5m 24s [id=os-uf6ghi789]
...
module.compute.alicloud_instance.agent[0]: Creation complete after 1m 52s [id=i-uf6jkl012]
module.gateway.alicloud_alb_listener.gateway: Creation complete after 12s
module.observability.alicloud_log_alert.cost_ceiling: Creation complete after 3s
...

Apply complete! Resources: 31 added, 0 changed, 0 destroyed.

Outputs:

agent_endpoints      = [
  "http://alb-uf6.cn-shanghai.alb.aliyuncs.com",
]
gateway_url          = "http://alb-uf7.cn-shanghai.alb.aliyuncs.com/v1"
sls_dashboard_url    = "https://sls.console.aliyun.com/lognext/project/agents-dev/dashboard/agent-cost-overview"
total_estimated_cost = "~¥1450/月(dev 规格)"

这就是一个完整的 Agent stack。ALB endpoint、网关 URL、SLS 看板 URL——任何一个粘进浏览器都能用。

Day-2 运维

stack 起来了。然后呢?

加新 Agent

  1. dev.tfvars 里的 var.agent_quotas 加一项
  2. terraform apply -var-file=env/dev.tfvars
  3. null_resource 给 LiteLLM 配新 key
  4. 用新 LITELLM_API_KEY 环境变量部署你的新 Agent 代码

端到端约 30 秒。

扩容

改 module 调用里的 ecs_count(或者通过 tfvars 设)。terraform apply 起新实例、挂上 ALB,老实例全程健康(create_before_destroy)。零停机。

Dev → prod 提升

1
2
terraform workspace select prod
terraform apply -var-file=env/prod.tfvars

同样 module,不同规格(HA RDS、更大 OpenSearch 配额、更多 ECS、真钉钉 webhook、真 LLM key、成本上限 ¥800 而不是 ¥100)。第一次 prod apply 7-10 分钟;后续 apply 几秒。

销毁 dev

实验做完了:

1
2
terraform workspace select dev
terraform destroy -var-file=env/dev.tfvars

会因为 prod-like 资源上的 deletion_protection = true 和 bootstrap state bucket 上的 prevent_destroy = true 失败。这是有意的。dev 里设 deletion_protection = local.is_prod,所以只在 prod 开——terraform destroy 在 dev 能跑。

实操提示: terraform destroy 之前永远先 terraform plan -destroy。读 plan 输出。要销毁的资源数应当跟你想的一样。我见过一个工程师因为忘记切 workspace 把 staging 销了。

接你真实的 Agent 代码

stack 是平台。Agent 本身来自你仓库(var.agent_repo_url),ECS 启动时 cloud-init 部署。Agent 代码需要遵守的最小契约:

1
2
3
4
5
6
7
8
9
# 这些来自 cloud-init 设的环境变量
LLM_GATEWAY_URL    = os.environ["LLM_GATEWAY_URL"]    # http://alb.../v1
LITELLM_API_KEY    = os.environ["LITELLM_API_KEY"]    # 按 Agent 的 key
DATABASE_URL       = os.environ["DATABASE_URL"]       # postgres://...
VECTOR_ENDPOINT    = os.environ["VECTOR_ENDPOINT"]    # OpenSearch HTTP
ARTIFACTS_BUCKET   = os.environ["ARTIFACTS_BUCKET"]   # OSS bucket 名
SLS_PROJECT        = os.environ["SLS_PROJECT"]
SLS_LOGSTORE       = os.environ["SLS_LOGSTORE"]
ARMS_OTLP_ENDPOINT = os.environ["ARMS_OTLP_ENDPOINT"]

所有这些从 Terraform 输出取值。Agent 代码形状上保持云无关——只读环境变量——但运行时完全接到阿里云 stack 上。

成本汇总

dev workspace 真实账单,低流量:

组件月费
VPC + NAT + EIP~¥150
ECS x1 (c7.large)~¥250
RDS Postgres (小)~¥350
OpenSearch 向量~¥800
OSS (10 GB 标准)~¥2
LLM 网关 ECS x1~¥150
ALB(小)~¥50
SLS + ARMS~¥300
KMS~¥10
dev 合计~¥2060/月

Prod 配 HA、更大规格、跨区 DR:LLM API 成本之外大约 ¥6000-9000/月。LLM 账单通常是最大头——这就是为什么有第六篇的网关和第七篇的成本告警。

我跳过的

  • CDN 公网服务产物 URL——alicloud_cdn_domain 可用,但多数 Agent 通过自己网关服产物
  • WAF 在 ALB 前——对外 prod 必需,dev 用 Intranet ALB
  • PrivateLink 到 DashScope——规模上省 NAT 出网,alicloud_privatelink_* 可配
  • 自定义域名 + SSL——alicloud_alb_listener 支持 SSL 证书但你得自带(或用 ACM)

四个都值得在基础跑通后加。第一天别加。

接下来

你现在拥有一个生产形状的 Agent runtime 在阿里云上,全部用 Terraform 表达,自带可观测、密钥管理、成本守门。下一步看你的项目:

  • 更多 Agent: 加进 var.agent_quotasterraform apply
  • 不同 LLM provider: 加进网关 module 的 local.litellm_config
  • 多 region: 加 provider 别名,复制 stack
  • GitOps:terraform apply 包进有 PR review 守门的 CI 流水
  • Pulumi 或 Crossplane 迁移: 资源图直接对应

最重要的一件事是你的基础设施现在在 git 里。每次变更可评审。每个环境可重现。每笔成本可归因。这就是 IaC 给你的,也就是让在阿里云上交付 Agent 成为可持续实践而不是永久救火的原因。

谢谢读完本系列。如果你基于本系列交付了一个 stack,我很想听你改了什么、为什么——这就是模式演进的方式。

Liked this piece?

Follow on GitHub for the next one — usually one a week.

GitHub