大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
Modules 模块复用与 Registry
这一课你将能做到
- 把一组相关资源封装成模块,通过输入变量和输出形成清晰边界
- 在 root 配置中调用本地模块和 Registry 模块,并正确锁定模块版本
- 给模块传入多个 provider 实例,说出模块设计中「抽象粒度」的取舍原则
为什么要学
学到这里你会发现:VPC、安全组、ECS 服务、RDS 这些资源组合在 dev/staging/prod 中反复出现,配置文件也越来越长。复制粘贴三份「网络配置」的问题在于——修一个安全组坑要改三个地方,而且总会漏。模块就是 Terraform 的「函数 / 组件」:把一组资源和它们的逻辑打包命名,调用方只传几个参数。
模块还有一个初学者常忽略的价值:它是表达「架构边界」的工具。本站代码里 network、database、application、observability 四个模块,对应架构的四个层,新人打开目录就知道系统长什么样。会用模块,Terraform 配置才从「一堆文件」升级为「可维护的系统」。
先建立图像
调用方 main.tf
┌───────────────────────────────────────────┐
│ module "web" { │
│ source = "./modules/web" ← 入口 │
│ env = "dev" ← 入参 │
│ } │
│ module.web.endpoint ← 返回值 │
└───────────┬───────────────────────────────┘
▼
modules/web/(模块内部,调用方看不见)
variables.tf main.tf outputs.tf
模块与 root 的唯一区别:模块是被调用方,命令、语法完全相同;它还能嵌套调用别的模块。
跟着做
步骤 1:搭建模块目录并编写内部资源
新建目录结构:
modules-demo/
main.tf
modules/
web/
variables.tf
main.tf
outputs.tf
modules/web/variables.tf:
variable "env" {
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.env)
error_message = "env 必须是 dev/staging/prod。"
}
}
variable "instance_type" {
type = string
default = "t3.micro"
}
variable "vpc_id" {
type = string
}
variable "subnet_ids" {
type = list(string)
}
modules/web/main.tf:
resource "aws_security_group" "web" {
name = "web-${var.env}"
description = "CodeRoadMap Web 层"
vpc_id = var.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_lb" "web" {
name = "web-${var.env}"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.web.id]
subnets = var.subnet_ids
}
modules/web/outputs.tf:
output "alb_dns" {
description = "ALB 的 DNS 名"
value = aws_lb.web.dns_name
}
output "sg_id" {
value = aws_security_group.web.id
}
模块边界就三条:variables 是入参、outputs 是返回、内部资源地址对外不可见(你无法在 root 里写 module.web.aws_lb.web.id,只能通过 output 暴露)。
步骤 2:在 root 调用本地模块
根目录 main.tf:
terraform {
required_version = ">= 1.7.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}
provider "aws" { region = "ap-northeast-1" }
module "web" {
source = "./modules/web"
env = "dev"
instance_type = "t3.micro"
vpc_id = aws_vpc.main.id
subnet_ids = aws_subnet.main[*].id
}
output "web_url" {
value = "http://${module.web.alb_dns}"
}
执行:
terraform init # 除了下载 provider,还会安装模块(terraform get)
terraform plan
state 中模块内资源的地址是带前缀的,例如 module.web.aws_lb.web。两个不同模块块(module "web_dev"、module "web_prod")各自独立,互不冲突。
步骤 3:调用 Registry 模块并锁定版本
不必什么轮子都自己造。VPC 这种通用组件用官方维护的 AWS VPC 模块:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.8.1" # Registry 模块必须显式锁版本
name = "coderoadmap-vpc"
cidr = "10.0.0.0/16"
azs = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
public_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
private_subnets = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"]
enable_nat_gateway = true
single_nat_gateway = true # dev 省钱,prod 应每 AZ 一个
tags = { Project = "coderoadmap" }
}
source 简写 terraform-aws-modules/vpc/aws 等价于 Registry 完整地址。本地路径模块不能写 version,Registry/Git 模块必须写;模块版本同样被 lock 文件记录,升级改版本号后重新 init。也可以直接引用 Git:
source = "git::https://github.com/yourorg/tf-modules.git//modules/web?ref=v1.2.0"
ref 一定要打 tag 固定,不要跟分支漂移。
步骤 4:向模块传入多个 provider
模块默认继承默认 provider;用到别名 provider(第 3 课的 us-east-1)时,调用必须显式传递:
module "cdn" {
source = "./modules/cdn"
providers = {
aws = aws
aws.virginia = aws.virginia
}
}
模块内声明 configuration_aliases:
# modules/cdn/versions.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
configuration_aliases = [aws.virginia]
}
}
}
这是初学者最容易漏的一环:不传 providers,模块内引用别名的资源会报 Provider configuration not present。
步骤 5:模块组合(composition)而不是巨型模块
本站推荐的组织方式——每个层一个薄模块,root 负责拼装:
main.tf
├─ module "network" → 输出 vpc_id / subnet_ids
├─ module "database" → 接收 vpc_id / subnet_ids,输出 db endpoint
├─ module "application" → 接收网络与数据库信息
└─ module "observability"→ 接收各层 ARN,建告警和仪表盘
信息单向流动:module "database" { subnet_ids = module.network.private_subnet_ids }。反模式是做一个「什么都能配」的万能模块,几十个变量互相耦合,调用比手写还难。判断标准:模块内资源是否同生命周期、同职责;是就封装,不是就拆开。
讲清楚要点
- 模块名即地址前缀:资源地址
module.<块名>.资源类型.本地名,同一模块被多次调用靠块名区分。 - 显式优于隐式:入参必走 variables、返回必走 outputs、别名 provider 必走 providers,模块内部不暴露给调用方。
- 版本固定是模块可复用的前提:Registry 模块写精确版本,Git 模块打 tag,跟着分支走等于每天引入未知变更。
- 先有三处重复再抽象:过早封装会把「只出现一次的偶然组合」固化成接口,改起来处处受限。
- 薄组合层 + 厚能力模块:root/environment 层只做参数注入和拼装,业务规则沉到模块里,跨环境复用的是模块而不是复制目录。
你可能会懵的地方
-
改了本地模块代码要不要重新 init? 本地路径模块改内容不需要(Terraform 每次直接读目录);只有新增/删除模块块、改 source 才需要重新 init。
-
module.web为什么有时报错「不存在」? 拼写或 init 未完成时引用不到;另外 module 块和资源一样,有 count/for_each 时地址是module.web[0]、module.web["prod"]。 -
模块能不能用自己的 backend? 不能。只有 root module 配置 backend;所有模块共享调用链顶层的那一个 state。
-
Registry 模块安全吗?会不会有后门? Verified 模块由 HashiCorp 校验发布者,但代码仍要自己评审;公司可建私有 registry(含 HCP/GitLab/GitHub 方案)统一白名单,CI 再叠加安全扫描(第 12 课)。
-
模块版本升级后 state 会乱吗? state 跟着资源地址走,只要新版本没有重命名/删除资源地址,升级安全;升级前看模块 CHANGELOG,先在 dev 验证 plan。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| Registry 模块不写 version | init 报错或拉到不可控版本 | 显式固定版本并入库锁文件 |
| Git source 不写 ref | 配置随分支漂移 | ?ref=vX.Y.Z 打 tag |
| 在 root 直接引用模块内部资源 | 越界,编译报错 | 模块通过 output 暴露所需属性 |
| 一个 mega-module 管全站 | 变量爆炸、无法独立变更 | 按层拆模块,root 组合 |
| 模块用别名 provider 却不传 providers | plan 报 provider 缺失 | providers 映射 + configuration_aliases |
卡点急救
Module not installed:新增 module 块后没跑 init,执行terraform init(或terraform get)即可。- 模块内报变量「not declared」:调用方传了模块 variables 里没有的参数(拼写或版本不对);对照模块版本的 variables.tf 检查参数名。
- 调用同一模块两次出现资源名冲突:模块内资源写死了 name(如
name = "web"),让所有 name 类参数都基于var.env或由调用方传入,保证多次实例化不撞名。
面试常问
-
Terraform 模块有什么好处?什么时候不该拆模块? 复用资源组合、封装实现细节、表达架构边界、独立版本演进;当组合只出现一次、内部资源生命周期不一致或调用参数比资源还多时,不该硬拆,过早抽象会制造耦合。
-
怎么安全地复用和升级第三方模块? 优先 Verified/私有白名单模块,固定精确版本(或 Git tag),锁文件入库;升级走 CHANGELOG 评审 → dev plan → 灰度环境验证 → prod,模块本质是别人的代码,按依赖管理对待。
-
模块怎么使用多个 provider / 跨区域? root 声明别名 provider,调用模块时通过
providers映射传入,模块侧用configuration_aliases声明接收,资源内用 provider 元参数选择。
自测三问
- 模块边界靠哪三种结构维持?为什么调用方不能直接访问模块内资源?
- 本站四层模块之间信息如何流动?为什么不让 application 模块直接建 VPC?
- 引用 Git 模块时
ref跟分支和跟 tag,半年后行为分别可能是什么?
小结一句话
模块是 Terraform 的组件单元:用 variables/outputs/providers 划定边界、用版本和 tag 固定依赖、用薄 root 组合厚模块,让架构在所有环境中复用同一套实现。
延伸
- Module Blocks 官方文档
- Terraform Registry 使用指南
- Module Composition 组合模式
- 下一课:Provisioners——local-exec/remote-exec 的工作方式、触发时机,以及为什么官方建议尽量不用它。