1. 为什么需要模块化
一句话总结: 模块是对「一组相关资源 + 稳定接口」的封装,解决复制粘贴、命名约束、变更波及面三大问题,是 Terraform 可维护性的分水岭。
没有模块时,同一个 VPC、安全组、IAM 角色的配置会在十几个目录里各复制一份,改一处要全局搜索替换。模块化之后,同一逻辑只维护一份,通过参数化复用。
无模块:
dev/main.tf ── VPC + 子网 + 安全组 + IAM(手写)
prod/main.tf ── VPC + 子网 + 安全组 + IAM(复制)
staging/main.tf ── ...(再复制)
有模块:
modules/network ── VPC + 子网(一份,参数化)
dev/main.tf ── module "network" { source = "../modules/network" }
prod/main.tf ── module "network" { source = "...", env = "prod" }
1.1 模块带来的收益
| 收益 | 说明 |
|---|---|
| 复用 | 一份逻辑,多环境多项目调用 |
| 抽象 | 使用者只需关心 input/output,不关心内部资源 |
| 隔离 | 变更被限制在模块内部,波及面可控 |
| 规范 | 命名、标签、权限策略被强制统一 |
2. 输入变量 input
一句话总结: input 是模块的「参数表」,用 type、default、validation、nullable 四个维度把模块接口做严谨,接口越严谨,调用方越不会传错。
2.1 变量声明
variable "environment" {
type = string
default = "dev"
description = "部署环境,可选 dev/staging/prod"
}
variable "instance_type" {
type = string
}
variable "subnet_cidrs" {
type = list(string)
}
variable "tags" {
type = map(string)
default = {}
}
variable "ingress_rules" {
type = list(object({
port = number
protocol = string
cidr = string
}))
}
2.2 类型约束与 nullable
variable "bucket_force_destroy" {
type = bool
default = false
}
variable "optional_value" {
type = string
default = null # 允许不传,后续用 coalesce 兜底
}
variable "must_provide" {
type = string # 无 default → 调用方必须传
}
| 维度 | 作用 | 使用建议 |
|---|---|---|
type | 约束类型 | 尽量写具体,避免 any |
default | 兜底值 | 语义稳定才给 default |
nullable | 是否允许 null | 与 default=null 配合做可选参数 |
description | 文档化 | 生成模块文档用 |
validation | 值域校验 | 拦截非法取值 |
2.3 validation 块
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment 必须是 dev/staging/prod 之一"
}
}
variable "cidr" {
type = string
validation {
condition = can(cidrhost(var.cidr, 0))
error_message = "cidr 必须是合法的 CIDR 网段"
}
}
一句话:validation 在 plan 阶段就能拦下非法输入,比到 apply 才报错省得多。多用
can()做「试算式」校验。
3. 输出 output
一句话总结: output 是模块对外的「返回值」,应只暴露调用方真正需要的值,
sensitive标记防止敏感信息被明文打印。
3.1 输出声明
output "vpc_id" {
value = aws_vpc.main.id
}
output "subnet_ids" {
value = aws_subnet.main[*].id
}
output "security_group_id" {
value = aws_security_group.web.id
}
output "database_password" {
value = aws_db_instance.db.password
sensitive = true # apply 后不回显明文
}
3.2 调用方消费
module "network" {
source = "../modules/network"
}
resource "aws_instance" "web" {
subnet_id = module.network.subnet_ids[0]
security_groups = [module.network.security_group_id]
}
| 场景 | 设计要点 |
|---|---|
| 暴露 ID | 输出 *.id,少输出整块属性 |
| 暴露集合 | 输出 list/map 便于调用方索引 |
| 敏感值 | 加 sensitive = true |
| 依赖传递 | 输出让依赖图跨模块连通 |
3.3 用 output 控制依赖方向
模块之间通过 output 传递依赖(module.app 引用 module.network.vpc_id 即自动建立跨模块依赖图)。避坑:模块间禁止直接引用对方内部资源(module.a.aws_x.y 非法),必须走 output,这是模块封装的边界。
4. locals 与内部逻辑
一句话总结: locals 是模块内部私有的「中间变量」,负责把 input 加工成内部资源可用的形态,让资源定义保持声明式可读。
locals {
name_prefix = "${var.environment}-${var.project}"
# 计算出的标签
all_tags = merge(
{ project = var.project, env = var.environment },
var.tags,
)
# 由环境推导实例规格
instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"
# 网段分段
subnet_cidrs = [
for idx, cidr in var.vpc_cidr : cidrsubnet(cidr, 8, idx)
]
}
4.1 locals 的适用边界
| 用法 | 推荐度 | 说明 |
|---|---|---|
| 派生命名/标签 | 高 | merge + 前缀 |
| 环境差异映射 | 高 | 条件表达式 |
| 网络规划 | 高 | cidrsubnet 计算 |
| 复杂业务逻辑 | 低 | 应在代码/数据中表达 |
| 副作用操作 | 禁止 | locals 必须纯函数 |
一句话:locals 把「输入 → 中间形态」的转换集中起来,资源定义只读 locals(如
name = local.name_prefix),可读性与可测试性都会更好。
5. 模块版本管理
一句话总结: 模块 source 可以是本地路径、Git、Registry;生产环境必须用带版本约束的 Registry 引用,防止「构建依赖漂移」。
5.1 source 类型
| source | 写法 | 适用 |
|---|---|---|
| 本地 | source = "../modules/network" | 同仓库内部 |
| Git | source = "git::https://github.com/org/repo.git?ref=v1.2.0" | 跨仓库 |
| Registry | source = "terraform-aws-modules/vpc/aws" | 公开/私有发布 |
| 压缩包 | source = "https://.../module.zip" | 一次性 |
5.2 版本约束
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0" # 精确锁定
}
module "s3" {
source = "registry.terraform.io/terraform-aws-modules/s3-bucket/aws"
version = ">= 4.0.0, < 5.0.0" # 范围约束
}
# 锁定模块版本到 lockfile
terraform init -upgrade
# .terraform.lock.hcl 记录已解析的确切版本与校验和
| 约束写法 | 语义 |
|---|---|
"5.0.0" | 精确版本 |
">= 4.0, < 5.0" | 范围 |
"~> 5.0" | 5.0 系列内最新 |
"!= 5.0.1" | 排除 |
一句话:版本约束 +
.terraform.lock.hcl一起提交,才能保证「昨天能 apply,今天也能 apply」的确定性。
6. 模块设计模式
一句话总结: 组合模块、抽象层、接口稳定、依赖注入是四大核心模式;接口(input/output)比内部实现更重要,接口一变就是 breaking change。
6.1 组合模块
# 上层「应用模块」组合底层「网络模块」「计算模块」
module "network" {
source = "../modules/network"
cidr = var.cidr
}
module "compute" {
source = "../modules/compute"
vpc_id = module.network.vpc_id
instance_type = var.instance_type
subnet_cidrs = module.network.subnet_ids
}
6.2 抽象层模式
# 面向调用方暴露业务语义,隐藏云厂商差异
module "web_tier" {
source = "./modules/web_tier"
region = var.region
# 调用方不感知内部用了 ALB 还是 NLB
}
output "web_endpoint" {
value = module.web_tier.endpoint
}
6.3 接口稳定性 checklist
| 原则 | 做法 |
|---|---|
| 输出最小化 | 只输出调用方需要的 |
| 输入默认化 | 常用项给合理默认值 |
| 命名稳定 | 输出名一旦发布别轻易改 |
| 语义化 | subnet_ids 优于 ids |
| 版本化 | 接口变更发新版本而非改旧版 |
6.4 反模式 过度抽象
# ❌ 反模式:一个模块塞几十个变量,谁都能传,没人知道怎么用
variable "settings" {
type = map(any) # any 让校验形同虚设
}
# ✅ 改进:细分对象类型
variable "settings" {
type = object({
replica_count = number
enable_backup = bool
retention = number
})
default = { replica_count = 1, enable_backup = true, retention = 7 }
}
7. 模块测试与避坑
一句话总结: 模块要像代码一样测试:terraform validate 做静态校验,terraform test 做运行时验证,再配合 conftest/tflint 检查策略。
7.1 测试工具链
| 工具 | 作用 |
|---|---|
terraform validate | 语法与类型校验 |
terraform plan | 检查期望 diff |
terraform test | 1.6+ 内置,模块级断言 |
tflint | 静态规则检查 |
conftest / OPA | 策略校验(IAM、标签合规) |
terratest | Go 语言集成测试 |
# Terraform 1.6+ 模块测试文件 tests/basic.tftest.hcl
run "basic" {
command = plan
variables { environment = "dev" }
assert {
condition = aws_vpc.main.cidr_block == "10.0.0.0/16"
error_message = "VPC CIDR 不正确"
}
}
7.2 常见避坑清单
| 坑 | 现象 | 对策 |
|---|---|---|
| 模块内硬编码环境 | 换个环境没法复用 | 全部走 input |
| output 暴露敏感值 | apply 打印密钥 | sensitive = true |
| source 用绝对路径 | 换机器就找不到 | 相对路径或 Registry |
| 版本裸奔 | 模块升级引发 diff 风暴 | 锁 version |
| 模块过大 | 几百个资源难维护 | 拆成组合模块 |
| 跨模块直接引用 | 编译报错 | 必须走 output |
# 校验整个模块目录
terraform validate
terraform fmt -recursive
terraform test
一句话:模块的验收标准是「换个环境、换个人、换台机器,plan 的结果仍然一致且可预期」。
8. 总结
模块化把 Terraform 从「脚本堆砌」升级为「组件工程」:
| 环节 | 要点 |
|---|---|
| 动机 | 复用、抽象、隔离、规范 |
| input | type + default + validation 三件套 |
| output | 最小暴露 + sensitive |
| locals | 内部纯转换,资源定义只读 locals |
| 版本 | Registry + 版本约束 + lockfile |
| 模式 | 组合、抽象层、接口稳定、依赖注入 |
| 测试 | validate / plan / test / tflint / terratest |
一句话收尾:模块的接口比实现更重要,先把 input/output 契约设计稳,再谈内部资源。下一篇「Provider 生态与自定义」将讲解模块与资源背后的 Provider 体系,以及如何开发自己的 Provider。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。