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

发布时间:2026/10/23 5:01:01
编写第一个 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_providersREADME.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.md1.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的只有 registrysource 写法示例能否写 version本地路径./modules/s3_bucket否Registryhashicorp/consul/aws是version ~ 0.11Gitgit::https://example.com/vpc.git?refv1.2.0用?ref锁版本HTTP 归档https://example.com/vpc-module.zip否本地路径适合开发期稳定后推 Git 打 tag用?ref固定版本号。2. 输入variables.tf2.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_namefrom-env terraform plan-varnamefrom-cli结果取from-cli。参数没生效时先查这条链八成是某个auto.tfvars把人坑了。3. 输出outputs.tf3.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 trueplan 和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块里加validationcondition返回 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 65535CIDR 格式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.tfS3 相关段落几乎一致只有前缀和版本控制开关不同。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.tfmodules/放可复用单元envs/放各环境入口。模块目录只留通用逻辑环境差异通过变量传进去。模块粒度按「能不能独立描述一件事」切。S3 模块只管 bucket 本身需要 VPC 就让调用方把vpc_id传进来别在模块里顺手建 VPC。5.3 迁移步骤建modules/s3_bucket/把三个文件搬进去硬编码改成variable声明环境目录改写成module块参数走变量搬状态地址避免销毁重建terraform plan确认输出No changes状态迁移是唯一容易出事的一步。资源地址多了模块前缀不搬就是「旧的销毁 新的创建」terraform statemvaws_s3_bucket.thismodule.s3_bucket.aws_s3_bucket.thisterraform statemvaws_s3_bucket_versioning.thismodule.s3_bucket.aws_s3_bucket_versioning.thisterraform planNo 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调用方漏传传值或给变量加defaultInvalid value for variablevalidation的condition返回 false看error_message里的允许范围Invalid expression valuecondition没返回 bool包一层can()或补上比较运算Reference to undeclared output读了模块没声明的输出在outputs.tf补outputModule 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 mvplan 为No changes模块内没有残留provider块7. 参考Terraform 文档 · 模块结构https://developer.hashicorp.com/terraform/language/modules/develop/structureTerraform 文档 · 输入变量https://developer.hashicorp.com/terraform/language/values/variablesTerraform 文档 · 输出值https://developer.hashicorp.com/terraform/language/values/outputsTerraform 文档 · 自定义校验https://developer.hashicorp.com/terraform/language/expressions/custom-validationsTerraform 文档 · 模块源https://developer.hashicorp.com/terraform/language/modules/sources