「模块化设计模式」

讲解 Terraform 模块化设计:input 类型约束与 validation、output 返回值设计、locals 内部逻辑、模块版本管理与 source 引用、组合与抽象层模式与测试。

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"同仓库内部
Gitsource = "git::https://github.com/org/repo.git?ref=v1.2.0"跨仓库
Registrysource = "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 test1.6+ 内置,模块级断言
tflint静态规则检查
conftest / OPA策略校验(IAM、标签合规)
terratestGo 语言集成测试
# 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 从「脚本堆砌」升级为「组件工程」:

环节要点
动机复用、抽象、隔离、规范
inputtype + default + validation 三件套
output最小暴露 + sensitive
locals内部纯转换,资源定义只读 locals
版本Registry + 版本约束 + lockfile
模式组合、抽象层、接口稳定、依赖注入
测试validate / plan / test / tflint / terratest

一句话收尾:模块的接口比实现更重要,先把 input/output 契约设计稳,再谈内部资源。下一篇「Provider 生态与自定义」将讲解模块与资源背后的 Provider 体系,以及如何开发自己的 Provider。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「terraform」更多文章

  1. 「漂移检测与收敛」
  2. 「资源重构与迁移」
  3. 「数据源与远程数据读取」