☰
编写第一个 Terraform Module:结构、输入输出与变量校验
2026/10/23 5:00:41 网站建设 项目流程

三套环境的 S3 配置各抄了一份,改个参数要动三个目录。有一回漏改,plan 里 dev 的 bucket 前缀跟 prod 撞了。

💡 示例基于 Terraform 1.x + AWS provider 5.x,模块放在./modules/s3_bucket,本地路径引用,不推 registry 也能跑。

1. 模块结构与调用关系

1.1 三段文件结构

一个目录就是一个模块,目录里的.tf文件全部参与求值。约定拆成三个文件,把输入、加工、输出分开。

文件职责内容
variables.tf输入variable块,声明调用方能传什么
main.tf加工resource/data/locals,真正干活的资源
outputs.tf输出output块,声明调用方能读到什么
versions.tf依赖约束terraform块 +required_providers
README.md文档参数表、调用示例,registry 会渲染

文件名本身没有语义,Terraform 只认后缀。拆三个文件是为了让人一眼找到入口和出口。

跑terraform plan的那个目录叫 root module,被它引用的目录叫 child module。child module 不能单独 apply,只能由 root 传参调用。

terraform-aws-s3-bucket/ ├── variables.tf ......... 输入:name / versioning / force_destroy / tags ├── main.tf .............. 加工:aws_s3_bucket + 版本控制 + 公共访问阻断 ├── outputs.tf ........... 输出:bucket_id / bucket_arn / bucket_domain_name ├── versions.tf .......... required_providers: aws ~> 5.0 └── README.md

1.2 调用方式与资源地址

调用方在自己的main.tf里写module块,source指向模块目录,其余参数按变量名传。

module "s3_bucket" { source = "./modules/s3_bucket" name = "logs-archive" versioning = true force_destroy = false tags = { env = "dev" } }

模块内部的资源地址会加上模块前缀,aws_s3_bucket.this变成module.s3_bucket.aws_s3_bucket.this。调用方读输出写module.s3_bucket.bucket_id,碰不到内部资源名。

调用方 main.tf ──传变量──▶ variables.tf ──┬─校验通过─▶ main.tf 资源 ─▶ outputs.tf ──输出──▶ 调用方 └─校验失败─▶ plan 报错

source有四类写法,能写version的只有 registry:

source 写法示例能否写 version
本地路径./modules/s3_bucket否
Registryhashicorp/consul/aws是,version = "~> 0.11"
Gitgit::https://example.com/vpc.git?ref=v1.2.0用?ref=锁版本
HTTP 归档https://example.com/vpc-module.zip否

本地路径适合开发期,稳定后推 Git 打 tag,用?ref=固定版本号。

2. 输入:variables.tf

2.1 四个常用属性

属性作用不写会怎样
type约束传入值的类型视为any,写错类型到 plan 才炸
default默认值,写了就变可选不写即必填,调用方漏传直接报错
description参数说明文档靠人肉维护
sensitive打码,plan 里显示(sensitive value)明文进 plan 和日志
variable "name" { type = string description = "Bucket 名称前缀,全局唯一" } variable "versioning" { type = bool description = "是否开启版本控制" default = false } variable "force_destroy" { type = bool description = "销毁时是否清空对象" default = false }

name没有default,调用方必须传。另外两个有默认值,不传就取默认。

2.2 复杂类型与可选字段

类型写法适合
基础stringnumberbool单个值
集合list(string)set(string)map(string)同类型多值
结构object({...})一组固定字段
元组tuple([string, number])位置固定、类型不同

对象里部分字段可以不填,用optional(),Terraform 1.3 起支持:

variable "lifecycle_rule" { type = object({ enabled = bool days = optional(number, 30) prefix = optional(string, "") }) default = { enabled = true } }

days和prefix没传时取括号里的默认值。调用方只写必填项,模块内部按完整结构使用。

⚠️list用位置索引,中间插一个元素会让后面整体前移;set成员无序,取值顺序不保证。要按名字引用就换成map。

2.3 变量来源与优先级

同一个变量可能从多处赋值,Terraform 按固定顺序取,后者覆盖前者:

环境变量 TF_VAR_name ▼ 覆盖 terraform.tfvars ▼ terraform.tfvars.json ▼ *.auto.tfvars / *.auto.tfvars.json(按文件名排序) ▼ -var / -var-file(命令行,写在后面的赢)
exportTF_VAR_name=from-env terraform plan-var="name=from-cli"

结果取from-cli。参数没生效时先查这条链,八成是某个auto.tfvars把人坑了。

3. 输出:outputs.tf

3.1 value 与 description

调用方只能读到output声明过的值,模块内部资源属性不声明就出不去。

output "bucket_id" { description = "Bucket 名称" value = aws_s3_bucket.this.id } output "bucket_arn" { description = "Bucket ARN,用于策略授权" value = aws_s3_bucket.this.arn }

引用写module.s3_bucket.bucket_arn。value支持表达式,拼好的字符串也能出。

属性用途
value必填,输出内容
description说明,registry 文档用
sensitive打码,向上传播
depends_on强制依赖,输出前等某资源就绪
precondition输出前校验,不满足直接报错

3.2 敏感输出

密码、密钥这类值加sensitive = true,plan 和terraform output都显示(sensitive value)。

output "secret_key" { description = "访问密钥" value = aws_iam_access_key.this.secret sensitive = true }

取真实值用terraform output -raw secret_key。sensitive会向上传播,引用敏感输出的另一个output也要标敏感,否则 plan 报错。

4. 变量校验

4.1 validation 块

variable块里加validation,condition返回 false 时 plan 直接失败,附上error_message。

variable "name" { type = string description = "Bucket 名称前缀" validation { condition = can(regex("^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$", var.name)) error_message = "name 只允许小写字母、数字、连字符,长度 3-63。" } validation { condition = !strcontains(var.name, "..") error_message = "name 不能含连续两个点。" } }

can()把可能报错的表达式包成布尔值,正则不匹配时返回 false,不会抛异常中断。一个变量可以挂多个validation,全部通过才继续。

4.2 condition 的约束

约束说明
必须返回 bool返回字符串或数字报Invalid expression value
只能用纯函数uuid()timestamp()bcrypt()每次结果不同,禁用
引用范围1.9 之前只能引用被校验的那个变量;1.9 起可引用其他输入变量
报错时机plan 阶段,早于任何云 API 调用

跨变量校验在 1.9 之前用precondition兜底:

variable "environment" { type = string } variable "enable_versioning" { type = bool default = false }

environment = "prod"且没开版本控制时,希望在 plan 阶段拦住,把校验挂到资源上:

resource "aws_s3_bucket" "this" { bucket = var.name lifecycle { precondition { condition = var.environment != "prod" || var.enable_versioning error_message = "生产环境的 bucket 必须开启版本控制。" } } }

precondition写在lifecycle里,condition能读任意变量和资源属性,比validation宽松。

4.3 常用校验写法

需求condition
非空字符串length(var.name) > 0
枚举值contains(["dev", "staging", "prod"], var.environment)
数字区间var.port >= 1 && var.port <= 65535
CIDR 格式can(cidrhost(var.vpc_cidr, 0))
前缀匹配startswith(var.name, "logs-")
列表非空length(var.subnet_ids) > 0
互斥开关!(var.public && var.private)

💡 error_message 写清期望值,别只写「参数非法」

plan 失败时调用方只能看到这一行提示,把允许范围和实际值都带上: validation { condition = contains([“dev”, “staging”, “prod”], var.environment) error_message = “environment 只能是 dev / staging / prod,当前值 ${var.environment}。” }

5. 抽取可复用模块

5.1 抽取前的状态

三套环境各有一份main.tf,S3 相关段落几乎一致,只有前缀和版本控制开关不同。

envs/ ├── dev/main.tf ....... aws_s3_bucket + versioning + 公共访问阻断 ├── staging/main.tf ... 同一段资源定义,复制一遍 └── prod/main.tf ...... 同一段资源定义,再复制一遍

改一条公共规则要动三处,漏一处 plan 就出差异。把这些段落收进一个目录,环境目录只留module块。

5.2 目录规划

. ├── modules/ │ └── s3_bucket/ │ ├── variables.tf │ ├── main.tf │ ├── outputs.tf │ └── versions.tf └── envs/ ├── dev/main.tf ├── staging/main.tf └── prod/main.tf

modules/放可复用单元,envs/放各环境入口。模块目录只留通用逻辑,环境差异通过变量传进去。

模块粒度按「能不能独立描述一件事」切。S3 模块只管 bucket 本身,需要 VPC 就让调用方把vpc_id传进来,别在模块里顺手建 VPC。

5.3 迁移步骤

  1. 建modules/s3_bucket/,把三个文件搬进去,硬编码改成variable声明
  2. 环境目录改写成module块,参数走变量
  3. 搬状态地址,避免销毁重建
  4. terraform plan确认输出No changes

状态迁移是唯一容易出事的一步。资源地址多了模块前缀,不搬就是「旧的销毁 + 新的创建」:

terraform statemv'aws_s3_bucket.this''module.s3_bucket.aws_s3_bucket.this'terraform statemv'aws_s3_bucket_versioning.this''module.s3_bucket.aws_s3_bucket_versioning.this'terraform plan
No changes. Your infrastructure matches the configuration.

plan 里出现 destroy,说明有资源地址漏搬,反向执行一次state mv就能退回去。

⚠️state mv只改状态文件里的地址,不动真实资源。执行前先terraform state list对一遍清单,挪错了在真实环境里看不出来。

5.4 模块内的 provider 声明

模块里写provider "aws" {}在 0.13 之后被弃用,provider 配置由根模块决定,模块只声明依赖版本。

terraform { required_version = ">= 1.5.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } }

~> 5.0允许 5.x 小版本升级,挡住 6.0 的不兼容变更。

6. 常见报错与排查

报错 / 现象原因处理
Unsupported argument模块没声明该变量,调用方却传了在variables.tf补variable,或删掉传参
Missing required argument变量没有default,调用方漏传传值,或给变量加default
Invalid value for variablevalidation的condition返回 false看error_message里的允许范围
Invalid expression valuecondition没返回 bool包一层can(),或补上比较运算
Reference to undeclared output读了模块没声明的输出在outputs.tf补output
Module version requirements have not been met本地路径上写了version去掉version,改用?ref=锁 tag
抽模块后 plan 要销毁重建资源地址没搬terraform state mv到新地址
输出显示(sensitive value)该输出标了sensitiveterraform output -raw xxx取原值

动手前过一遍清单:

  • 必填变量都写了description,调用方看得懂
  • 名字类变量配了validation,非法值挡在 plan 之前
  • condition里没有uuid()/timestamp()这类非纯函数
  • 输出覆盖了调用方要用的字段,敏感值标了sensitive
  • 抽完模块执行了state mv,plan 为No changes
  • 模块内没有残留provider块

7. 参考

  • Terraform 文档 · 模块结构:https://developer.hashicorp.com/terraform/language/modules/develop/structure
  • Terraform 文档 · 输入变量:https://developer.hashicorp.com/terraform/language/values/variables
  • Terraform 文档 · 输出值:https://developer.hashicorp.com/terraform/language/values/outputs
  • Terraform 文档 · 自定义校验:https://developer.hashicorp.com/terraform/language/expressions/custom-validations
  • Terraform 文档 · 模块源:https://developer.hashicorp.com/terraform/language/modules/sources

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询