说真的,搞 Terraform 管 AWS VPC 这事儿,我踩过的坑比我吃过的盐还多。上个月我们团队刚把一个三层的生产 VPC 从手动配置迁移到 Terraform,中间炸了好几次监控,翻车翻得我头皮发麻。今天不整那些虚头巴脑的"最佳实践",我就把真正用血泪换来的 10 条硬核经验甩出来。
别信"默认配置",那是个坑
AWS 给的 VPC 默认配置,讲真,就是给新手体验用的。生产环境你敢用默认 CIDR 块?我见过太多人直接 cidr_block = "10.0.0.0/16",结果跟其他 VPC 对等连接时直接撞车。
我的做法: 用 10.xxx.0.0/16 这种模式,xxx 按环境编号(dev=10, staging=20, prod=30)。别问我为什么,某次 prod 和 staging 对等连接时 IP 重叠,修了整整 4 个小时。
模块化?别过度设计
Reddit 上有个哥们说得对——“模块应该赚取它们的存在价值”。别一上来就搞十几个嵌套模块,VPC 这种东西,一个 aws_vpc 资源加几个 aws_subnet 就够了。
# 这是我踩坑后的最终方案
resource "aws_vpc" "main" {
cidr_block = "10.${var.env_id}.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.env_name}-vpc"
Environment = var.env_name
ManagedBy = "terraform"
CreatedAt = timestamp() # 别学我,这会导致每次 plan 都变
}
}
等等,上面那个 timestamp() 我后来去掉了——每次 terraform plan 都提示变化,烦死。血的教训。
子网划分:别省那点 IP
我见过最离谱的配置是一个 /24 子网里跑了 200 多个 EC2。NAT 网关的带宽直接被打满,P99 从 200ms 飙到 3.2s。
推荐子网大小:
| 子网类型 | 推荐 CIDR | 可用 IP | 适用场景 |
|---|---|---|---|
| 公有子网 | /24 | 251 | ALB、NAT 网关、堡垒机 |
| 私有应用子网 | /20 | 4091 | ECS、EKS worker nodes |
| 私有数据子网 | /22 | 1019 | RDS、ElastiCache、Aurora |
别问我为什么数据子网用 /22——RDS 多 AZ 部署加上 read replica,IP 消耗比你想象的快得多。
路由表:显式优于隐式
Terraform 的 aws_route_table 和 aws_main_route_table 这俩资源,我用一次翻车一次。主路由表太隐式了,某次同事手贱改了关联,结果所有私有子网都跑到了互联网上——幸亏是 staging 环境。
# 显式关联,别偷懒
resource "aws_route_table_association" "private_app" {
count = length(var.private_app_subnet_cidrs)
subnet_id = aws_subnet.private_app[count.index].id
route_table_id = aws_route_table.private.id
}
安全组 vs NACL:别搞混了
这是社区里吵得最凶的话题之一。我的结论很简单:
- 安全组:有状态,用于 EC2/ELB 级别。99% 的场景用这个就够了。
- NACL:无状态,用于子网级别。只有需要显式拒绝特定 IP 时才用。
某次我们被 DDoS,安全组没法快速封禁来源 IP,NACL 救了一命。但平时真别用 NACL——调试无状态规则能让你怀疑人生。
VPC Flow Logs:不开等于裸奔
我看到 Reddit 上有人问"为什么我的流量莫名其妙被拒绝了?"——没有 Flow Logs,你连查都查不了。我们团队把 Flow Logs 丢到 CloudWatch Logs,然后接 Athena 查询。
resource "aws_flow_log" "main" {
iam_role_arn = aws_iam_role.flow_log.arn
log_destination = aws_cloudwatch_log_group.flow_log.arn
traffic_type = "ALL"
vpc_id = aws_vpc.main.id
}
traffic_type = "ALL" 别省,ACCEPT 和 REJECT 都要看。上次排查一个跨 VPC 访问问题,就是靠 REJECT 日志发现安全组规则写错了。
NAT 网关:贵,但值得
单 AZ 的 NAT 网关一个月大概 $32,加上数据传输费。我们试过用 NAT 实例省钱,结果运维成本翻了三倍。某次实例挂了,整个私有子网出不去,EKS 节点拉不了镜像,全线瘫痪。
结论: 生产环境至少两个 AZ 各部署一个 NAT 网关,别心疼那点钱。单点故障的代价远高于 NAT 网关的费用。
Terraform State:别放在本地
“Remote state or go home”——这话虽然糙,但理不糙。我们团队用 S3 + DynamoDB 锁,配置如下:
terraform {
backend "s3" {
bucket = "mycompany-terraform-state"
key = "vpc/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-lock"
encrypt = true
}
}
DynamoDB 锁是必须的,不然两个人同时 apply 直接炸裂。我们有一次就是忘了加锁,结果 state 文件损坏,恢复花了整整一天。
环境隔离:Workspace 不够用
Terraform Workspace 听起来很美,但实践下来问题一堆——state 文件混在一起,一不小心就把 prod 的配置 apply 到 dev 上了。
我的方案: 用目录隔离。
terraform/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ └── prod/
└── modules/
├── vpc/
└── security-groups/
每个环境有自己的 backend 配置,永远不可能搞混。多花 5 分钟创建目录,省下 5 小时排查环境混淆的问题。
标签:不是装饰品
AWS 账单按标签分组,没标签你连哪个部门花了多少钱都查不出来。我们团队强制以下标签:
Environment: dev/staging/prod
Team: platform/data/ml
CostCenter: xyz-123
Owner: user@company.com
ManagedBy: terraform
说个真事:某个月 AWS 账单突然多了 $2000,查了半天发现是一个废弃的 NAT 网关没删。如果当时有 Owner 标签,直接找到负责人,5 分钟解决。
常见问题 FAQ
Q: VPC 对等连接还是 Transit Gateway? A: 少于 5 个 VPC 用对等连接,超过就用 Transit Gateway。对等连接不支持传递路由,这是最大的坑。
Q: 每个环境都要独立的 VPC 吗? A: 必须的。共享 VPC 的风险太大——dev 的误操作可能影响 prod。虽然 AWS 有 RAM 共享,但生产环境我建议完全隔离。
Q: Terraform 版本怎么管理?
A: required_version 锁定大版本,比如 >= 1.0, < 2.0。别用 latest,某次 Terraform 1.5 升级把我们的 provider 搞崩了。
Q: VPC 的 CIDR 怎么规划? A: 预留足够空间。我们一个中型项目用了 3 个 /16,现在后悔没留更大的。建议用 RFC 1918 地址,10.0.0.0/8 够你用到退休。
Q: 私有子网访问 S3 用啥? A: VPC Endpoint (Gateway 类型),免费且不走公网。别用 NAT 网关访问 S3,浪费钱还慢。
总结表
| 实践 | 推荐 | 不推荐 |
|---|---|---|
| State 存储 | S3 + DynamoDB | 本地文件 |
| 环境隔离 | 目录隔离 | Workspace |
| 子网大小 | /20 起 | /24 以下 |
| NAT 方案 | NAT 网关 | NAT 实例 |
| 流量日志 | Flow Logs | 不开 |
| 路由关联 | 显式资源 | 主路由表 |
| 标签策略 | 强制标签 | 无标签 |
最后说一句:Terraform 是个好工具,但它不会替你思考。每个 terraform apply 之前,想想这行代码在生产环境会不会炸。我们团队现在强制 PR review + terraform plan 输出审查,翻车率从 30% 降到了 5% 以下。
别让你的 VPC 成为下一个事故报告的主角。