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 在流水线中横向校验环境变更。
四、环境目录内部的最小实现
以 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 文件。满足这五条,目录结构即具备可持续扩展的基础。