用 Terraform 给 AI Agent 上云(三):可复用的 VPC 与安全基线 第一个可复用 module——三可用区 VPC,公私网交换机分层,NAT 出网,按 tier 分层的安全组,以及按数据域分的 KMS 主密钥。同样的代码出现在我交付过的每一个 Agent stack 里,参数化但本体不变。
CK
Chen Kai
March 16, 2026 · 7 min read · 3094 words
这一篇造的是我所有 Agent 项目里被复制粘贴最多的一段 Terraform:一个 vpc-baseline module,给后续每一个组件(ECS、RDS、OpenSearch、ACK)一个合理的落点。
读完之后你会拥有:
一个跨三个可用区的 VPC 六个交换机(每个区一个公网 + 一个私网),CIDR 不重叠 一个 NAT 网关 + EIP,让私网子网能出网调 LLM API 三个按 tier 叠的安全组(ALB → agent runtime → memory) 三把 KMS 主密钥,每个数据域一把(memory、secrets、logs) 干净的 module 接口:进 name + CIDR + zones,出一堆 ID 总共大概 200 行 HCL。一次写完,永久参考。
心智模型 代码之前先看图:
为什么三个区?因为阿里云保留了任何一个周日对某个 zone 做维护的权利,单区部署意味着维护窗口期间你的 Agent 全离线。VPC 内跨区流量免费;三个区的唯一代价是子网算术的运维复杂度。
为什么公网 + 私网?Agent runtime 应该住在私网,这样一个配错的安全组也不会意外把它暴露到 0.0.0.0/0。公网子网放 ALB(负载均衡)和 NAT 网关——那些必须 通互联网的东西。Agent 通过 NAT 出网,不直接出网。
我用的 CIDR 布局:
子网 Zone CIDR 主机数 public-al 10.20.0.0/2811 public-bm 10.20.0.16/2811 public-cn 10.20.0.32/2811 private-al 10.20.1.0/24251 private-bm 10.20.2.0/24251 private-cn 10.20.3.0/24251
公网 /28 因为只放一个 NAT 和一个 ALB IP。私网 /24 因为 Agent ECS、RDS、OpenSearch 节点都住在那里。
Module 骨架 建目录:
modules/vpc-baseline/
├── main.tf
├── variables.tf
├── outputs.tf
└── versions.tf
输入(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
variable "name" {
description = "所有资源的命名前缀,例如 agents-prod"
type = string
}
variable "cidr_block" {
description = "顶层 VPC CIDR,子网从这里推导"
type = string
default = "10.20.0.0/16"
}
variable "zones" {
description = "目标 region 的三个可用区 ID"
type = list ( string )
validation {
condition = length(var.zones) == 3
error_message = "vpc-baseline 必须正好三个 zone。"
}
}
variable "tags" {
description = "module 创建的所有资源都打这些 tag"
type = map ( string )
default = {}
}
强制三个 zone 是有立场的,但和图对得上。如果你需要两区或四区,fork 出来——别加条件分支。带条件的 module 会变得没法读。
VPC 和子网 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
resource "alicloud_vpc" "this" {
vpc_name = var . name
cidr_block = var . cidr_block
tags = var . tags
}
resource "alicloud_vswitch" "public" {
for_each = { for i, z in var.zones : i = > z }
vpc_id = alicloud_vpc . this . id
zone_id = each . value
cidr_block = cidrsubnet ( var . cidr_block , 12 , each . key ) # /28,从 .0 开始
vswitch_name = "${var.name}-public-${substr(each.value, -1, 1)}"
tags = var . tags
}
resource "alicloud_vswitch" "private" {
for_each = { for i, z in var.zones : i = > z }
vpc_id = alicloud_vpc . this . id
zone_id = each . value
cidr_block = cidrsubnet ( var . cidr_block , 8 , each . key + 1 ) # /24,从 .1.0 开始
vswitch_name = "${var.name}-private-${substr(each.value, -1, 1)}"
tags = var . tags
}
三件值得说的事:
cidrsubnet(prefix, newbits, netnum) 是 Terraform 的 CIDR 算术。cidrsubnet("10.20.0.0/16", 8, 1) 返回 "10.20.1.0/24"。背下来——你会反复用。for_each 配 index/value map 给出稳定的 resource 地址——alicloud_vswitch.private["0"] 永远指第一个 zone,哪怕你重排列表。和 count 对比,count 重排会引发整体重建。substr(each.value, -1, 1) 取 zone ID 最后一个字符(l/m/n),让资源名字排序好看。NAT 网关 + EIP 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
resource "alicloud_nat_gateway" "this" {
vpc_id = alicloud_vpc . this . id
vswitch_id = alicloud_vswitch . public [ "0" ]. id
nat_gateway_name = "${var.name}-nat"
nat_type = "Enhanced"
payment_type = "PayAsYouGo"
tags = var . tags
}
resource "alicloud_eip_address" "nat" {
address_name = "${var.name}-nat-eip"
bandwidth = "100"
internet_charge_type = "PayByTraffic"
isp = "BGP"
tags = var . tags
}
resource "alicloud_eip_association" "nat" {
allocation_id = alicloud_eip_address . nat . id
instance_id = alicloud_nat_gateway . this . id
}
resource "alicloud_snat_entry" "private" {
for_each = alicloud_vswitch . private
snat_table_id = alicloud_nat_gateway . this . snat_table_ids
source_vswitch_id = each . value . id
snat_ip = alicloud_eip_address . nat . ip_address
}
Enhanced NAT 是现代版——Tablestore、PrivateLink 和大多数新服务必需。PayByTraffic 适合 Agent 这种突发出网(LLM 流式)而非稳态出网的场景。
SNAT 条目才是真正让私网实例出网的东西。没有它,private-a 里的 Agent 解析不到 dashscope.aliyuncs.com。
安全组按 tier 分层 阿里云上正确的安全组用法是 每个 tier 一个 SG ,规则引用 SG ID 而不是 CIDR:
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
resource "alicloud_security_group" "alb_public" {
name = "${var.name}-alb-public"
vpc_id = alicloud_vpc . this . id
tags = var . tags
}
resource "alicloud_security_group_rule" "alb_https_in" {
security_group_id = alicloud_security_group . alb_public . id
type = "ingress"
ip_protocol = "tcp"
port_range = "443/443"
cidr_ip = "0.0.0.0/0"
policy = "accept"
priority = 1
}
resource "alicloud_security_group" "agent_runtime" {
name = "${var.name}-agent-runtime"
vpc_id = alicloud_vpc . this . id
tags = var . tags
}
resource "alicloud_security_group_rule" "agent_from_alb" {
security_group_id = alicloud_security_group . agent_runtime . id
type = "ingress"
ip_protocol = "tcp"
port_range = "8080/8080"
source_security_group_id = alicloud_security_group . alb_public . id
policy = "accept"
priority = 1
}
关键一行是 source_security_group_id = alicloud_security_group.alb_public.id。它说"只接受来自 ALB SG 里任何实例的入站 8080"——而不是某个 CIDR。后面给 ALB 换 IP 不会破任何东西。
实操提示: 阿里云安全组的默认行为是入站全拒、出站全允。这个默认是对的——别加"出站全拒"规则,你只会把 SDK 调用搞挂。除非有明确合规要求,否则不要限制出站;Agent 系统全开出站是常态。
每个下游 tier 我都重复这个模式:
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
resource "alicloud_security_group" "memory_rds" {
name = "${var.name}-memory-rds"
vpc_id = alicloud_vpc . this . id
tags = var . tags
}
resource "alicloud_security_group_rule" "rds_from_agent" {
security_group_id = alicloud_security_group . memory_rds . id
type = "ingress"
ip_protocol = "tcp"
port_range = "5432/5432"
source_security_group_id = alicloud_security_group . agent_runtime . id
policy = "accept"
priority = 1
}
resource "alicloud_security_group" "vector_store" {
name = "${var.name}-vector-store"
vpc_id = alicloud_vpc . this . id
tags = var . tags
}
resource "alicloud_security_group_rule" "vector_from_agent" {
security_group_id = alicloud_security_group . vector_store . id
type = "ingress"
ip_protocol = "tcp"
port_range = "9200/9200"
source_security_group_id = alicloud_security_group . agent_runtime . id
policy = "accept"
priority = 1
}
写完之后给一台 ECS 挂正确的 SG 就一行 security_groups = [module.vpc.agent_runtime_sg_id],网络层结构性正确。
每个数据域一把 KMS 任何说得过去的合规框架都强制要求静态加密。阿里云方式是 每个数据域一把客户主密钥(CMK) ,这样轮转一把不影响另一把,按 key 审计访问。
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
locals {
cmks = {
memory = "RDS 数据和 OSS 对象的加密"
secrets = "KMS Secrets Manager 条目加密"
logs = "SLS 日志数据加密"
}
}
resource "alicloud_kms_key" "this" {
for_each = local . cmks
description = each . value
key_usage = "ENCRYPT/DECRYPT"
key_spec = "Aliyun_AES_256"
pending_window_in_days = 7
status = "Enabled"
automatic_rotation = "Enabled"
rotation_interval = "365d"
protection_level = "SOFTWARE"
tags = merge(var.tags, { Domain = each . key })
}
resource "alicloud_kms_alias" "this" {
for_each = local . cmks
alias_name = "alias/${var.name}-${each.key}"
key_id = alicloud_kms_key . this [ each . key ]. id
}
为什么用别名?因为 CMK ID 是个没人记得住的 UUID;别名 alias/agents-prod-memory 人类可读且在 key 轮转时稳定。从 RDS、OSS 等地方引用别名,你可以换底层 key 而下游配置不变。
pending_window_in_days = 7 意味着删除的 key 有 7 天恢复窗口。别缩短——误删 key 是那种能终结职业生涯的错误。
Module 输出 outputs.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
output "vpc_id" {
value = alicloud_vpc . this . id
}
output "private_vswitch_ids" {
value = [ for s in alicloud_vswitch . private : s . id ]
}
output "public_vswitch_ids" {
value = [ for s in alicloud_vswitch . public : s . id ]
}
output "nat_gateway_id" {
value = alicloud_nat_gateway . this . id
}
output "nat_eip_address" {
value = alicloud_eip_address . nat . ip_address
}
output "alb_public_sg_id" {
value = alicloud_security_group . alb_public . id
}
output "agent_runtime_sg_id" {
value = alicloud_security_group . agent_runtime . id
}
output "memory_rds_sg_id" {
value = alicloud_security_group . memory_rds . id
}
output "vector_store_sg_id" {
value = alicloud_security_group . vector_store . id
}
output "kms_keys" {
value = { for k, v in alicloud_kms_key.this : k = > v . id }
}
output "kms_aliases" {
value = { for k, v in alicloud_kms_alias.this : k = > v . alias_name }
}
这些刚好是后面五篇会用到的 ID。把输出有意识地命名和成形之后,调用方可以这样写:
1
2
3
4
5
6
7
8
9
10
11
module "vpc" {
source = "./modules/vpc-baseline"
name = "agents-prod"
zones = [ "cn-shanghai-l", "cn-shanghai-m", "cn-shanghai-n" ]
}
resource "alicloud_instance" "agent" {
vswitch_id = module . vpc . private_vswitch_ids [ 0 ]
security_groups = [ module . vpc . agent_runtime_sg_id ]
# ...
}
调用 module 顶层 main.tf:
1
2
3
4
5
6
7
8
9
10
11
12
13
module "vpc" {
source = "./modules/vpc-baseline"
name = "agents-${terraform.workspace}"
cidr_block = "10.20.0.0/16"
zones = [ "cn-shanghai-l", "cn-shanghai-m", "cn-shanghai-n" ]
tags = {
Project = "research-agent-stack"
Environment = terraform . workspace
ManagedBy = "terraform"
}
}
从项目根目录跑 terraform plan 会输出大致:
Plan: 27 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ agent_runtime_sg_id = (known after apply)
+ nat_eip_address = (known after apply)
+ private_vswitch_ids = [
+ (known after apply),
+ (known after apply),
+ (known after apply),
]
+ vpc_id = (known after apply)
27 个资源差不多对(1 VPC + 6 vSwitch + 1 NAT + 1 EIP + 1 EIP-assoc + 3 SNAT + 4 SG + 4 SG-rule + 3 KMS key + 3 KMS alias = 27)。Apply,约 90 秒,你拿到了一个生产级别的网络。
成本估算 cn-shanghai,按月粗略:
VPC、vSwitch、安全组、KMS key:免费 NAT 网关:Enhanced 类型 ~¥120/月,加按 GB 出网流量 EIP:~¥20/月 IP 保留费,加 PayByTraffic 流量 KMS:每 key 每天前 100 次调用免费,之后 ~¥0.005/次 中低流量下网络基线 ¥150-300/月。物有所值——后面每一篇都继承这副骨架。
下一篇 第四篇把计算落到这个网络上。三种模式——ECS 配 pm2、ACK 跑生产 fleet、Function Compute 跑事件触发的 Agent——以及我用来在它们之间选的成本拐点模型。然后是真正的 alicloud_instance 块,用 cloud-init 把 Python + Node + Agent runtime bootstrap 起来。
实操提示: 如果你将来需要加第四个 zone(阿里云会周期性地加),terraform apply 一下就行——for_each 模式干净处理更长的列表。但是:variables.tf 里的 validation block 会拒绝它,所以你得先把 validation 放宽。这个故意的摩擦就是重点——加 zone 是值得想一想的网络变更。
Liked this piece? Follow on GitHub for the next one — usually one a week.
GitHub →