模块化
本章目标:把「造一个 VPC / 一套 EKS」这类重复逻辑抽成 module,定义清晰的输入/输出,按版本复用,再用 root module 把它们组合起来。模块是 Terraform 复用与协作的基石。
完成后你应能写出一个可被多环境调用的本地模块,并理解注册表模块的版本引用。
1. 什么是 module
任何包含 .tf 文件的目录都是一个 module。你写 main.tf 的那个目录叫 root module;用 module 块调用别的目录,就叫调用子模块:
module "vpc" {
source = "../../modules/vpc"
cidr_block = "10.10.0.0/16"
env = "staging"
}
source 可以是:
| source 形式 | 示例 |
|---|---|
| 本地路径 | "./modules/vpc"、"../../modules/vpc" |
| Git 仓库 | "git::https://github.com/org/modules.git//vpc?ref=v1.2.0" |
| 公开注册表 | "terraform-aws-modules/vpc/aws" |
| 私有注册表 | "app.terraform.io/org/vpc/aws" |
社区有成熟的 terraform-aws-modules/* 等高质量模块。生产项目优先评估复用,只在有定制需求时才自研。但对外源模块要做版本锁定与供应链审查(见第 9 章)。
2. 一个规范的模块长什么样
modules/vpc/
├── main.tf # 资源定义
├── variables.tf # 输入接口(必填项、默认值、校验)
├── outputs.tf # 输出接口(暴露给调用方的值)
├── versions.tf # terraform / provider 约束
└── README.md # 用法、输入输出说明、示例
2.1 输入:variables.tf
variable "cidr_block" {
description = "VPC 网段"
type = string
}
variable "env" {
description = "环境名,用于打 tag"
type = string
}
variable "enable_nat" {
description = "是否创建 NAT 网关"
type = bool
default = false
}
2.2 实现:main.tf
resource "aws_vpc" "this" {
cidr_block = var.cidr_block
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "${var.env}-vpc"
Environment = var.env
ManagedBy = "terraform"
}
}
2.3 输出:outputs.tf
output "vpc_id" {
description = "VPC ID,供其他模块引用"
value = aws_vpc.this.id
}
output "cidr_block" {
description = "VPC 网段"
value = aws_vpc.this.cidr_block
}
调用方就能用 module.vpc.vpc_id 引用。
3. 版本化:让模块可追溯、可回滚
引用外部模块务必带版本,否则别人一改源码你就跟着炸:
module "vpc" {
source = "git::https://github.com/org/terraform-modules.git//vpc?ref=v1.2.0"
# ↑ 固定 tag/commit
cidr_block = "10.10.0.0/16"
env = "staging"
}
| 引用方式 | 风险 |
|---|---|
?ref=v1.2.0(受保护 tag) | ✅ 可复现,推荐 |
?ref=<commit SHA> | ✅ 不可变,适合高风险生产模块 |
?ref=main(分支) | ❌ 分支一变结果就变,禁止用于生产 |
| 本地路径 | 中(跟随本地仓库,但无跨团队版本概念) |
terraform init 后生成的 .terraform.lock.hcl 会锁定 provider 的版本与校验和,不会锁定 Git/注册表模块的版本。模块可复现性依赖 source 中显式的受保护 tag 或 commit SHA;两者都必须进代码评审。
4. 组合:root module 拼装子模块
root module 不直接堆几百行资源,而是调用一组模块,像搭积木:
# envs/prod/main.tf
module "vpc" {
source = "../../modules/vpc"
cidr_block = "10.20.0.0/16"
env = "prod"
}
module "eks" {
source = "../../modules/eks"
env = "prod"
vpc_id = module.vpc.vpc_id # 把 vpc 的输出喂给 eks
subnet_ids = module.vpc.private_subnet_ids
}
依赖关系由 module.eks.vpc_id = module.vpc.vpc_id 这类引用自动建立,无需手动 depends_on。
5. 给模块加 meta-argument
模块块也支持 count / for_each,用于「按环境批量生成同类模块」:
variable "regions" {
type = map(string)
default = {
"ap-guangzhou" = "10.10.0.0/16"
"na-ashburn" = "10.20.0.0/16"
}
}
module "vpc" {
for_each = var.regions
source = "../../modules/vpc"
cidr_block = each.value
env = "prod-${each.key}"
}
# 引用:module.vpc["ap-guangzhou"].vpc_id
6. 好的模块设计
- 输入/输出即接口:模块内部怎么实现可以改,只要输入/输出契约不变,调用方无感。
- 别在模块里写死账号/地域:这些应通过变量传入,让模块环境无关。
- 合理默认值:常用参数给
default,减少调用方负担;但必填项故意不设 default,强制调用方显式传。 - 描述与校验齐全:
description+validation让使用者少踩坑。 - 单一职责:一个模块管一类资源(VPC 归 VPC,EKS 归 EKS),别把整个平台塞进一个模块。
过度抽象(为了「未来可能的复用」造出 200 个开关的万能模块)反而难维护。先写具体,复用需求出现 2~3 次再抽模块,更稳。
7. 测试模块
- 静态:
terraform validate(模块内)+ 调用方plan看差异是否合理。 - 集成:在隔离的测试账号 / 临时目录
apply一套最小组合,确认能起来、能销毁。 - 自动化:Terraform 1.6+ 支持
terraform test(写.tftest.hcl断言),可接进 CI 跑模块回归。
8. 练习与验收
练习 A
- 在
modules/vpc/建好main.tf/variables.tf/outputs.tf/versions.tf - 暴露
vpc_id与cidr_block两个输出 - 在
envs/staging用module调用它,plan通过
练习 B
- 给模块加一个
enable_nat(bool,默认 false)开关,在main.tf里用count条件创建 NAT 网关 - 把模块推到 Git,改用
?ref=v0.1.0引用,体会版本锁定
验收清单
- 模块目录结构规范(main/variables/outputs/versions)
- 输入有默认值与校验,输出暴露关键值
- 能用本地路径与 Git
?ref两种方式引用模块 - root module 用模块输出拼装依赖
- 提交
.terraform.lock.hcl保证依赖一致
下一章:变更流程 —— 用 plan / apply 评审变更,并把 plan 接进 CI 做门禁。