CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Terraform 基础设施即代码/课时/Module 复用
RawBlame
课时目录12 节

Terraform 基础设施即代码

  1. 01Terraform 是什么
  2. 02HCL 语法
  3. 03init 与 Provider
  4. 04plan 与 apply
  5. 05State 管理
  6. 06变量与输出
  7. 07Module 复用
  8. 08AWS 资源示例
  9. 09Terraform 与 K8s
  10. 10CI 中的 Terraform
  11. 11Terraform vs Helm
  12. 12综合练习

Module 复用

模块结构、source、版本。

35 分钟

Module 模块复用

这一课你将能做到

  1. 说清楚模块解决什么问题,识别根模块、子模块和注册表模块
  2. 自己写一个带变量和输出的本地模块,并在根模块中调用
  3. 正确使用官方 AWS VPC 模块,知道模块版本、源码来源和重构时的 state 迁移

为什么要学

当你的 .tf 文件超过几百行,VPC、EC2、RDS 的配置全挤在一起,改一处要翻半天。多个项目都要一套「标准网络」,复制粘贴很快失控——和代码里复制工具类一样。模块就是 Terraform 的「函数/类」:把一组资源打包,定义输入(variables)和输出(outputs),对外只暴露接口。团队会沉淀自己的模块库,AWS 官方和社区模块(如 terraform-aws-modules)是日常开发的重要基石,必须会用、会写、会选。

人话:模块像宜家的成品柜——你不用从木头开始打,告诉它「要几层、什么颜色」(输入变量),它给你柜子并告诉你抽屉尺寸(输出)。整个基础设施就是这些柜子组合起来的。

先建立图像

模块的三种来源和层级:

根模块(root module):你执行命令的目录
  └─ 子模块:module 块调用
        ├─ 本地路径:./modules/network
        ├─ Git 地址:git::https://github.com/org/tf-modules//network
        └─ 注册表:  terraform-aws-modules/vpc/aws(含版本号)

调用形式:

module "network" {
  source = "./modules/network"   # 必填
  # 其余都是传给子模块的变量
  cidr = "10.0.0.0/16"
}
# 引用子模块输出:module.network.vpc_id

跟着做

步骤 1:写一个本地安全组模块

新建目录结构:

modules-demo/
├── main.tf
└── modules/
    └── sg/
        ├── main.tf
        ├── variables.tf
        └── outputs.tf

modules/sg/variables.tf:

variable "name" {
  type = string
}

variable "vpc_id" {
  type = string
}

variable "ingress_ports" {
  type = list(number)
}

modules/sg/main.tf:

resource "aws_security_group" "app" {
  name        = var.name
  description = "由模块创建:${var.name}"
  vpc_id      = var.vpc_id

  dynamic "ingress" {
    for_each = var.ingress_ports
    content {
      from_port   = ingress.value
      to_port     = ingress.value
      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"]
  }
}

dynamic 块按端口列表动态生成多个 ingress 规则,这是模块里处理「数量不固定」的标准手法。

modules/sg/outputs.tf:

output "sg_id" {
  value = aws_security_group.app.id
}

步骤 2:在根模块调用

main.tf:

terraform {
  required_version = ">= 1.7.0"
}

provider "aws" {
  region = "ap-northeast-1"
}

module "app_sg" {
  source        = "./modules/sg"
  name          = "springboot-sg"
  vpc_id        = "vpc-0123456789abcdef0"
  ingress_ports = [22, 8080]
}

output "security_group_id" {
  value = module.app_sg.sg_id
}

执行:

terraform init
terraform plan

init 会发现并注册本地模块;plan 中资源地址带模块前缀,形如 module.app_sg.aws_security_group.app。

步骤 3:使用官方 VPC 注册表模块

把 main.tf 里的手工网络换成官方模块(真实项目推荐做法):

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.8.1"

  name = "springboot-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["ap-northeast-1a", "ap-northeast-1c"]
  public_subnets  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnets = ["10.0.11.0/24", "10.0.12.0/24"]

  enable_nat_gateway = true
  single_nat_gateway = true   # 演示省钱:一个 NAT;生产建议每 AZ 一个

  tags = {
    Project = "springboot-demo"
    Env     = "dev"
  }
}

module "app_sg" {
  source        = "./modules/sg"
  name          = "springboot-sg"
  vpc_id        = module.vpc.vpc_id
  ingress_ports = [22, 8080]
}

output "vpc_id" {
  value = module.vpc.vpc_id
}

注册表模块首次使用必须重新 terraform init 以下载。模块页面(registry.terraform.io)会列出全部输入变量和输出,通常几十个,只覆盖需要改的,其余走默认值。

步骤 4:模块也能传 provider

一个根模块里让子模块用别的 region/provider:

module "app_sg" {
  source    = "./modules/sg"
  providers = {
    aws = aws.singapore
  }
  # ...其余变量
}

模块内默认所有资源走传入的 provider。跨账号部署常用此模式。

步骤 5:把资源重构进模块时迁移 state

假设原来 SG 是根模块直接写的 aws_security_group.old,现在要搬进 module.app_sg。直接改代码 plan 会显示「删旧建新」。正确姿势:

terraform state mv aws_security_group.old module.app_sg.aws_security_group.app

先改好模块代码并 init,再 mv,最后 plan 确认无差异。模块重命名同理:terraform state mv module.old module.new。

步骤 6:模块的组合建议

常见的项目分层:

infra/
├── main.tf              # 只做「组装」,几乎不写资源
├── modules/
│   ├── network/         # VPC、子网、路由
│   ├── compute/         # EC2/ASG + SG
│   └── database/        # RDS + 参数组 + 子网组
└── envs/{dev,prod}.tfvars

根模块像 main 方法:声明 provider、调用三个模块、用模块输出把它们「接线」(如 compute 模块接收 network 模块的子网 ID)。

讲清楚要点

  1. source 是唯一必填参数:本地模块改完代码即时生效;注册表/Git 模块 init 后缓存到 .terraform/modules,升级版本要改引用并重新 init。
  2. 模块接口 = variables + outputs:模块内资源对外不可见,想被外部拿到必须 output;外部只能通过变量影响内部。
  3. 注册表模块必须钉版本:version = "5.8.1" 或约束表达式,不写版本会报错;升级走版本号 + plan 验证。
  4. 模块不是越多越好:只复用一两次的配置不必抽模块,过度分层会让跳转和调试成本变高,三复用再抽象是常见经验线。
  5. 优先官方认证模块:terraform-aws-modules 维护活跃、边界考虑周全;小众第三方模块要看源码、issues 和更新频率。

你可能会懵的地方

  • 模块和 .tf 拆文件有什么区别? 同一目录下拆成多个 .tf 文件只是物理分文件,资源地址没有前缀,互相直接可见;模块是独立命名空间,有明确的输入输出边界和独立 state 地址。

  • 改了子模块代码要重新 init 吗? 本地路径模块不用,代码即时生效;注册表/Git 源模块改不了(是缓存副本),要换版本或源地址才需 init。

  • module has not been initialized 怎么处理? 新增了 module 块还没 init。执行 terraform init(或 terraform get)即可。

  • 模块输出拿不到,报不存在? 检查子模块 outputs.tf 是否定义了该输出,以及引用拼写;output 的值若依赖有 count 条件为 0 的资源,会报错,需要在模块内处理可选输出。

  • 能在模块之间直接互相引用吗? 只能通过根模块中转:A 模块 output,根模块把值传给 B 模块的变量。模块之间不允许直接引用,保持组装关系清晰。

错法对照

错法问题正解
注册表模块不写 versioninit 报错/不可重复钉版本号
模块内部写死环境名、CIDR无法复用全部通过 variable 暴露
重构进模块不做 state mv资源被销毁重建改代码 + init + state mv
在子模块里声明 provider 配置与根配置耦合、难复用模块只声明 required_providers,配置由根传入
直接修改 .terraform/modules 里的缓存代码下次 init 被覆盖维护源仓库或 fork 后改 source

卡点急救

  1. Unsupported argument(模块不认识某参数):子模块 variables.tf 没声明该变量,或版本不对。核对模块版本的输入变量列表。
  2. 模块下载失败(Git/registry):检查源地址写法(双斜杠 // 分隔目录)、凭证和网络;公司 Git 可用 .netrc 或 SSH key。
  3. plan 显示整组模块资源重建:多半改了模块 source、version 或 for_each 的 key,先 terraform state list 查地址,用 state mv 对齐。

面试常问

  • 为什么要使用 Terraform 模块? 封装一组高内聚资源,提供统一接口和输出,实现复用、标准化命名与安全基线;降低根模块复杂度,支持团队沉淀模块资产和版本化演进。

  • 调用模块时 source 有哪些形式? 本地路径(./modules/x);注册表地址(命名空间/模块/云 + version);Git URL(git::https/ssh,可带 ref 分支标签);还支持 GCS、S3 等归档源。

  • 把已有资源重构进模块,怎么避免重建? 先完成模块代码和 init,用 terraform state mv 旧地址 module.名.内部地址 迁移映射,plan 确认无差异后提交。

自测三问

  1. 模块的输入和输出分别用什么块定义?外部为什么不能直接引用模块里的资源?
  2. 使用 terraform-aws-modules/vpc/aws 需要做对哪几件事?
  3. 模块内资源想使用根模块的非默认 provider,怎么接线?

小结一句话

模块是带明确输入输出边界的资源包:本地模块快速封装、注册表模块钉版本使用,重构移动资源记得 state mv 保命。

延伸

  • Modules 官方文档
  • Module Sources 源码形式
  • AWS VPC 官方模块
  • 下一课:AWS 实战——VPC、EC2、安全组、RDS 与 S3 落地 Spring Boot 环境。
加载本节练习…
← 上一课变量与输出下一课 →AWS 资源示例