1. 许可证变更与分叉始末
一句话总结: 2023 年 HashiCorp 把 Terraform 从 MPL 2.0 改为 BUSL 1.1,社区为保住开源许可而分叉出 OpenTofu,并把它捐给 Linux 基金会,这决定了此后两条产品线的所有差异。
理解迁移,先要理解「为什么要分叉」。这不是一次简单的版本分支,而是一次许可证驱动的社区重组。
1.1 时间线
| 时间 | 事件 |
|---|---|
| 2023-08 | HashiCorp 宣布 Terraform 由 MPL 2.0 转为 BUSL 1.1 |
| 2023-08 | 社区发起 OpenTF 宣言,主张保留开源许可 |
| 2023-09 | 项目更名 OpenTofu,并入 Linux 基金会 |
| 2024-01 | OpenTofu 发布首个稳定版 1.6 |
| 2024-04 | Terraform 发布 1.8,引入 provider 定义的函数 |
| 2024-05 | OpenTofu 1.7 引入 state 加密与 provider 迭代 |
| 2025-01 | Terraform 1.10 引入 terraform stacks 与 ephemeral 资源 |
| 2025-05 | OpenTofu 1.9 对齐 ephemeral 资源与 -exclude 参数 |
1.2 BUSL 到底限制了什么
BUSL 1.1 的核心是「非生产性使用免费、与 HashiCorp 产品竞争的生产性使用受限」。落到工程实践上:
- 你可以继续用 Terraform CLI 管理自己的基础设施,这属于允许的使用。
- 你不能把 Terraform 包装成与 Terraform Cloud 竞争的商业服务对外售卖。
- BUSL 每个版本在发布四年后自动转为 MPL 2.0,因此「旧版本」最终仍是开源的。
一句话总结: BUSL 不影响绝大多数终端用户,但它剥夺了「把 Terraform 作为开源组件嵌入商业产品」的自由,这正是企业法务部门最先警觉的地方。
2. 版本与功能差异
一句话总结: OpenTofu 与 Terraform 共享 HCL 语法与 state 格式,但版本号从 1.6 起各走各的路线,功能上互有领先,迁移前必须逐项比对差异清单。
2.1 版本号对应关系
两者都从 1.6 出发,但此后并不一一对应。粗略的对照关系如下:
| Terraform | OpenTofu | 关系 |
|---|---|---|
| 1.6 | 1.6 | 同源,行为几乎一致 |
| 1.7 | 1.7 | OpenTofu 加入 state 加密、provider 迭代 |
| 1.8 | 1.8 | OpenTofu 加入变量与 provider 的 for_each |
| 1.9 | 1.9 | 功能开始明显分岔 |
| 1.10 | 1.9+ | Terraform 的 stacks 未进入 OpenTofu |
| 1.11 | 1.10 | 各自独立演进 |
不要假设版本号相同就功能相同。迁移评估的正确做法是列出你实际用到的特性,逐个到两边文档核对。
2.2 关键功能差异
| 维度 | Terraform | OpenTofu |
|---|---|---|
| 许可证 | BUSL 1.1 | MPL 2.0 |
| 状态加密 | 仅 Terraform Cloud/Enterprise | 开源版内置 |
| Provider 注册表 | 官方 registry 为主 | 官方 + 去中心化镜像 |
变量 for_each | 支持 | 支持且更早落地 |
| 测试框架 | terraform test | tofu test,语法相近 |
| Provider 定义函数 | 1.8 引入 | 1.7 起支持 |
| Ephemeral 资源 | 1.10 引入 | 1.9 引入 |
| Stacks 编排 | 有 | 无对应物 |
一句话总结: 对大多数团队而言,两边功能足以覆盖日常需求;真正的分水岭是「状态加密」与「Stacks」——前者 OpenTofu 开源即得,后者只有 Terraform 有。
3. 状态与配置的兼容性
一句话总结: 两者的 state 格式都是 v4、backend 协议一致、provider 校验和互通,因此同一份 state 可以被两边读写,这既是迁移可行的基础,也是双跑风险的来源。
3.1 state 与 backend
state 文件格式在两边都是 version 4,字段结构一致。常用的 backend 也都兼容:
terraform {
backend "s3" {
bucket = "acme-tfstate-prod"
key = "network/terraform.tfstate"
region = "ap-northeast-1"
dynamodb_table = "acme-tfstate-lock"
encrypt = true
}
}
# 用 OpenTofu 读取同一份远端 state,先做只读验证
tofu init -backend-config=backend.hcl
tofu state list | head -20
上面两条命令足以确认「同一份 state 两边都能解析」。注意 tofu init 会生成自己的 .terraform 目录,但不会覆盖远端 state。
3.2 锁文件与 provider 校验和
.terraform.lock.hcl 是迁移中最容易被忽视的坑。它记录了每个 provider 的版本与校验和,而两边默认写入的哈希集合并不完全相同。
# .terraform.lock.hcl 片段
provider "registry.terraform.io/hashicorp/aws" {
version = "5.60.0"
constraints = "~> 5.60"
hashes = [
"h1:AbCdEf...",
"zh:1234567890abcdef...",
]
}
# 让锁文件同时包含两边都能识别的哈希
tofu providers lock \
-platform=linux_amd64 \
-platform=darwin_arm64 \
-platform=windows_amd64
tofu providers lock 会把官方 registry 与 OpenTofu registry 的哈希都写进去,避免「同一份锁文件在另一侧 init 时报校验和不匹配」。
一句话总结: state 与 backend 的兼容让迁移变得可行,但锁文件的哈希集合差异会让
init直接失败——迁移前先跑一次providers lock补齐哈希,是最廉价的风险消除手段。
4. OpenTofu 的状态加密与去中心化注册表
一句话总结: 状态加密与去中心化注册表是 OpenTofu 相对 Terraform 开源版最实在的两项增量,前者让 state 里的明文密钥就地消失,后者让 provider 来源不再单点。
4.1 状态加密
Terraform 开源版的 state 是明文的,敏感值只能靠 backend 侧加密兜底。OpenTofu 把加密做进了引擎:
terraform {
encryption {
key_provider "pbkdf2" "passphrase" {
passphrase = var.state_passphrase
}
method "aes_gcm" "default" {
keys = key_provider.pbkdf2.passphrase
}
state {
method = method.aes_gcm.default
enforced = true
}
}
}
关键点是 enforced = true:它让引擎拒绝写出未加密的 state,从而避免「配置漏写导致某次 apply 落盘明文」。
生产环境更常用云 KMS 作为 key provider:
terraform {
encryption {
key_provider "aws_kms" "main" {
kms_key_id = "arn:aws:kms:ap-northeast-1:123456789012:key/abcd-1234"
key_spec = "AES_256"
region = "ap-northeast-1"
}
method "aes_gcm" "kms" {
keys = key_provider.aws_kms.main
}
state {
method = method.aes_gcm.kms
}
}
}
迁移提醒:加密开关一旦打开,state 就被重写为密文;此时若回退到 Terraform,旧版无法解密。加密与回滚是互斥的,必须放在迁移的最后一步。
4.2 去中心化注册表与镜像
OpenTofu 支持从多个来源解析 provider,企业可以自建镜像:
terraform {
required_providers {
aws = {
source = "registry.opentofu.org/hashicorp/aws"
version = "~> 5.60"
}
}
}
# 通过环境变量把 provider 解析指向内部镜像
export TF_REGISTRY_CLIENT_TIMEOUT=30
export TOFU_REGISTRY_MIRROR="https://mirror.internal.example.com/tofu"
tofu init
镜像的价值不只是「离线可用」:它还消除了「上游 registry 删版本或改签名」这类供应链单点风险。
一句话总结: 状态加密把「state 明文」这个长期顽疾从根上解决,代价是与 Terraform 的 state 互读性——所以它必须排在迁移顺序的末端,而不是开头。
5. 渐进迁移策略
一句话总结: 迁移不是「换个二进制就完事」,而是「二进制替换 → 只读验证 → 双跑对比 plan → 按模块分批切换」的四段式推进,每一段都有明确的回退点。
5.1 二进制替换与只读验证
第一步永远是不改任何东西的只读验证:
# 1. 安装 OpenTofu 并与 Terraform 并存
brew install opentofu
tofu version
terraform version
# 2. 在只读模式下初始化,复用同一份远端 state
tofu init -backend-config=envs/prod/backend.hcl
# 3. 与 Terraform 的 plan 逐字节对比
terraform plan -out=tf.plan -no-color > plan.terraform.txt
tofu plan -out=tofu.plan -no-color > plan.opentofu.txt
diff -u plan.terraform.txt plan.opentofu.txt
diff 为空是「可以继续」的最强信号。若有差异,先定位是 provider 版本差异还是引擎行为差异,不要带着未知差异往下走。
5.2 双跑对比与 CI 并行验证
把上面这套对比固化进 CI,让它每天自动跑:
# .github/workflows/tofu-parity.yml
name: tofu-parity
on:
schedule:
- cron: "0 2 * * *"
workflow_dispatch:
jobs:
parity:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: opentofu/setup-opentofu@v1
with:
tofu_version: 1.9.0
- name: tofu plan
run: |
tofu init -backend-config=envs/prod/backend.hcl
tofu plan -no-color -lock=false > tofu.txt || true
- uses: actions/upload-artifact@v4
with:
name: tofu-plan
path: tofu.txt
关键是 -lock=false:并行验证阶段两边都会读 state,加锁会互相阻塞。同时用 || true 容忍非零退出(plan 有 diff 时退出码为 2)。
5.3 按模块分批切换
不要一次全量切换。推荐顺序:
| 阶段 | 范围 | 观察期 |
|---|---|---|
| 第 1 批 | 只读账户、审计类、无状态资源 | 1 周 |
| 第 2 批 | 非核心环境(dev/staging) | 2 周 |
| 第 3 批 | 核心网络与 IAM | 2 周 |
| 第 4 批 | 生产业务资源 | 4 周 |
| 第 5 批 | 打开状态加密 | 长期 |
每批切换的判定标准是「该批模块连续两次 apply 的 plan 为空,且 CI parity 无差异」。
一句话总结: 渐进迁移的实质是把「一次性大爆炸」拆成若干次可观测、可回退的小步,而
plan的逐字节对比是贯穿始终的绿灯。
6. 回滚与风险控制
一句话总结: 回滚的前提是「两边都能读写同一份 state」,因此版本 pin、锁文件、加密开关与双写顺序,构成了回滚能力的四道闸门。
6.1 四道闸门
| 闸门 | 做法 | 破坏回滚的典型错误 |
|---|---|---|
| 版本 pin | CI 与本地都固定 tofu_version | 用 latest,某天自动升版 |
| 锁文件 | 迁移期提交两边哈希 | 只提交一侧哈希,另一侧 init 失败 |
| 加密 | 最后一步才 enforced = true | 迁移初期就开加密 |
| 双写 | 同一时刻只允许一侧 apply | 两边同时 apply 造成互相覆盖 |
6.2 双写风险与规避
「双写」指同一份 state 被 Terraform 与 OpenTofu 交替 apply。风险在于:
- 两边 provider 版本不同,可能写出对方不认识的 state 字段。
- 两边的 plan 缓存不同步,可能出现「A 刚建、B 计划删除」。
- 锁机制(如 S3 + DynamoDB)虽然能防并发,但防不了「先后顺序错乱」。
规避办法是串行化:迁移期用一个共享的锁服务(CI 中的 concurrency group 或 DynamoDB 锁表),并规定「同一模块在同一时间窗口只允许一个引擎操作」。
# 迁移期强制串行:任何 apply 前先申请外部锁
tofu apply -lock-timeout=10m -out=tofu.plan
# 禁止使用 -lock=false 做 apply(只允许用于 plan)
6.3 迁移检查清单
[ ] 备份当前 state(tofu state pull > backup-$(date +%F).tfstate)
[ ] 记录当前两边版本号与 provider 版本
[ ] 锁文件已补齐双平台哈希
[ ] CI parity 连续 7 天无差异
[ ] 回滚剧本已演练(含 state 恢复步骤)
[ ] 加密开关处于关闭状态
[ ] 变更窗口内无其他 IaC 任务
[ ] 法务已确认 BUSL 使用边界
一句话总结: 回滚能力不是「事后补救」,而是在迁移开始前就用版本 pin、锁文件与串行化设计好的;一旦打开状态加密,回滚窗口就永久关闭。
7. 生态与长期维护判断
一句话总结: 两边 provider 生态高度重叠、模块基本通用,选择的关键不在「谁能跑」,而在「你的团队愿意承担哪一种维护风险」。
7.1 provider 与模块兼容现状
| 类别 | 兼容情况 |
|---|---|
| 官方 hashicorp provider | 完全兼容,两边同一份二进制 |
| 主流社区 provider | 完全兼容,多数已上架 OpenTofu registry |
| 私有 provider | 兼容,但需自行镜像到内部 registry |
| Public Module Registry | 基本兼容,注意部分模块用了 Terraform 独有特性 |
| Terraform Cloud 专属功能 | 不兼容,OpenTofu 无对应物 |
绝大多数 provider 是编译好的二进制插件,走 gRPC 协议,因此引擎是哪个并不重要。真正不兼容的是引擎级能力:Stacks、Terraform Cloud 的远程运行与策略集、Sentinel 策略。
7.2 混合使用的边界
「混用」在实践中是常态,但有明确边界:
- 同一 state 不要混用:一个 state 文件同一时间只由一个引擎管理。
- 模块可以混用:同一份模块代码两边都能调用,只要不用引擎独有特性。
- CI 可以混用:一个流水线跑 Terraform、另一个跑 OpenTofu 做 parity 对比,是推荐的过渡形态。
- 策略层不要混用:Sentinel 与 OPA/Rego 是两套体系,混用会让审计口径分裂。
7.3 长期判断
选择取决于三个问题:
- 你的组织是否受 BUSL 约束?如果是,OpenTofu 是唯一合规选项。
- 你是否重度依赖 Terraform Cloud/Stacks?如果是,迁移成本会显著高于收益。
- 你能否承受「跟随一个小得多的社区」?OpenTofu 的迭代速度不慢,但生态广度仍不及 HashiCorp 阵营。
一句话总结: 生态兼容度已经高到「技术层面几乎无痛」,因此决策本质上是许可证与治理模型的取舍,而不是功能对比。
8. 总结
OpenTofu 与 Terraform 的分叉,是一次由许可证驱动的社区重组,而迁移则是一次「同格式、双引擎」的平滑过渡工程:
| 环节 | 要点 |
|---|---|
| 背景 | BUSL 1.1 促使社区分叉,OpenTofu 归入 Linux 基金会 |
| 版本 | 1.6 同源后各走各路,须逐特性核对而非对号入座 |
| 兼容 | state v4 与 backend 协议一致,是迁移可行的基础 |
| 锁文件 | providers lock 补齐双平台哈希,消除 init 失败 |
| 加密 | 状态加密是 OpenTofu 最大增量,也是回滚的终点 |
| 迁移 | 二进制替换 → 只读验证 → 双跑对比 → 分批切换 |
| 回滚 | 版本 pin、锁文件、加密开关、串行化四道闸门 |
| 生态 | provider 高度通用,分歧集中在引擎级能力与策略层 |
一句话收尾:迁移的技术门槛远低于心理门槛,真正需要谨慎的是「加密开关」与「双写顺序」这两个不可逆动作。把 plan 对比当作绿灯、把回滚剧本当成交付物,OpenTofu 与 Terraform 的共存就能长期稳定运行。下一篇「状态手术进阶」将深入 state 的手工修复与导入重建,处理迁移中最棘手的一类故障。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。