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. 参考

相关推荐
那明眸似水6 小时前
云原生项目三:Docker秒级启动NextCloud个人网盘存储办公系统
linux·运维·docker·云原生·容器
腾飞开源7 小时前
03_Kubernetes 核心概念拆解:从 Spec/Status 收敛到工作负载控制器的设计逻辑
云原生·容器·kubernetes
知已难寻18 小时前
Docker 入门:容器技术与 Docker 基础
java·运维·服务器·docker·云原生·容器
阿里云云原生1 天前
生产更可信,订阅更耐用丨Higress 社区进展报告
云原生
阿里云云原生1 天前
如何用 STAROps Elasticsearch 智能助手看穿“搜索变慢”
云原生
雾时之林1 天前
国内Docker 镜像加速器配置
云原生·eureka
牛奶咖啡131 天前
云计算平台应用实践——云计算与云平台
云计算·云平台·云平台的三大服务模式·云计算与云平台的关系·laas paas saas·云平台的主流架构·国内外的主流云平台对比与选型
我是koten2 天前
【运维杂谈】K8s遇到Terminating的名称空间删除不了怎么办?(解决方案亲测有效)
linux·运维·服务器·云原生·kubernetes·k8s·名称空间
阿里云云原生2 天前
稳定复现超 70% 的缺陷,报警到自动修复的 Agent 流水线实践
云原生
阿里云云原生2 天前
从“自证清白”到“因果链条”:智能体如何重塑运维排障效率
云原生