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 规范治理闭环:规范 → 自动检查 → CI 拦截 → 反馈修正 规范定义 • 命名:snake_case • 模块:接口/实现分离 • 变量:type+description • 输出:最小必要集 • 版本:versions.tf 锁定 自动检查工具 • terraform fmt -check • terraform validate • tflint(规则集) • checkov / tfsec • 自定义 lint 脚本 CI 流水线拦截 • pre-commit 钩子 • PR 触发检查 • 失败即阻断合并 • 报告注释到 PR • 规范版本化 反馈修正 • 自动修复 • 人工评审 • 规范迭代 • 例外登记 规范迭代回写 示例:CI 中串联检查命令 terraform fmt -check -recursive # 格式一致性 terraform validate # 语法与引用有效性 tflint --recursive # 规则集检查 ./scripts/check-naming.sh # 自定义命名规则

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 代码规范才真正成为团队质量治理的基础设施。