3. 平台化交付的架构设计
当 Terraform 从“一个人管一个云账号”走向“一个平台支撑几十个团队、上百套环境”时,最先崩溃的往往不是技术,而是仓库结构。把所有 .tf 文件堆在一个仓库里,短期看似方便,长期会带来三个致命问题:模块改动无法独立发布、环境之间互相污染、团队职责边界模糊导致谁都不敢改。平台化交付的架构设计,本质上就是回答一个问题:代码、模块、环境、职责这四样东西,应该分别放在哪里,又通过什么契约连接起来。
四层仓库拓扑:从“单体”到“分层”
可扩展的 Terraform 交付拓扑通常由四类仓库构成,它们承担不同职责,拥有不同的变更频率和发布节奏:
- 模块仓库(Module Registry):存放可复用的 Terraform 模块,如
vpc、rds、k8s-cluster。每个模块独立版本化,通过 Git Tag 发布,例如v2.3.1。模块仓库是平台团队的核心资产,变更频率中等,但影响面最大。 - 环境仓库(Environment Repo):存放具体环境的根配置(root module),如
prod-cn-north、staging-cn-east。每个环境目录只做三件事:声明 provider、引用模块版本、传入变量。环境仓库变更频率高,但每次变更范围小。 - 平台仓库(Platform Repo):存放 CI/CD 流水线定义、策略即代码(Sentinel/OPA)、状态后端配置、账号基线。它不直接创建业务资源,而是定义“所有环境必须遵守的规则”。
- 团队仓库(Team Repo):可选层,用于存放某个业务团队特有的组合模块或临时实验。它必须通过平台仓库审批后才能引用到环境仓库,避免绕过治理。
这四层的关系不是简单的包含,而是引用与约束:环境仓库引用模块仓库的版本,平台仓库约束环境仓库的可用模块和参数范围,团队仓库在平台仓库的审批下向环境仓库贡献配置。
环境目录的粒度:按“生命周期”而非“业务线”划分
环境目录的划分方式直接决定状态文件(state)的边界。一个常见误区是按业务线划分,比如 order-service/、user-service/,结果每个目录里都包含 VPC、数据库、缓存,导致网络资源被重复创建,跨服务引用变得极其困难。更合理的做法是按生命周期划分:把变更频率相同、销毁时间相同的资源放在同一个状态文件里。
例如,一个典型的三层划分:
terraform-envs/
├── prod-cn-north/
│ ├── 00-network/ # VPC、子网、路由表(极少变更)
│ ├── 10-data/ # RDS、Redis、对象存储(季度级变更)
│ ├── 20-compute/ # ECS、K8s 节点池(周级变更)
│ └── 30-app/ # 应用配置、DNS、SLB(日级变更)
├── staging-cn-east/
│ └── ...(同构目录)
└── dev-cn-south/
└── ...(同构目录)
每个子目录独立执行 terraform apply,独立持有 state。这样做的收益是:网络层变更不会触发计算层重建,数据层可以单独做快照和恢复,应用层可以高频发布而不影响底层。代价是需要通过 terraform_remote_state 或数据源(data source)在层之间传递输出值,例如 00-network 输出 vpc_id,20-compute 通过远程状态读取它。
模块接口设计:把“可变”收敛到变量
模块仓库能否被多环境复用,取决于接口设计。一个可扩展的模块应该做到:调用方只通过变量控制差异,不通过修改模块代码来适配环境。以 VPC 模块为例,它的变量设计应该覆盖:
| 变量名 | 类型 | 说明 | 示例值 |
|---|---|---|---|
cidr_block | string | VPC 网段 | "10.0.0.0/16" |
az_count | number | 可用区数量 | 3 |
enable_nat | bool | 是否启用 NAT 网关 | true |
tags | map(string) | 统一标签 | { env = "prod", team = "platform" } |
环境目录调用时只需写:
module "vpc" {
source = "git::ssh://git@gitlab.example.com/terraform-modules/vpc.git?ref=v2.3.1"
cidr_block = "10.0.0.0/16"
az_count = 3
enable_nat = true
tags = {
env = "prod"
team = "platform"
}
}
注意 ?ref=v2.3.1 这个版本锁定。它让环境仓库的变更与模块仓库的发布解耦:模块团队发布 v2.4.0 后,环境团队可以选择在下一个迭代窗口升级,而不是被动接受。这是平台化交付中“谁发布、谁升级”职责分离的关键机制。
团队职责与审批流:用仓库权限固化边界
架构设计最终要落到权限上。推荐的 Git 权限模型是:
- 模块仓库:平台团队拥有写权限,业务团队只读。模块变更必须经过代码评审 + 自动化测试(
terraform validate、tflint、checkov)才能合并。 - 环境仓库:业务团队拥有自己环境目录的写权限,但
prod-*目录的合并需要平台团队审批。审批检查项包括:模块版本是否在允许列表内、变量是否超出策略范围、是否包含terraform destroy风险操作。 - 平台仓库:仅平台团队可写,变更需要双人评审。策略文件(OPA/Sentinel)的修改必须附带测试用例。
- 团队仓库:业务团队自由写,但引用到环境仓库时,平台团队检查其是否封装了合规的模块版本。
allowed_modules = { vpc = ["v2.3.1", "v2.4.0"], rds = ["v1.8.0"] }。这样既给了业务团队选择权,又防止了未经测试的版本进入生产。
状态后端与工作区:拓扑的“地基”
四层仓库拓扑要真正运转,还需要一个统一的状态后端。推荐每个环境目录使用独立的 state 文件,存放在远程后端(如 S3 + DynamoDB 锁,或 Terraform Cloud)。state 的路径命名应与目录结构对齐,例如:
s3://company-terraform-state/prod-cn-north/00-network/terraform.tfstate
s3://company-terraform-state/prod-cn-north/10-data/terraform.tfstate
s3://company-terraform-state/prod-cn-north/20-compute/terraform.tfstate
这样做的好处是:状态文件与目录一一对应,权限可以按前缀授予,审计日志可以按环境过滤,误删恢复也可以精确到某一层。平台仓库中应包含一个 backend.tf 模板,环境目录通过 terraform init -backend-config=... 注入具体路径,避免每个环境手写后端配置。
可扩展性的判断标准
一个平台化交付拓扑是否可扩展,可以用三个问题检验:新增一个环境需要改几个仓库?(理想情况:只改环境仓库,复制目录并调整变量);模块升级需要通知几个团队?(理想情况:平台团队发布,业务团队按自己的节奏升级);生产事故能否快速定位到某一层?(理想情况:state 按层隔离,变更历史按目录追溯)。如果这三个问题的答案都是“一个”或“可控”,说明拓扑已经具备了横向扩展的基础。
最后需要强调:仓库划分不是越细越好。四层拓扑适合中大型组织;如果团队只有 5 个人、3 套环境,把模块和环境放在同一个仓库、用目录隔离即可。架构设计的目的是让协作成本随规模增长而缓慢上升,而不是一开始就追求“大厂同款”。