3. Terraform代码规范与风格治理
当团队从“一个人写 Terraform”进入“多人协作维护基础设施”阶段,代码风格不一致带来的成本会迅速放大:有人用 snake_case,有人用 camelCase;有人把变量默认值写死在模块里,有人坚持全部由根模块传入;有人把 20 个资源塞进一个 main.tf,有人拆成十几个文件却缺乏统一入口。这些问题不会让 terraform apply 立刻失败,却会让评审变慢、变更风险变高、新人上手周期变长。本节的目标不是追求“好看”,而是建立一套可自动检查、可被 CI 拦截、可随团队演进的规范体系。
命名规范:从资源地址到变量名
Terraform 的命名规范应覆盖四类对象:资源与数据源、变量、输出、模块。推荐统一采用小写下划线风格(snake_case),因为 Terraform 官方风格指南即以此为准,且与 HCL 属性名一致,避免大小写混用导致的引用歧义。
资源命名建议采用“资源类型_用途_环境”结构,例如 aws_instance.web_prod、aws_s3_bucket.logs_staging。这里的“用途”应表达业务语义而非技术细节,避免出现 aws_instance.instance1 这类无信息量名称。数据源建议加 data_ 前缀或使用 existing_ 语义,例如 data.aws_vpc.existing_vpc,让读者一眼区分“创建”与“引用”。
变量命名要避免把类型写进名字,例如 instance_type_string 是冗余的;但可以保留单位或语义后缀,如 timeout_seconds、disk_size_gb。输出名应描述“消费者拿到什么”,例如 vpc_id、load_balancer_dns_name,而不是 output1。
模块结构:让入口、实现与接口分离
一个可维护的模块通常包含以下文件:main.tf 放核心资源,variables.tf 放输入声明,outputs.tf 放输出声明,versions.tf 锁定 provider 与 Terraform 版本,README.md 说明用途与示例。对于复杂模块,可再拆出 locals.tf、data.tf、iam.tf 等按职责划分的文件,但必须保证 variables.tf 与 outputs.tf 始终是模块的“公开接口”。
模块目录建议采用 modules/<模块名>/ 与 environments/<环境>/ 分离的布局。环境目录只负责组合模块、传入参数、声明 backend;模块目录只负责实现资源,不包含具体环境值。这样同一模块可被 dev、staging、prod 复用,差异通过变量注入而非复制代码实现。
变量与输出规范:接口即契约
变量声明必须包含 type 与 description,是否设置 default 取决于该值是否“环境无关”。建议遵循以下判断:若变量在不同环境取值不同,则不设默认值,强制调用方显式传入;若变量有安全且通用的默认值(如 tags = {}),可设默认值。对敏感变量使用 sensitive = true,避免日志泄露。
输出同样需要 description,并尽量只暴露必要字段。输出过多会让模块耦合变重;输出过少则迫使调用方用 data 反查,增加隐式依赖。一个实用原则是:输出应覆盖“调用方组合该模块时真正需要的连接点”,例如 VPC 模块输出 vpc_id 与 private_subnet_ids,而不输出每个子网的 CIDR。
terraform fmt:格式一致性的最低成本手段
terraform fmt 负责对齐、缩进、换行等纯格式问题,不改变语义。团队应统一使用 terraform fmt -recursive 对整个仓库格式化,并在 CI 中使用 terraform fmt -check -recursive 作为门禁。注意 -check 只报告不修改,适合流水线;本地开发可先执行 terraform fmt -recursive 自动修复。
# 本地格式化
terraform fmt -recursive
# CI 检查(有未格式化文件则退出码非 0)
terraform fmt -check -recursive
# 查看哪些文件需要格式化
terraform fmt -check -recursive -list=true
需要强调的是,fmt 只解决“排版”,不解决命名、模块边界、变量默认值等语义规范。因此它必须与 lint 规则配合使用。
自定义 lint 规则:把团队约定变成可执行检查
通用工具如 tflint 已覆盖大量规则,但团队特有约定往往需要自定义。常见做法有三种:一是编写 tflint 插件或使用其 rule 配置;二是用 conftest + OPA/Rego 对 plan JSON 做策略检查;三是用脚本对 HCL 做静态扫描。下面给出一个基于 grep 与 awk 的轻量命名检查示例,适合快速落地。
#!/usr/bin/env bash
# scripts/check-naming.sh
# 检查资源名是否使用 snake_case,且不以数字结尾
set -euo pipefail
fail=0
while IFS= read -r line; do
name=$(echo "$line" | sed -E 's/.*resource "[^"]+" "([^"]+)".*/\1/')
if [[ ! "$name" =~ ^[a-z][a-z0-9_]*$ ]]; then
echo "命名不合规: $name (应使用 snake_case)"
fail=1
fi
done < <(grep -rE '^resource "' --include='*.tf' .)
exit $fail
更推荐的方式是使用 tflint 的 naming_convention 规则,通过 .tflint.hcl 配置命名格式、前缀与后缀。例如强制资源名以 _prod 或 _dev 结尾,或禁止使用 temp、test 等临时词。配置示例如下:
rule "naming_convention" {
enabled = true
format = "snake_case"
resource {
prefix = ""
suffix = ""
}
variable {
format = "snake_case"
}
output {
format = "snake_case"
}
}
落地节奏与例外管理
规范治理最怕“一次性全量整改”。建议分三步:第一步只开启 fmt 与 validate,让格式先统一;第二步引入 tflint 的少量高价值规则(命名、未使用变量、provider 版本),在 CI 中设为 warning 而非 error;第三步将 warning 逐步升级为 error,并建立例外登记机制——对确实无法满足的规则,在代码中以注释 # tflint-ignore: rule_name 显式豁免,并在 PR 中说明原因。这样既保持规范刚性,又避免“为了过检查而写坏代码”。
.tflint.hcl、.pre-commit-config.yaml 与 CI 配置一起纳入版本控制,使规范本身也可评审、可回滚。规范变更应像代码变更一样走 PR,而不是口头约定。最终判断标准不是“检查是否全绿”,而是:新成员能否在一天内按模板写出符合规范的模块;评审者能否把精力放在架构与风险上,而不是格式与命名;当规范需要调整时,能否只改一处配置就让全仓库生效。做到这三点,Terraform 代码规范才真正成为团队质量治理的基础设施。