Terraform 模块版本管理:约束语法、源类型与升级策略

上周三 prod 的 plan 里多出 14 个资源,我一行代码都没改。module.network 的 source 指向 GitHub 的 main 分支,同事合并了子网改造的 PR,terraform init 就把新代码拉了下来。

💡 示例基于 Terraform 1.5.7 + AWS provider 5.x,模块仓库放 GitHub,私有 Registry 用 Terraform Cloud。

1. 模块源类型与锁定能力

1.1 五类 source 的差异

version 参数只对 Registry 源生效,其余源靠路径或 ref 锁定。

源类型 写法 支持 version 锁定手段
本地路径 ./modules/network 否 与 root 同一个 commit
公共 Registry hashicorp/consul/aws 是 version = "0.11.0"
私有 Registry app.terraform.io/acme/network/aws 是 version + 发布 tag
Git git::https://...?ref=v1.4.2 否 ref 写 tag 或 commit SHA
对象存储 s3::https://.../network-1.4.2.zip 否 对象名带版本号

本地路径在 monorepo 里最常见,模块和 root 同一个 commit,回滚代码等于回滚模块。代价是跨团队复用要共享仓库权限。

1.2 Registry 源

plain 复制代码
module "network" {
  source  = "app.terraform.io/acme/network/aws"
  version = "1.4.2"

  env      = var.env
  vpc_cidr = var.vpc_cidr
}

私有 Registry 的地址是 <HOSTNAME>/<ORG>/<NAME>/<PROVIDER>,先 terraform login 再 init,否则报 Could not retrieve the list of available versions。公共 Registry 省略 hostname。

1.3 Git 源

plain 复制代码
module "network" {
  source = "git::https://github.com/acme/tf-modules.git//modules/network?ref=v1.4.2"

  env      = var.env
  vpc_cidr = var.vpc_cidr
}

// 后面是仓库内的子目录,?ref= 接受分支名、tag、commit SHA。

⚠️ ?ref=main 等于没锁版本。分支 HEAD 一动,init -upgrade 拉到的就是另一份代码。

1.4 源解析流程

latex 复制代码
source ─┬─ 本地路径 ────────────────────────▶ 直接读磁盘
        ├─ Registry + version ──────────────▶ 查版本列表 ─▶ 下载 zip
        ├─ Git + ?ref ──────────────────────▶ clone / fetch
        └─ S3 / GCS ────────────────────────▶ 下载对象
                                            │
                                            ▼
                             .terraform/modules/<key>/
                             modules.json 记 source 与 ref

解析结果只在本机缓存,换台机器或删掉 .terraform/ 就得重新拉一遍。

2. 版本约束语法

2.1 运算符

写法 含义 匹配范围
1.4.2 精确等于 只有 1.4.2
!= 1.4.2 排除该版本 1.4.1、1.4.3
>= 1.4.0 下限 1.4.0 及以上
<= 1.5.0 上限 1.5.0 及以下
~> 1.4.0 放开 patch 1.4.x,不含 1.5.0
~> 1.4 放开 minor 1.x,不含 2.0.0
>= 1.4, < 2.0 区间组合 1.4 到 1.x 末尾

多个条件用逗号连接,按「与」处理,全部满足才可选。

2.2 ~> 的边界

~> 只放开最右边那一段,左边几段锁死。

约束 接受 拒绝
~> 1.4.0 1.4.0 -- 1.4.99 1.5.0、2.0.0
~> 1.4 1.4.0 -- 1.99.x 2.0.0
~> 1 1.0.0 -- 1.99.x 2.0.0

生产环境用 ~> 1.4.0,patch 自动跟进,minor 手动升。写成 ~> 1.4 会把 minor 升级一起放进来,模块加个必填变量就炸。

2.3 约束写在哪

三个维度的版本各管一层,写在三个位置。

plain 复制代码
terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.30"
    }
  }
}
位置 管什么 写错的表现
required_version terraform 二进制 Unsupported Terraform Core version
required_providers provider Failed to query available provider packages
module.version 模块 Module version requirements have not been met

3. 锁文件与模块缓存

3.1 lock 文件的范围

.terraform.lock.hcl 只记录 provider 的版本与校验和,模块不在里面。

latex 复制代码
provider "registry.terraform.io/hashicorp/aws" {
  version     = "5.31.0"
  constraints = "~> 5.30"
  hashes = [
    "h1:...",
    "zh:...",
  ]
}

模块的解析结果落在 .terraform/modules/modules.json,跟着 .terraform/ 一起被忽略。

json 复制代码
{
  "Modules": [
    { "Key": "", "Source": "", "Dir": "." },
    { "Key": "network", "Source": "git::https://github.com/acme/tf-modules.git//modules/network?ref=v1.4.2", "Dir": ".terraform/modules/network" }
  ]
}

提交策略分开:lock 进仓库,modules 目录不进。lock 里没有模块版本,模块的确定性完全靠 version 约束和 ?ref 撑着。

lock 提交的另一个作用是校验和。换镜像或走代理拉到同版本但内容不同的 provider,h1: 对不上会直接报错中断。

3.2 init -upgrade 的作用

bash 复制代码
terraform init              # 按 lock 与缓存还原,不换版本
terraform init -upgrade     # 在约束区间内取最新,重写 lock

-upgrade 只改版本选择,不改 source 和 version 的字面值。约束写得宽,它就直接跳到区间上界。

⚠️ -upgrade 对 Git 源的 ?ref=main 同样生效,拉一次就换一次代码。CI 里跑 init 不跑 -upgrade,避免构建结果随机漂移。

4. 升级策略

4.1 升级流程

latex 复制代码
读 CHANGELOG ─▶ 改 version ─▶ init -upgrade ─▶ plan
                                                   │
              ┌────────────────────────────────────┘
              ▼
              只有 ~ 变更 ──────────▶ 直接合入
              出现 -/+ 替换 ─▶ 补 moved ─▶ 再 plan ─▶ 合入

plan 里的 -/+ 是资源替换,~ 是原地更新。替换数量超过个位数,先查是不是模块内部改了资源地址。

4.2 环境推进顺序

阶段 观察项 停留时间
dev plan 差异条数、~ 与 -/+ 的比例 1 个发布周期
staging apply 后接口连通性、日志报错 1--2 天
prod apply 时长、实际替换数量 无固定期限

一次只升一个模块,别把 network 和 compute 一起动。两个模块同时换版本,plan 里的差异分不清是谁带来的。

4.3 资源地址变化

模块内部把 aws_instance.this 改名成 aws_instance.web,调用方 plan 立刻看到 destroy + create。在模块里补一段 moved,升级就变回原地更新。

plain 复制代码
moved {
  from = aws_instance.this
  to   = aws_instance.web
}

moved 从 Terraform 1.1 开始支持,写在模块内部,调用方一行都不用改。资源换到另一个模块里,用 state mv 改地址:

bash 复制代码
terraform state mv 'module.network.aws_subnet.public' 'module.subnet.aws_subnet.public'

5. 私有 Registry 与模块治理

5.1 仓库命名与 tag

私有 Registry 按 <org>/<name>/<provider> 建索引,仓库名必须是 terraform-<provider>-<name>,否则发布页认不出来。

项 规范
仓库名 terraform-aws-network
模块地址 app.terraform.io/acme/network/aws
tag v1.4.2,语义化版本
分支 main 只放开发代码,不供引用
文档 根目录 README.md 写输入输出表

tag 与 Registry 上的版本一一对应,推 tag 触发一次发布。私有 Registry 只认 tag,分支上的代码不会出现在版本列表里,调用方也选不到。

latex 复制代码
模块仓库 ──push tag v1.4.2──▶ CI 校验 ──▶ 私有 Registry
                                            │
                                            ▼
root ──version = "1.4.2"──▶ init ──▶ .terraform/modules/network

5.2 发布前校验

CI 跑三件事:格式化、校验、示例 plan。

yaml 复制代码
- name: fmt
  run: terraform fmt -check -recursive
- name: validate
  run: terraform init -backend=false && terraform validate
- name: plan examples
  run: cd examples/basic && terraform init && terraform plan -input=false

terraform validate 只查语法与类型,不连云端。examples/ 下的 plan 才能抓到变量拼错、必填项漏传。

5.3 治理条目

  • 每个模块一个 owner,写进 CODEOWNERS
  • 破坏性变更升 major,CHANGELOG 标 BREAKING CHANGE
  • 旧 major 保留一个版本周期,README 顶部写迁移说明
  • root 里禁止出现 ?ref=main 和不带 version 的 Registry 源
  • 每季度跑一次全量 init -upgrade,统计落后版本

前两条靠人记不住,交给 CI 卡:PR 里改了 variables.tf 的必填项却没升 major,流水线直接拒。

6. 常见报错

报错文案基本能指到位置,先分清是源解析失败还是参数对不上。

报错 原因 处理
Module version requirements have not been met 约束区间内没有可用版本 放宽约束或补发 tag
Unsupported argument 模块升级后参数改名或删除 对着 CHANGELOG 改调用方
Invalid module source address // 子目录与 ?ref 顺序写错 改成 ...git//modules/x?ref=v1.4.2
Could not download module Git 凭据缺失或 tag 不存在 配 SSH key 或核对 tag
Module not installed 改了 source 没重新 init terraform init
Failed to query available provider packages 私有 Registry hostname 未登录 terraform login <host>

Git 源走 SSH 最省事,HTTPS + PAT 要配 credential helper,CI 里还要单独注一次环境变量。

7. 检查清单

  • 所有 Registry 源都写了 version 约束
  • Git 源的 ?ref 指向 tag 或 commit,没有分支名
  • .terraform.lock.hcl 已提交,.terraform/ 已忽略
  • 升级先在 dev 跑完 plan,再推 staging
  • 模块内部改名用 moved,不用 state mv 兜
  • 破坏性变更升 major 并写迁移说明

8. 关联文章

  • Terraform 模块组合:VPC、安全组、EC2 的输出传递与依赖编排
  • 编写第一个 Terraform Module:结构、输入输出与变量校验

9. 参考

相关推荐
时间售卖铺1 小时前
MySQL 表的约束
运维·数据库·mysql·云原生
做运维的阿瑞2 小时前
Terraform 模块组合:VPC、安全组、EC2 的输出传递与依赖编排
云原生·云计算·terraform
孔令飞2 小时前
【OneX】OneX AI 工具安装、配置及功能介绍
云原生·go·ai编程
时间售卖铺2 小时前
MySQL 安装部署
运维·数据库·mysql·云原生
FY@-@4 小时前
云计算基础(下):虚拟化原理与 DAS/NAS/SAN 存储
运维·服务器·云计算
做运维的阿瑞4 小时前
Terraform 敏感信息管理:sensitive 标记、外部密钥源与 state 加密
云原生·云计算·terraform
Z21-X4 小时前
云原生运维学习笔记:揭开虚拟化的硬核面纱,从底层架构到KVM实战
运维·笔记·学习·云原生
mengge.cloud5 小时前
1022小白入门教程(番外筑基)— nerdctl 容器基本功 + K8s 资源编写
linux·网络·云原生·容器·kubernetes·云计算·k8s
FY@-@5 小时前
云计算基础(上):从传统 IT 到 IaaS/PaaS/SaaS
云原生·云计算·paas