4. Terraform 模块结构:标准模块目录结构、根模块与子模块、模块注册表
Terraform 模块化设计的核心在于 可复用 与 可组合。一个设计良好的模块,必须遵循标准的目录结构,明确根模块与子模块的职责边界,并通过模块注册表实现团队间的共享与版本管理。本章将系统拆解这三个关键维度。
4.1 标准模块目录结构
HashiCorp 社区与官方最佳实践共同定义了 Terraform 模块的推荐目录布局。遵循此结构可确保模块的 可读性、可测试性 与 可发布性。
| 目录 / 文件 | 是否必须 | 用途说明 |
|---|---|---|
main.tf |
是 | 模块的主逻辑入口,定义核心资源与数据源。 |
variables.tf |
是 | 声明模块所有输入变量,包含类型、描述与默认值。 |
outputs.tf |
是 | 定义模块对外暴露的输出值,供根模块或其他子模块引用。 |
versions.tf |
推荐 | 声明 Terraform 与 Provider 的版本约束,确保兼容性。 |
README.md |
推荐 | 模块的文档说明,包含用途、用法示例、输入输出清单。 |
modules/ |
可选 | 存放嵌套子模块,用于拆分复杂逻辑或提供变体实现。 |
examples/ |
推荐 | 提供可直接运行的示例配置,展示模块的典型用法。 |
test/ |
推荐 | 存放 Terratest 或 OPA 等测试框架的测试用例。 |
关键原则: 根目录下的
main.tf、variables.tf、outputs.tf 是模块的“三件套”,缺一不可。缺少 outputs.tf 的模块将无法被外部有效消费。
以下是一个标准模块目录的完整示例:
terraform-aws-vpc/
├── main.tf
├── variables.tf
├── outputs.tf
├── versions.tf
├── README.md
├── modules/
│ ├── public_subnet/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── private_subnet/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── examples/
│ ├── complete_vpc/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ └── minimal_vpc/
│ ├── main.tf
│ └── terraform.tfvars
└── test/
├── vpc_test.go
└── fixtures/
└── test_fixture.tf
4.2 根模块与子模块
在 Terraform 工作空间中,根模块 是执行 terraform apply 的入口目录,它负责组合与编排子模块。而 子模块 是被根模块或其他子模块通过 module 块引用的独立单元。
最佳实践: 根模块应保持“薄”而“声明式”,只负责传递变量、调用子模块、定义远程状态配置。所有具体的资源创建逻辑都应下沉到子模块中。
根模块与子模块的职责对比如下:
| 维度 | 根模块 | 子模块 |
|---|---|---|
| 执行入口 | 是,包含 terraform.tfstate |
否,不直接执行 |
| 职责范围 | 编排、组合、传递上下文 | 封装单一资源或一组紧密耦合的资源 |
| 复用性 | 低,通常为项目定制 | 高,可在多个根模块间共享 |
| 版本管理 | 通常不独立版本化 | 通过 Git Tag 或注册表版本化 |
| 测试粒度 | 集成测试 | 单元测试 + 集成测试 |
以下是一个根模块调用子模块的典型代码片段:
# 根模块 main.tf
module "vpc" {
source = "./modules/vpc"
version = "1.2.0"
vpc_name = var.vpc_name
cidr_block = var.cidr_block
enable_nat_gateway = true
}
module "ecs_cluster" {
source = "terraform-aws-modules/ecs/aws"
version = "5.0.0"
cluster_name = "${var.environment}-ecs-cluster"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
}
避坑提示: 子模块内部不应直接引用根模块的变量或数据源。所有外部输入必须通过
variables.tf 显式声明,所有对外输出必须通过 outputs.tf 暴露。这保证了子模块的完全独立性与可移植性。
4.3 模块注册表
Terraform 模块注册表(Registry)是模块的 中央分发与版本管理平台。它允许团队像使用包管理器一样搜索、安装和版本化基础设施模块。
模块注册表分为两类:
- 公有注册表(Public Registry): 由 HashiCorp 官方维护,包含大量经过验证的社区模块(如
terraform-aws-modules/vpc/aws)。 - 私有注册表(Private Registry): 企业版 Terraform Cloud / Enterprise 提供的功能,或通过自建 Git 仓库 + 版本标签实现类似效果。
模块在注册表中的命名规范遵循 terraform-<PROVIDER>-<NAME> 格式:
| 命名示例 | Provider | 模块用途 |
|---|---|---|
terraform-aws-vpc |
AWS | 创建 VPC 及其子网、路由表 |
terraform-google-kubernetes |
Google Cloud | 创建 GKE 集群 |
terraform-azurerm-network |
Azure | 创建虚拟网络与子网 |
从注册表引用模块时,只需指定 source 与 version:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.1.0"
name = "my-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
enable_nat_gateway = true
enable_vpn_gateway = true
tags = {
Terraform = "true"
Environment = "dev"
}
}
版本约束最佳实践: 在
versions.tf 中为所有引用的注册表模块设置 ~> x.y.z 的版本范围,避免意外升级引入破坏性变更。例如 version = "~> 5.1.0" 表示允许补丁版本升级,但不允许次要或主要版本升级。
下图展示了从模块开发到注册表发布,再到根模块消费的完整生命周期:
4.4 模块结构决策树
在实际项目中,如何决定一个模块的拆分粒度?以下决策树可帮助架构师快速判断:
- 该资源组是否会被多个环境(dev/staging/prod)复用? → 是,则封装为子模块。
- 该资源组是否包含超过 5 个关联资源? → 是,考虑拆分为嵌套子模块。
- 该资源组是否有独立的生命周期(如独立版本迭代)? → 是,发布到注册表。
- 该资源组是否需要独立的测试覆盖? → 是,在
test/目录添加测试。 - 该资源组是否依赖外部模块的输出? → 是,通过
module块显式声明依赖。
经验法则: 一个模块的
main.tf 文件行数建议控制在 100-300 行之间。超过 300 行意味着模块职责过重,应考虑拆分为更细粒度的子模块。
通过严格遵循标准目录结构、清晰划分根模块与子模块的边界、并善用模块注册表进行版本管理,团队可以构建出高度可复用、可测试、可治理的 Terraform 基础设施代码库。这是实现 CI/CD 服务目录环境的基础设施即代码基石。