模块的核心概念
模块(Module)
一组
.tf 文件的集合,封装了一组相关资源的定义和配置逻辑。每个 Terraform 配置都是模块——当前工作目录的 .tf 文件是根模块(Root Module),它调用的其他模块是子模块(Child Module)。模块是 Terraform 代码复用和组织的基本单元。根模块(Root Module)
运行 terraform init/plan/apply 命令时所在目录的 .tf 文件构成根模块。每次 Terraform 操作从根模块开始,递归处理它调用的所有子模块。根模块拥有 State 文件的所有权,子模块的资源也存储在根模块的 State 中(以
module.name.resource_type.resource_name 形式)。子模块(Child Module)
被根模块或其他子模块通过
module 块调用的模块。子模块通过 variables.tf 定义输入接口,通过 outputs.tf 定义输出接口,内部实现对调用者透明——这与面向对象编程中的类和封装概念类似。模块接口
模块的公共 API:输入变量(variables.tf 中的 variable 块)是构造函数参数,输出值(outputs.tf 中的 output 块)是公有属性,内部资源对调用者不可见(调用者不能直接引用子模块内的 resource.name,只能引用 module.name.output_name)。
标准模块目录结构
一个设计良好的模块应当包含以下文件,职责分明,便于维护和测试:
modules/vpc/
├── main.tf # 主要资源定义(aws_vpc, aws_subnet 等)
├── variables.tf # 输入变量声明(模块的"参数")
├── outputs.tf # 输出值声明(模块的"返回值")
├── versions.tf # terraform 块,声明 Provider 版本约束
├── locals.tf # 局部变量,内部计算值(可选)
├── data.tf # Data Source 声明(可选,避免 main.tf 过长)
└── README.md # 模块文档(必须!说明用途、参数、示例)
# modules/vpc/versions.tf — 声明 Provider 版本约束
# 子模块声明依赖,根模块负责安装和配置 Provider
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0" # 子模块只声明最低要求,不锁定具体版本
}
}
}
# modules/vpc/variables.tf — 输入变量(模块的参数)
variable "name" {
type = string
description = "VPC 名称前缀,用于资源命名"
# 无 default 值:调用者必须显式传入(必填参数)
}
variable "vpc_cidr" {
type = string
description = "VPC 的 CIDR 地址块"
default = "10.0.0.0/16"
# 输入验证:确保传入合法的 CIDR 格式
validation {
condition = can(cidrhost(var.vpc_cidr, 0))
error_message = "vpc_cidr 必须是合法的 CIDR 格式,如 10.0.0.0/16"
}
}
variable "public_subnet_cidrs" {
type = list(string)
description = "公有子网 CIDR 列表,数量应与 azs 相同"
}
variable "private_subnet_cidrs" {
type = list(string)
description = "私有子网 CIDR 列表"
default = [] # 默认不创建私有子网
}
variable "azs" {
type = list(string)
description = "可用区列表,数量应与子网 CIDR 数量相同"
}
variable "enable_nat_gateway" {
type = bool
description = "是否创建 NAT Gateway(私有子网出站用)"
default = false # NAT Gateway 费用较高,默认不创建
}
variable "tags" {
type = map(string)
description = "附加到所有资源的标签"
default = {}
}
# modules/vpc/main.tf — 资源定义
locals {
# 合并传入标签与模块默认标签
common_tags = merge(var.tags, {
Module = "vpc"
Name = var.name
})
}
resource "aws_vpc" "this" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(local.common_tags, { Name = "${var.name}-vpc" })
}
# 公有子网(数量由 public_subnet_cidrs 列表长度决定)
resource "aws_subnet" "public" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.this.id
cidr_block = var.public_subnet_cidrs[count.index]
availability_zone = var.azs[count.index]
map_public_ip_on_launch = true
tags = merge(local.common_tags, {
Name = "${var.name}-public-${count.index + 1}"
Type = "Public"
})
}
# 私有子网(只在 private_subnet_cidrs 非空时创建)
resource "aws_subnet" "private" {
count = length(var.private_subnet_cidrs)
vpc_id = aws_vpc.this.id
cidr_block = var.private_subnet_cidrs[count.index]
availability_zone = var.azs[count.index]
tags = merge(local.common_tags, {
Name = "${var.name}-private-${count.index + 1}"
Type = "Private"
})
}
# Internet Gateway
resource "aws_internet_gateway" "this" {
vpc_id = aws_vpc.this.id
tags = merge(local.common_tags, { Name = "${var.name}-igw" })
}
# NAT Gateway(只在 enable_nat_gateway = true 时创建)
resource "aws_eip" "nat" {
count = var.enable_nat_gateway ? 1 : 0 # 条件创建
domain = "vpc"
}
resource "aws_nat_gateway" "this" {
count = var.enable_nat_gateway ? 1 : 0
allocation_id = aws_eip.nat[0].id
subnet_id = aws_subnet.public[0].id # 放在第一个公有子网
tags = merge(local.common_tags, { Name = "${var.name}-nat" })
}
# modules/vpc/outputs.tf — 输出值(模块的"返回值")
# 调用者通过 module.vpc.output_name 引用这些值
output "vpc_id" {
value = aws_vpc.this.id
description = "VPC ID"
}
output "vpc_cidr_block" {
value = aws_vpc.this.cidr_block
description = "VPC CIDR 地址块"
}
output "public_subnet_ids" {
value = aws_subnet.public[*].id # splat 表达式,返回 ID 列表
description = "公有子网 ID 列表"
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id
description = "私有子网 ID 列表"
}
output "nat_gateway_ip" {
value = var.enable_nat_gateway ? aws_eip.nat[0].public_ip : null
description = "NAT Gateway 公网 IP(未启用时为 null)"
}
调用模块的完整语法
# environments/prod/main.tf
# ===== 调用本地模块(最常用)=====
module "vpc" {
source = "../../modules/vpc" # 相对路径,指向模块目录
# 传递模块的输入变量(必填变量必须显式传入)
name = "prod"
vpc_cidr = "10.1.0.0/16"
public_subnet_cidrs = ["10.1.1.0/24", "10.1.2.0/24"]
private_subnet_cidrs = ["10.1.11.0/24", "10.1.12.0/24"]
azs = ["us-east-1a", "us-east-1b"]
enable_nat_gateway = true
tags = {
Environment = "production"
ManagedBy = "Terraform"
}
}
# 使用模块的输出值
resource "aws_instance" "app" {
ami = data.aws_ami.amazon_linux.id
subnet_id = module.vpc.private_subnet_ids[0] # 引用模块输出
vpc_security_group_ids = [aws_security_group.app.id]
# ...
}
# ===== 调用 Terraform Registry 公共模块 =====
module "eks" {
source = "terraform-aws-modules/eks/aws" # Registry 格式:命名空间/模块名/Provider
version = "~> 20.0" # 必须指定版本!
cluster_name = "my-eks-cluster"
cluster_version = "1.29"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
}
# ===== 调用 Git 仓库中的模块 =====
module "security_baseline" {
# 格式:git::URL//子目录路径?ref=tag/branch/commit
source = "git::https://github.com/my-org/terraform-modules.git//security?ref=v2.1.0"
# 使用 SSH(内网 GitLab 等)
# source = "git::git@github.com:my-org/terraform-modules.git//security?ref=v2.1.0"
}
# ===== 调用 S3/GCS 上托管的模块(私有存储)=====
module "database" {
source = "s3::https://s3-us-east-1.amazonaws.com/my-modules/rds-module.zip"
version = "~> 1.5"
}
# 模块的元参数(Meta-arguments,适用于所有 module 块)
module "app_servers" {
source = "../../modules/ec2-cluster"
# depends_on:显式声明模块依赖(通常不需要,Terraform 自动推断)
depends_on = [module.vpc]
# count:创建多个模块实例(0.13+ 支持)
count = var.create_app_servers ? 1 : 0
}
for_each 动态创建多个模块实例
for_each 允许用一个 module 块创建多个模块实例,每个实例有独立的 State 和独立的资源集合。
locals {
# 定义多个环境的配置,key 是环境名称
environments = {
dev = {
cidr = "10.1.0.0/16"
subnet_cidrs = ["10.1.1.0/24"]
azs = ["us-east-1a"]
enable_nat_gateway = false
}
staging = {
cidr = "10.2.0.0/16"
subnet_cidrs = ["10.2.1.0/24", "10.2.2.0/24"]
azs = ["us-east-1a", "us-east-1b"]
enable_nat_gateway = false
}
prod = {
cidr = "10.3.0.0/16"
subnet_cidrs = ["10.3.1.0/24", "10.3.2.0/24"]
azs = ["us-east-1a", "us-east-1b"]
enable_nat_gateway = true
}
}
}
# 一个 module 块创建 3 套 VPC(dev, staging, prod)
module "vpc" {
for_each = local.environments # each.key = 环境名,each.value = 配置对象
source = "../../modules/vpc"
name = each.key
vpc_cidr = each.value.cidr
public_subnet_cidrs = each.value.subnet_cidrs
azs = each.value.azs
enable_nat_gateway = each.value.enable_nat_gateway
tags = { Environment = each.key }
}
# 引用格式:module.vpc["dev"].vpc_id, module.vpc["prod"].public_subnet_ids
# 在其他资源中引用各环境的输出
output "all_vpc_ids" {
# for 表达式:将 module.vpc 的 map 转换为 env => vpc_id 的 map
value = { for env, mod in module.vpc : env => mod.vpc_id }
}
# State 中的资源地址:
# module.vpc["dev"].aws_vpc.this
# module.vpc["staging"].aws_vpc.this
# module.vpc["prod"].aws_vpc.this
模块设计最佳实践
1. 变量验证(variable validation)
为模块变量添加验证逻辑,让错误的输入在 plan 阶段被早期发现,而不是等到 apply 时 API 报错:
# modules/rds/variables.tf
variable "instance_class" {
type = string
description = "RDS 实例规格,如 db.r6g.large"
validation {
# can() 在表达式报错时返回 false 而不是抛出异常
condition = can(regex("^db\\.(r|t|m)[3-9]g?\\.", var.instance_class))
error_message = "instance_class 必须是合法的 RDS 实例规格,如 db.r6g.large 或 db.t3.micro"
}
}
variable "environment" {
type = string
description = "部署环境"
validation {
# contains() 检查值是否在允许列表中
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment 必须是 dev、staging 或 prod 之一"
}
}
variable "backup_retention_days" {
type = number
description = "备份保留天数(生产建议 7 天以上)"
default = 7
validation {
# 多个条件用 && 连接
condition = var.backup_retention_days >= 1 && var.backup_retention_days <= 35
error_message = "backup_retention_days 必须在 1 到 35 之间(AWS RDS 限制)"
}
}
variable "allowed_cidr_blocks" {
type = list(string)
description = "允许访问数据库的 CIDR 列表"
validation {
# alltrue() + for 表达式验证列表中的每个元素
condition = alltrue([for cidr in var.allowed_cidr_blocks : can(cidrhost(cidr, 0))])
error_message = "allowed_cidr_blocks 中的所有条目必须是合法的 CIDR 格式"
}
}
variable "port" {
type = number
description = "数据库端口号"
default = 5432
validation {
condition = var.port > 1024 && var.port < 65535
error_message = "port 必须在 1025 到 65534 之间(避免系统保留端口)"
}
}
2. 灵活的可选变量设计
# 使用 object 类型变量传递结构化配置
variable "scaling_config" {
type = object({
min_size = number
max_size = number
desired_capacity = optional(number) # optional() 让字段可选(Terraform 1.3+)
})
default = {
min_size = 1
max_size = 3
desired_capacity = null # null 表示使用 AWS 默认值
}
}
# 使用 optional() 默认值(Terraform 1.3+)
variable "alb_config" {
type = object({
idle_timeout = optional(number, 60) # 第二个参数为默认值
enable_deletion_protection = optional(bool, false)
access_logs_bucket = optional(string) # 无默认值,不传时为 null
})
default = {} # 空对象,所有字段使用 optional 默认值
}
3. .terraform.lock.hcl — Provider 锁文件
.terraform.lock.hcl 是 Terraform 的 Provider 锁文件(类似 npm 的 package-lock.json),记录所有 Provider 的精确版本和哈希校验值,确保团队所有成员和 CI/CD 使用完全相同的 Provider 版本。
# .terraform.lock.hcl(自动生成,应提交到 Git)
provider "registry.terraform.io/hashicorp/aws" {
version = "5.50.0" # 精确版本
constraints = "~> 5.0" # 来源于 required_providers 的约束
hashes = [
# 不同操作系统和架构对应的二进制文件哈希
"h1:xxxx...", # h1 哈希:目录中文件内容的哈希
"zh:xxxx...", # zh 哈希:zip 包的哈希
]
}
# 更新 Provider 到满足约束的最新版本
terraform init -upgrade
# 同时生成多平台的哈希(开发用 Mac ARM64,CI 用 Linux AMD64)
# 避免在不同平台上 init 时报哈希不匹配错误
terraform providers lock \
-platform=linux_amd64 \
-platform=linux_arm64 \
-platform=darwin_arm64 \
-platform=darwin_amd64
# 验证 Provider 完整性(防止供应链攻击)
terraform providers mirror /tmp/mirror-dir # 本地缓存所有 Provider
4. 常见模块设计模式
# 模式1:条件创建资源(count 或 for_each + 空集合)
variable "create_bastion" {
type = bool
default = false
}
resource "aws_instance" "bastion" {
count = var.create_bastion ? 1 : 0 # 0 表示不创建,1 表示创建
# ...
}
output "bastion_ip" {
# 当 count = 0 时,aws_instance.bastion 为空列表,用 try 避免索引越界
value = try(aws_instance.bastion[0].public_ip, null)
}
# 模式2:动态块(dynamic block)——可选的嵌套配置
variable "ingress_rules" {
type = list(object({
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
}))
default = []
}
resource "aws_security_group" "this" {
name = "${var.name}-sg"
vpc_id = var.vpc_id
# dynamic 块根据变量动态生成多个同类嵌套块
dynamic "ingress" {
for_each = var.ingress_rules # 遍历规则列表
content { # content 块定义每个 ingress 块的内容
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
}
# 模式3:模块版本控制 — 使用 Git Tag 固定版本
module "network" {
# ?ref=v1.2.3 锁定到特定 Git Tag(语义化版本标签)
# 每次 terraform init 都会检出相同的代码,保证可重现性
source = "git::https://github.com/my-org/modules.git//vpc?ref=v1.2.3"
}
模块来源类型速查
本地路径(Local Path)
source = "../../modules/vpc":同一 repo 内的私有模块,路径相对于调用模块的目录。不支持 version 参数,代码修改立即生效(无需 init)。适合单仓库(monorepo)架构。Terraform Registry(公共)
source = "terraform-aws-modules/eks/aws" + version = "~> 20.0"。地址格式:命名空间/模块名/Provider。使用前检查模块的 Stars 数、最近更新时间和 README 质量。知名模块如 terraform-aws-modules 系列经过大量生产验证。Git 仓库
source = "git::https://github.com/org/repo.git//path?ref=v2.0.0":私有模块仓库。//path 指定子目录(双斜杠语法),ref 指定 tag/branch/commit 锁定版本。SSH 认证用 git::git@github.com。私有 Registry(Terraform Cloud/HCP)
Terraform Cloud 提供私有模块 Registry。格式类似公共 Registry,但需要 Terraform Cloud 账号认证。适合大型企业,支持版本管理、访问控制和模块文档生成。
HTTP/S3/GCS URL
直接从 URL 下载 zip 包:
source = "https://example.com/vpc-module-1.0.0.zip" 或 source = "s3::https://s3.amazonaws.com/bucket/vpc.zip"。适合私有存储的预打包模块。模块测试
Terraform 1.6+ 引入了原生测试框架(terraform test),可以编写 HCL 测试用例验证模块行为:
# modules/vpc/tests/basic.tftest.hcl
# 创建临时测试资源(运行后自动销毁)
run "creates_vpc_with_correct_cidr" {
# command = apply(默认,实际创建资源)或 plan(只验证计划)
command = plan
variables {
name = "test"
vpc_cidr = "10.0.0.0/16"
public_subnet_cidrs = ["10.0.1.0/24"]
azs = ["us-east-1a"]
}
# 断言验证期望行为
assert {
condition = aws_vpc.this.cidr_block == "10.0.0.0/16"
error_message = "VPC CIDR 应为 10.0.0.0/16"
}
assert {
condition = aws_vpc.this.enable_dns_hostnames == true
error_message = "必须启用 DNS hostnames"
}
assert {
condition = length(aws_subnet.public) == 1
error_message = "应创建 1 个公有子网"
}
}
# 运行测试:
# cd modules/vpc && terraform test
本章小结
本章核心要点
- 每个目录都是模块:根模块(工作目录)调用子模块;子模块通过 variables.tf 定义输入接口,outputs.tf 定义输出接口,调用者只能引用 outputs,不能直接引用子模块内的资源。
- 模块调用格式:
module "name" { source = "..." + 变量赋值 };输出引用格式module.name.output_name;State 中资源地址module.name.resource_type.resource_name。 - 版本约束必须指定:外部模块(Registry/Git/URL)必须固定版本;
.terraform.lock.hcl记录精确版本和哈希,应提交到 Git。子模块 versions.tf 只声明最低要求,不锁定具体版本。 - variable validation:用
condition + error_message验证输入,支持 regex、contains、alltrue 等函数;让非法输入在 plan 阶段早期报错。 - for_each 创建多模块实例:
module "m" { for_each = map },引用格式module.m["key"].output,适合多环境、多区域部署;State 中每个实例独立管理。 - dynamic 块:根据变量动态生成嵌套块(如多个 ingress 规则),比手动重复书写更灵活,适合规则数量不固定的场景。
- Terraform 1.6+ 原生测试框架:
.tftest.hcl文件编写测试用例,terraform test执行;支持 plan 模式(不创建资源)和 apply 模式(创建后验证)。