4. 环境抽象与变量分层策略
当 Terraform 从单环境走向多环境时,最先失控的往往不是资源本身,而是“差异”。开发、测试、预发、生产四套环境,90% 的资源配置是相同的,剩下 10% 的差异——实例规格、副本数、CIDR、域名、标签、开关——如果散落在各处的 main.tf 里,就会迅速演变成配置漂移:有人改了生产没同步测试,有人复制了一份 dev 目录改出 staging,半年后没人说得清哪套才是“标准”。环境抽象与变量分层要解决的,正是把“共性”沉淀为可复用模块,把“差异”收敛到少数几个受控入口,并让这些入口本身具备隔离与审计能力。
一、先分清“环境差异”的三种性质
不是所有差异都该用同一种方式管理。实践中可以把差异分成三类,分别对应不同的承载机制:
- 结构性差异:某环境多一个只读副本、多一套灾备 VPC。这类差异改变的是资源拓扑,不适合用变量开关硬塞,应通过模块组合或独立的环境根目录表达。
- 参数性差异:实例类型、磁盘大小、副本数量、保留天数。这类差异是同一结构的取值不同,最适合用变量分层。
- 敏感性差异:数据库密码、API Key、证书私钥。这类差异不能进版本库,必须走密钥管理或环境变量注入。
把这三类混在一起,是配置漂移的根源。下面的图展示了从“共性模块”到“环境差异”再到“敏感注入”的分层收敛关系,以及每一层应该由谁来承载。
二、tfvars 分层:从“一个文件”到“三层叠加”
最朴素的做法是每个环境一个 terraform.tfvars,但很快会遇到重复:四个环境里 region、project、owner 完全一样,却要抄四遍。更稳妥的是三层叠加:
envs/
├── common.tfvars # 所有环境共享:region、project、tags
├── dev/
│ ├── env.tfvars # dev 专属:cidr、instance_type、replicas
│ └── backend.hcl # dev 的 state 后端配置
├── staging/
│ ├── env.tfvars
│ └── backend.hcl
└── prod/
├── env.tfvars
└── backend.hcl
执行时按顺序传入,后面的覆盖前面的:
terraform apply \
-var-file=../common.tfvars \
-var-file=env.tfvars \
-var="image_tag=${GIT_SHA}"
这样 common.tfvars 是唯一的“标准基线”,任何环境想偏离基线,必须在自己的 env.tfvars 里显式写出,评审时一眼可见。反过来,如果某个参数在四个环境里都相同,就应该上移到 common.tfvars,而不是留在各环境里——这是抑制漂移最有效的习惯。
三、Terragrunt:把“重复的根目录”也抽象掉
tfvars 分层解决了变量差异,但每个环境仍然要维护一份 main.tf、provider.tf、backend.tf,这些文件内容几乎一样。Terragrunt 的价值在于用 terragrunt.hcl 把“模块引用 + 输入变量 + 后端配置”声明化,并通过 include 继承根配置:
# envs/terragrunt.hcl(根配置)
remote_state {
backend = "s3"
config = {
bucket = "acme-tfstate-${local.env}"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "cn-north-1"
}
}
# envs/prod/app/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../../modules//app"
}
inputs = {
instance_type = "m5.xlarge"
replicas = 5
cidr_block = "10.40.0.0/16"
}
关键收益是:state 路径由目录结构自动推导,天然实现环境隔离;inputs 就是该环境的全部差异,集中在一个文件里;根配置统一了后端、provider 版本、标签策略,改一处全环境生效。代价是引入了一个额外工具层,团队需要理解 include 与 dependency 的解析顺序。
四、安全隔离:三条不能破的线
分层方案再优雅,如果隔离没做好,一次误操作就能把生产打穿。以下三条是底线:
- state 必须物理隔离。每个环境独立的 backend key 或独立 bucket,配合独立的 IAM 角色。绝不允许 dev 的 CI 凭证有权限读写 prod 的 state。
- 敏感变量不进版本库。密码、密钥只通过
TF_VAR_*环境变量或密钥管理服务注入。如果必须落盘,用 sops 加密后提交密文,解密密钥由 CI 的 KMS 权限控制。 - 生产变更需要显式确认。在 CI 中为 prod 目录设置人工审批门禁,plan 输出必须包含资源变更摘要,apply 前二次确认。dev 可以自动 apply,prod 不行。
workspace 做环境隔离。Terraform workspace 共享同一份后端配置和同一套变量文件,只是 state 前缀不同,很容易出现“在 dev workspace 里 apply 了 prod 变量”的事故。workspace 适合临时并行实验,不适合作为正式的多环境隔离机制。
五、一个可落地的分层检查清单
在把方案推给团队之前,用下面这份清单自查,能挡掉大部分后续漂移:
| 检查项 | 合格标准 | 不合格信号 |
|---|---|---|
| 共性参数位置 | 只出现在 common.tfvars 或模块默认值 | 同一参数在 3 个以上环境文件里重复 |
| 环境差异可见性 | 每个环境的差异集中在一个 env.tfvars | 差异散落在 main.tf、脚本、CI 变量中 |
| state 隔离 | 每环境独立 backend key + 独立 IAM | 共用 bucket 且无前缀隔离 |
| 敏感值 | 版本库中无明文密钥 | tfvars 里出现 password = "..." |
| 模块版本 | module 引用带版本号或 commit SHA | 直接引用本地路径且无版本约束 |
| 漂移检测 | 定期对 prod 执行 plan 并告警非空 diff | 只在 apply 时才跑 plan |
回到本质:环境抽象不是把变量拆得越细越好,而是让“什么是标准、什么是例外”变得一目了然。共性沉到模块和 common 层,差异收敛到每个环境的一个文件,敏感值走独立通道,state 按环境硬隔离。做到这四点,配置漂移就从“靠人自觉”变成了“结构上难以发生”。