3. 模块化设计原则
在 Terraform 中,模块不是简单地把资源文件挪到一个目录里,而是对基础设施能力的一次边界划分。划分得好,模块像标准件一样可复用、可升级、可组合;划分得差,模块会变成“参数搬运工”,调用方需要了解内部实现,改一处牵动全局。本节围绕五条原则展开:高内聚低耦合、单一职责、接口稳定、版本化、可组合性,并说明它们如何落到 Terraform 的目录结构、变量、输出、Provider 与版本约束上。
一、高内聚低耦合:把变化关在模块内部
高内聚指模块内部资源围绕同一目标协作,低耦合指模块之间尽量只通过输入变量和输出值交互。以“创建一套带日志与加密的 S3 存储桶”为例,桶、桶策略、生命周期规则、服务端加密、访问日志桶应放在同一模块内,因为它们随同一业务需求变化;而“谁可以访问这个桶”的 IAM 角色绑定,更适合由调用方或独立 IAM 模块处理,避免存储模块承担权限职责。
落地时可用一条判断标准:如果修改模块内部某个资源属性,调用方是否需要同步修改?若需要,说明耦合偏高。例如模块内部把桶名写死为 app-logs-prod,调用方就无法在测试环境复用;应改为变量 bucket_name,由调用方传入。低耦合还要求模块不直接读取调用方的本地文件、不隐式依赖调用方已创建的资源,除非通过变量显式传入。
二、单一职责:一个模块只回答一个问题
单一职责不是“一个模块只能有一个资源”,而是“一个模块只对一类变化负责”。网络模块负责 VPC、子网、路由表、安全组;计算模块负责实例、启动模板、自动伸缩组;数据库模块负责实例、参数组、备份策略。若把 VPC 与 RDS 放进同一模块,网络策略变化和数据库版本升级会互相干扰,测试与复用都会变难。
实际划分时,可以按“生命周期”和“变更频率”辅助判断:VPC 很少重建,RDS 可能因版本升级而重建,二者生命周期不同,应拆开。另一个常见误区是把“环境差异”塞进同一模块的大量 if 分支,导致模块内部出现 count = var.env == "prod" ? 1 : 0 这类复杂条件。更好的做法是抽出公共模块,再用环境目录传入不同参数。
三、接口稳定:变量与输出是模块的合同
模块接口包括输入变量、输出值和必要的 Provider 要求。接口稳定意味着:新增功能尽量以可选变量加入,不随意重命名或删除已有变量;输出值一旦被下游引用,就不要轻易改变名称和类型。变量应设置清晰的 type、description,对关键参数设置 default 或 validation,让错误在 terraform plan 阶段暴露,而不是等到 apply 中途失败。
variable "bucket_name" {
type = string
description = "S3 存储桶名称,需全局唯一"
validation {
condition = can(regex("^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$", var.bucket_name))
error_message = "桶名只能包含小写字母、数字和连字符,长度 3-63。"
}
}
variable "enable_versioning" {
type = bool
description = "是否开启对象版本控制"
default = true
}
output "bucket_arn" {
value = aws_s3_bucket.this.arn
description = "存储桶 ARN,供 IAM 策略引用"
}
输出应只暴露调用方真正需要的值,例如 bucket_arn、bucket_id,而不是把内部所有资源属性都输出。输出过多会扩大接口面积,增加后续维护成本。
四、版本化:让升级可控、可回退
当模块被多个环境或多个团队引用时,必须版本化。Terraform 模块来源支持 Git 标签、Registry 版本和本地路径。生产环境应引用固定版本,例如:
module "s3_logs" {
source = "git::https://example.com/infra/modules/s3.git?ref=v1.4.0"
bucket_name = "app-logs-prod"
}
版本号建议遵循语义化版本:修复 bug 升 patch,新增向后兼容的可选变量升 minor,删除变量、改变输出类型或默认行为升 major。升级时先在测试环境把 ref 改为新版本,执行 terraform plan,重点观察是否出现资源重建、输出变化或变量缺失;确认后再合并到生产。若模块内部资源地址发生变化,应使用 moved 块或 terraform state mv 避免误删重建。
五、可组合性:小模块拼出完整平台
可组合性要求模块之间通过输出与输入自然拼接,而不是互相硬编码。例如网络模块输出 vpc_id 和 private_subnet_ids,计算模块接收这些值创建实例,数据库模块再接收安全组 ID 和子网 ID。组合关系应像下面这样清晰:
可组合性还要求模块不要互相调用。若网络模块内部直接调用数据库模块,就会形成隐式依赖,调用方无法单独替换数据库实现。正确做法是根模块负责编排,子模块只暴露能力。对于必须共享的标签、命名前缀、Provider 配置,可通过变量传入,而不是让子模块自行读取全局变量。
六、把原则落到目录与评审清单
一个可落地的模块目录通常包含:main.tf 放资源,variables.tf 放输入,outputs.tf 放输出,versions.tf 锁定 Terraform 与 Provider 版本,README.md 说明用途与示例,examples/ 提供可运行调用样例。评审模块时,可以逐条检查:模块是否只负责一类变化;变量是否有类型和描述;输出是否最小必要;是否锁定版本;是否通过变量而非硬编码连接其他模块;是否提供示例调用。
最后要强调,五条原则不是彼此独立的。高内聚低耦合决定边界,单一职责决定粒度,接口稳定决定协作成本,版本化决定升级安全,可组合性决定平台能否像积木一样扩展。把它们同时落到变量、输出、目录和版本约束上,Terraform 模块才真正从“代码复用”升级为“平台能力复用”。