2. Terraform 模块基础回顾:模块结构、输入输出变量、本地值与资源定义的最佳实践
在深入模块测试策略之前,必须统一团队对 Terraform 模块基础结构的认知。一个标准化的模块骨架是测试自动化的前提——如果模块结构混乱、变量命名随意、资源定义缺乏约束,测试将无法有效覆盖关键路径。本节从模块文件布局、输入输出契约、本地值使用规范以及资源定义原则四个维度,梳理服务目录中模块应遵循的最佳实践。
2.1 标准模块文件结构
Terraform 模块本质上是一个包含 .tf 或 .tf.json 文件的目录。服务目录中的模块应遵循以下文件组织约定,确保可读性与可测试性:
| 文件/目录 | 用途 | 是否必须 |
|---|---|---|
main.tf |
核心资源定义,包含所有被管理的云资源 | 是 |
variables.tf |
输入变量声明,定义模块的对外接口 | 是 |
outputs.tf |
输出值声明,定义模块的返回契约 | 是 |
locals.tf |
本地值定义,用于表达式复用与逻辑简化 | 推荐 |
versions.tf |
Terraform 与 Provider 版本约束 | 推荐 |
README.md |
模块说明、用法示例、注意事项 | 推荐 |
examples/ |
完整可运行的用法示例目录 | 推荐 |
tests/ |
测试用例目录(后续章节重点) | 推荐 |
实践建议: 将
variables.tf 和 outputs.tf 视为模块的“API 契约”。任何对这两个文件的修改都应触发版本号变更和回归测试。服务目录的 CI 流水线应自动检测这两个文件的变更。
2.2 输入变量设计原则
输入变量是模块与调用者之间的主要接口。设计不当的变量会导致模块难以使用、测试困难。以下是服务目录模块中变量定义的最佳实践:
- 类型约束优先:始终使用
type参数,避免any。优先使用object()对复杂参数进行结构化约束。 - 提供合理默认值:对于非必填参数,默认值应体现最常用的生产配置,而非空值。
- 使用
nullable控制空值行为:Terraform 1.x 中显式设置nullable = false可避免调用者意外传入null导致错误。 - 变量描述必须包含业务含义:描述中应说明该变量对基础设施的影响,而非仅重复变量名。
- 敏感变量标记:对密码、密钥等敏感信息设置
sensitive = true。
# variables.tf 示例:符合服务目录标准的变量定义
variable "environment" {
description = "部署环境名称,用于资源命名和标签。可选值:dev, staging, prod"
type = string
default = "dev"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "环境名称必须为 dev、staging 或 prod。"
}
}
variable "vpc_config" {
description = "VPC 网络配置,包含 CIDR 和子网信息"
type = object({
cidr_block = string
public_subnets = list(string)
private_subnets = list(string)
enable_nat_gateway = optional(bool, true)
})
default = {
cidr_block = "10.0.0.0/16"
public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnets = ["10.0.10.0/24", "10.0.20.0/24"]
enable_nat_gateway = true
}
nullable = false
}
2.3 输出值契约规范
输出值是模块对调用者的承诺。服务目录中的每个模块应遵循以下输出规范:
- 最小必要原则:只输出调用者确实需要的属性,避免输出整个资源对象。
- 统一命名风格:使用
资源类型_属性名格式,例如vpc_id、subnet_ids。 - 输出描述必须说明用途:描述中应包含该输出值在调用方如何被使用。
- 敏感输出标记:对可能泄露敏感信息的输出设置
sensitive = true。
# outputs.tf 示例:清晰、可测试的输出契约
output "vpc_id" {
description = "创建的 VPC ID,用于关联安全组和子网"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "公有子网 ID 列表,用于负载均衡器和 NAT 网关"
value = aws_subnet.public[*].id
}
output "database_endpoint" {
description = "RDS 数据库连接端点,格式:host:port"
value = aws_db_instance.main.endpoint
sensitive = true
}
2.4 本地值(locals)使用规范
本地值用于在模块内部简化表达式、避免重复计算。服务目录模块中应遵循以下规范:
| 场景 | 推荐做法 | 避免做法 |
|---|---|---|
| 资源命名 | 使用 local.name_prefix 统一拼接 |
在每个资源中重复写 "${var.project}-${var.environment}" |
| 标签合并 | 使用 local.common_tags 定义公共标签 |
在每个资源中逐个写标签键值对 |
| 条件表达式 | 将复杂条件提取为 local.enable_feature |
在资源参数中直接写三元表达式嵌套 |
| 数据转换 | 使用 local.merged_config 合并默认值与用户输入 |
在资源参数中写 merge() 调用 |
# locals.tf 示例:合理使用本地值提升可维护性
locals {
name_prefix = "${var.project}-${var.environment}"
common_tags = {
Environment = var.environment
Project = var.project
ManagedBy = "Terraform"
Module = "vpc-network"
}
# 合并用户自定义标签与公共标签,用户标签可覆盖公共标签
all_tags = merge(local.common_tags, var.extra_tags)
# 根据环境决定是否启用 NAT 网关
enable_nat = var.environment == "prod" ? true : var.vpc_config.enable_nat_gateway
}
2.5 资源定义最佳实践
资源定义是模块的核心。服务目录中的资源定义应遵循以下原则:
- 显式依赖优于隐式依赖:使用
depends_on明确声明资源间依赖关系,避免 Terraform 自动推导失败导致的并行执行问题。 - 使用
for_each而非count管理同类资源:for_each在资源增删时不会引起索引偏移,更适合服务目录场景。 - 生命周期管理:对关键资源(如数据库、存储桶)设置
prevent_destroy = true,对可替换资源设置create_before_destroy = true。 - 避免硬编码:所有环境相关的值必须通过变量或本地值传入,禁止在资源参数中直接写字符串常量。
# main.tf 示例:符合最佳实践的资源定义
resource "aws_vpc" "main" {
cidr_block = var.vpc_config.cidr_block
enable_dns_support = true
enable_dns_hostnames = true
tags = merge(local.all_tags, {
Name = "${local.name_prefix}-vpc"
})
lifecycle {
create_before_destroy = true
}
}
resource "aws_subnet" "public" {
for_each = { for idx, cidr in var.vpc_config.public_subnets : idx => cidr }
vpc_id = aws_vpc.main.id
cidr_block = each.value
availability_zone = data.aws_availability_zones.available.names[each.key]
tags = merge(local.all_tags, {
Name = "${local.name_prefix}-public-subnet-${each.key}"
Tier = "public"
})
depends_on = [aws_vpc.main]
}
常见陷阱: 在
for_each 中使用 toset() 对列表去重时,如果列表元素顺序不重要,务必在文档中说明。Terraform 的集合是无序的,可能导致每次 plan 显示资源被重建的假象。
2.6 模块结构全景图
下图展示了服务目录中一个标准 Terraform 模块的完整结构、数据流以及各文件之间的调用关系。该图可作为团队模块开发的参考模板。
2.7 模块版本化与语义化版本
服务目录中的每个模块都应遵循语义化版本控制(SemVer),版本号格式为 MAJOR.MINOR.PATCH:
| 版本位 | 变更类型 | 示例 |
|---|---|---|
| MAJOR | 不兼容的 API 变更(删除/重命名变量或输出) | 2.0.0 |
| MINOR | 向后兼容的新功能(新增可选变量、新增输出) | 1.3.0 |
| PATCH | 向后兼容的缺陷修复或内部优化 | 1.2.1 |
版本约束在
versions.tf 中的声明方式: 模块自身应声明其所需 Terraform 版本和 Provider 版本的最小值,而非固定版本。调用方通过 source 和 version 参数锁定模块版本。
# versions.tf 示例
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0.0, < 6.0.0"
}
}
}
2.8 模块测试前置条件检查清单
在进入后续章节的测试策略之前,请使用以下清单确认你的模块已满足基础质量要求:
- 模块目录包含
main.tf、variables.tf、outputs.tf、versions.tf - 所有输入变量都有
type、description和合理的default - 所有输出值都有
description,且不暴露整个资源对象 - 本地值用于命名、标签和条件表达式,避免在资源中重复硬编码
- 资源定义使用
for_each而非count(除非有明确理由) - 关键资源设置了
prevent_destroy或create_before_destroy - 模块遵循语义化版本,并在
versions.tf中声明了版本约束 - 模块包含
README.md和examples/目录