云计算百科
云计算领域专业知识百科平台

编写第一个 Terraform Module:结构、输入输出与变量校验

三套环境的 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 否
Registry hashicorp/consul/aws 是,version = "~> 0.11"
Git git::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 复杂类型与可选字段

类型写法适合
基础 string number bool 单个值
集合 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(命令行,写在后面的赢)

export TF_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 迁移步骤

  • 建 modules/s3_bucket/,把三个文件搬进去,硬编码改成 variable 声明
  • 环境目录改写成 module 块,参数走变量
  • 搬状态地址,避免销毁重建
  • terraform plan 确认输出 No changes
  • 状态迁移是唯一容易出事的一步。资源地址多了模块前缀,不搬就是「旧的销毁 + 新的创建」:

    terraform state mv 'aws_s3_bucket.this' 'module.s3_bucket.aws_s3_bucket.this'
    terraform state mv '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 variable validation 的 condition 返回 false 看 error_message 里的允许范围
    Invalid expression value condition 没返回 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) 该输出标了 sensitive terraform 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
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 编写第一个 Terraform Module:结构、输入输出与变量校验
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!