3. 基础设施代码仓库设计

基础设施代码仓库不是简单的“把 .tf 文件放进 Git”。它同时承担四件事:让环境之间边界清晰、让模块可复用、让变更可追溯、让多人协作不互相踩踏。设计失当的仓库,往往在第二个环境、第三个团队加入时开始失控:dev 的变量被复制到 prod、模块被就地修改导致其他环境意外变更、state 文件被两人同时 apply 而锁冲突。本节围绕目录结构、环境分层、模块复用、版本控制与协作规范展开,并给出可直接参考的仓库骨架。

一、目录结构:按“环境—模块—全局”三层组织

推荐采用“环境目录 + 模块目录 + 全局共享”的顶层布局。环境目录存放每个环境独立的根模块(root module),只负责组合模块、传入变量、声明 backend;模块目录存放可复用的子模块;全局目录存放跨环境共享但需单独管理的东西,例如 IAM、DNS 托管区、Terraform Cloud/后端资源本身。

infra-repo/
├── environments/
│   ├── dev/
│   │   ├── main.tf          # 组合模块
│   │   ├── variables.tf
│   │   ├── terraform.tfvars # dev 参数
│   │   └── backend.tf       # state 指向 dev key
│   ├── staging/
│   └── prod/
├── modules/
│   ├── network/             # VPC、子网、路由
│   ├── compute/             # 实例/容器编排
│   └── database/            # RDS 等
├── global/
│   └── iam/
└── .github/workflows/       # CI 流水线

关键原则:环境目录之间不互相引用。dev 的根模块不能 source 到 prod 的目录,否则一次改动会跨环境传播。环境之间共享的只能是 modules/ 下的子模块,且通过版本约束固定。

二、环境分层:用变量与 backend 隔离,而非复制代码

环境分层的目标是“同一套模块,不同参数与不同 state”。每个环境拥有独立的 state 文件,通常通过 backend 的 key 区分:

# environments/prod/backend.tf
terraform {
  backend "s3" {
    bucket         = "company-tfstate"
    key            = "prod/network/terraform.tfstate"
    region         = "cn-north-1"
    dynamodb_table = "tf-lock"
    encrypt        = true
  }
}

dev 只需把 key 改为 dev/network/terraform.tfstate。这样同一模块在 dev 与 prod 各自维护状态,互不影响。环境差异通过 terraform.tfvars 表达,例如实例规格、副本数、是否开启多可用区。建议在 variables.tf 中为每个变量写明类型与默认值,并在 tfvars 中只覆盖需要变化的项,避免“每个环境一份完整变量表”带来的漂移。

环境命名建议固定为 dev / staging / prod,并在 CI 中按目录名自动选择 workspace 或 backend key,减少人工判断。

三、模块复用:接口稳定优先于实现灵活

模块的价值在于“一次编写、多处调用”,但前提是接口稳定。一个可复用模块应满足:输入变量有明确类型与描述、输出值覆盖调用方需要的引用、内部资源命名不依赖外部环境名。例如 network 模块:

# modules/network/variables.tf
variable "vpc_cidr" {
  type        = string
  description = "VPC 网段,例如 10.0.0.0/16"
}
variable "az_count" {
  type        = number
  default     = 2
  description = "可用区数量"
}

# modules/network/outputs.tf
output "vpc_id" { value = aws_vpc.this.id }
output "subnet_ids" { value = aws_subnet.public[*].id }

调用方在 environments/dev/main.tf 中:

module "network" {
  source   = "../../modules/network"
  vpc_cidr = "10.10.0.0/16"
  az_count = 2
}

模块版本控制有两种常见方式:一是同仓库内相对路径引用,适合模块与环境同步演进;二是独立模块仓库加 Git tag 引用,例如 source = "git::https://...//modules/network?ref=v1.4.0",适合多仓库共享。若模块被多个环境使用,建议打 tag 并在环境目录中固定 ref,避免“模块一改,所有环境下次 plan 全变”。

四、版本控制策略与团队协作规范

基础设施代码的版本控制不只是 Git 分支,还包括 state 版本与模块版本。建议:主分支保护,禁止直接 push;所有变更走 Pull Request;CI 在 PR 阶段执行 terraform fmt -check、terraform validate 与 terraform plan,并把 plan 结果作为评论附在 PR 上。合并到 main 后,由流水线对目标环境执行 apply,prod 可加人工审批。

协作规范中必须明确:谁可以改 prod、谁负责 review、state 锁冲突如何处理。常见做法是 prod 目录的 CODEOWNERS 指定平台团队,dev/staging 由业务团队自管。变量命名统一小写下划线,资源命名包含环境前缀,例如 prod-vpc-main,便于在云控制台快速识别归属。

基础设施代码仓库:环境分层与模块复用关系 modules/ 可复用子模块(接口稳定,按 tag 固定) network vpc_cidr / az_count compute instance_type / count database engine / storage_gb tag: v1.4.0 版本固定 environments/ 环境根模块(独立 state,独立参数) dev key: dev/network/… vpc_cidr 10.10.0.0/16 staging key: staging/network/… vpc_cidr 10.20.0.0/16 prod key: prod/network/… vpc_cidr 10.30.0.0/16 团队协作与 CI 流水线 Pull Request fmt / validate / plan Review + 审批 CODEOWNERS 合并 main 触发 apply prod 人工确认 审批后执行 apply 目标

上图的读法:modules/ 提供稳定接口,environments/ 下每个环境通过相对路径或 tag 引用模块,并各自持有独立 backend key;PR 阶段完成格式、校验与 plan,合并后由流水线按目录选择目标环境执行 apply,prod 额外增加人工确认。这样,模块复用与环境隔离同时成立,团队协作也有明确的准入与审批路径。

不要把 state 文件提交到 Git。state 可能包含敏感属性(如数据库密码、私钥),应存放在加密的远程后端,并开启版本控制与访问审计。

最后给出一个可落地的检查清单,用于评审仓库设计是否达标:环境目录是否互不引用;每个环境是否有独立 backend key;模块是否通过 tag 或明确版本固定;变量是否有类型与描述;CI 是否在 PR 阶段执行 plan;prod 是否有 CODEOWNERS 与人工审批;state 是否远程加密存储。满足这些条件,仓库才具备支撑多环境、多团队持续交付的基础。