3. 多环境标准化目录结构设计

当 Terraform 从单环境试验走向多环境交付时,最先失控的往往不是代码,而是目录。开发、测试、预发、生产四套环境如果各自复制一份配置,模块版本、后端状态、变量默认值会迅速分叉;如果全部塞进一个目录,又无法隔离状态与权限。本节要解决的核心问题是:设计一套可扩展的仓库目录,让环境、模块、全局配置、策略与 CI/CD 各归其位,并通过命名规范约束它们的引用关系。

一、目录分层的职责边界

推荐采用“按职责分层、按环境实例化”的结构。五个顶层目录各承担单一职责:

  • environments/:环境实例层。每个环境一个目录,只放该环境的后端配置、变量赋值和模块调用,不放资源定义本身。
  • modules/:可复用模块层。每个模块自包含 main.tf、variables.tf、outputs.tf、versions.tf,对外只暴露变量与输出。
  • global/:全局共享配置。如 IAM 基础角色、DNS 托管区、Terraform 远程后端存储桶、密钥管理,这些跨环境唯一且先于环境创建。
  • policies/:策略即代码。存放 OPA/Sentinel 策略、合规检查规则,供 CI 在 plan 阶段校验。
  • ci/:流水线定义。存放各环境的 pipeline 模板、变量文件与执行脚本,把“怎么跑 Terraform”从业务配置中剥离。

这种分层的价值在于:模块变更影响所有环境但只改一处;环境差异被压缩到 variables 与 backend 两个文件;策略与流水线可独立演进,不污染资源代码。

二、真实项目结构示例

下面是一个可直接落地的仓库骨架,命名统一使用小写加连字符,环境目录用短名(dev/staging/prod),模块目录用“资源-用途”命名:

terraform-infra/
├── environments/
│   ├── dev/
│   │   ├── backend.tf          # 后端:key = dev/terraform.tfstate
│   │   ├── main.tf             # 调用 modules/network、modules/app
│   │   ├── variables.tf
│   │   └── terraform.tfvars    # 环境专属赋值
│   ├── staging/
│   │   ├── backend.tf
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── terraform.tfvars
│   └── prod/
│       ├── backend.tf
│       ├── main.tf
│       ├── variables.tf
│       └── terraform.tfvars
├── modules/
│   ├── network-vpc/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── outputs.tf
│   │   └── versions.tf
│   ├── app-service/
│   └── database-rds/
├── global/
│   ├── iam-baseline/
│   ├── dns-zone/
│   └── state-backend/
├── policies/
│   ├── deny-public-s3.rego
│   └── require-tags.rego
└── ci/
    ├── pipelines/
    │   ├── dev.yml
    │   └── prod.yml
    └── scripts/
        └── plan.sh

关键点在于 environments 下的每个目录都是一个独立的 Terraform 根模块,拥有自己的 state。dev 与 prod 之间不共享 state,也不通过相对路径互相引用,只通过 modules/ 复用逻辑。

三、命名规范与引用约定

命名规范是目录可扩展的前提。建议约定:

  • 目录名一律小写,单词间用连字符,如 network-vpc,避免下划线与驼峰混用。
  • 环境短名固定为 dev、staging、prod,禁止出现 dev2、prod-new 这类临时名,新环境应通过新增目录并同步 ci 配置实现。
  • 模块 source 统一使用相对路径 ../../modules/network-vpc,禁止跨环境引用 ../prod。
  • state key 与目录路径保持一致,如 prod/terraform.tfstate,便于按环境定位与权限隔离。
  • 变量文件统一命名 terraform.tfvars,敏感值不落盘,改用 CI 注入的 TF_VAR_ 环境变量。

下图展示了五个目录之间的引用与数据流关系:环境目录向下调用模块,全局配置为环境提供后端与基础资源,策略与 CI 在流水线中横向校验环境变更。

environments/ 环境实例层(各自独立 state) dev/ backend.tf · main.tf · tfvars staging/ backend.tf · main.tf · tfvars prod/ backend.tf · main.tf · tfvars source = ../../modules/... modules/ 可复用模块层(逻辑唯一) network-vpc app-service database-rds global/ iam-baseline dns-zone state-backend policies/ deny-public-s3.rego require-tags.rego plan 阶段校验 ci/ pipelines/dev.yml pipelines/prod.yml scripts/plan.sh 提供后端/基础资源 合规校验 执行 plan/apply

四、环境目录内部的最小实现

以 dev 环境为例,main.tf 只做模块编排,不写资源:

# environments/dev/main.tf
module "network" {
  source      = "../../modules/network-vpc"
  env         = "dev"
  cidr_block  = var.vpc_cidr
  tags        = local.common_tags
}

module "app" {
  source      = "../../modules/app-service"
  env         = "dev"
  vpc_id      = module.network.vpc_id
  subnet_ids  = module.network.private_subnet_ids
  instance_count = 2
}

backend.tf 中通过 key 区分环境状态:

# environments/dev/backend.tf
terraform {
  backend "s3" {
    bucket = "company-tfstate"
    key    = "dev/terraform.tfstate"
    region = "cn-north-1"
  }
}

prod 目录结构完全相同,仅 terraform.tfvars 中 instance_count = 6、vpc_cidr = "10.20.0.0/16" 等取值不同。这样“结构一致、取值不同”的差异被限制在变量层,评审时只需对比 tfvars 即可判断环境差异是否合理。

五、可扩展性与常见陷阱

当新增一个环境(如 uat)时,正确做法是复制 environments/dev 为 environments/uat,修改 backend key 与 tfvars,并在 ci/pipelines 下新增 uat.yml。模块与策略无需改动,这就是分层带来的扩展性。

常见陷阱:一是把模块直接放在 environments 下,导致无法复用;二是环境目录之间用相对路径互相引用,破坏 state 隔离;三是把敏感变量写入 tfvars 并提交仓库,应改用 CI 注入的 TF_VAR_ 变量或密钥管理服务;四是 policies 与 ci 混入环境目录,使流水线逻辑随环境复制而分叉。

落地检查清单:① 每个环境目录是否都有独立 backend key;② 模块 source 是否只指向 modules/;③ 环境间是否存在横向引用;④ 敏感值是否未落盘;⑤ 新增环境是否只需新增目录与 pipeline 文件。满足这五条,目录结构即具备可持续扩展的基础。