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):可选层,用于存放某个业务团队特有的组合模块或临时实验。它必须通过平台仓库审批后才能引用到环境仓库,避免绕过治理。

这四层的关系不是简单的包含,而是引用与约束:环境仓库引用模块仓库的版本,平台仓库约束环境仓库的可用模块和参数范围,团队仓库在平台仓库的审批下向环境仓库贡献配置。

模块仓库 terraform-modules vpc/ v2.3.1 rds/ v1.8.0 k8s-cluster/ v3.1.2 iam-baseline/ v1.2.0 Git Tag 发布 · 语义化版本 环境仓库 terraform-envs prod-cn-north/ staging-cn-east/ dev-cn-south/ sandbox-teamA/ 根配置 · 引用模块版本 平台仓库 terraform-platform policies/ (OPA) pipelines/ (CI/CD) backend/ (state) accounts/ (基线) 约束 · 审批 · 状态后端 团队仓库(可选) terraform-team-* composite/ 组合模块 experimental/ 实验配置 需平台审批后引用 引用 约束 审批后贡献 团队职责边界 平台团队:维护模块仓库 + 平台仓库,定义模块接口、策略、流水线模板 业务团队:维护环境仓库中自己的环境目录,选择模块版本、传入变量,不修改模块内部实现

环境目录的粒度:按“生命周期”而非“业务线”划分

环境目录的划分方式直接决定状态文件(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_blockstringVPC 网段"10.0.0.0/16"
az_countnumber可用区数量3
enable_natbool是否启用 NAT 网关true
tagsmap(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)的修改必须附带测试用例。
  • 团队仓库:业务团队自由写,但引用到环境仓库时,平台团队检查其是否封装了合规的模块版本。
实践建议:在 CI 流水线中增加一个“模块版本白名单”检查。环境仓库的 MR 中如果引用了不在白名单内的模块版本,流水线直接失败。白名单由平台仓库维护,例如 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 套环境,把模块和环境放在同一个仓库、用目录隔离即可。架构设计的目的是让协作成本随规模增长而缓慢上升,而不是一开始就追求“大厂同款”。