上周三 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. 参考
- Terraform 文档 · 模块源:https://developer.hashicorp.com/terraform/language/modules/sources
- Terraform 文档 · 版本约束:https://developer.hashicorp.com/terraform/language/expressions/version-constraints
- Terraform 文档 · 依赖锁文件:https://developer.hashicorp.com/terraform/language/files/dependency-lock
- Terraform 文档 · 模块重构(moved):https://developer.hashicorp.com/terraform/language/modules/develop/refactoring
- Terraform Registry 发布模块:https://developer.hashicorp.com/terraform/registry/modules/publish