- Published on
OpenTofu 1.12 的动态 prevent_destroy — lifecycle 静态约束松动之处及其代价
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 引言 — "Variables may not be used here"
- OpenTofu 1.12 做了什么 — 把求值的时机挪走
- 能引用什么,不能引用什么
- destroy = false — 这是另一码事
- prevent_destroy 变成动态之后,依然堵不上的漏洞
- Terraform 走了另一条路
- 那么,这个到底该不该用
- 结语
- 参考资料
引言 — "Variables may not be used here"
用 Terraform 用得久的人,多半都撞见过这个报错。想保护数据库实例不被误删,于是这样写,
variable "allow_destroy_database" {
type = bool
default = false
}
resource "aws_db_instance" "example" {
# ...
lifecycle {
prevent_destroy = !var.allow_destroy_database
}
}
Terraform 会拒绝。lifecycle 块里不能用变量。哪怕你想保护生产环境、同时让开发环境随便删,除了把同一个模块复制一份,或者用 count 技巧,也没有别的办法。
这个约束不是偷懒造成的。HashiCorp 的 lifecycle 文档把原因说得很清楚 — lifecycle 的设置会影响 Terraform 构建和遍历依赖关系图的方式,所以"only literal values can be used"。也就是说,这一步处理的时机太早,还轮不到对任意表达式求值。构建图需要先知道 lifecycle 的值,而对表达式求值又需要图已经存在。鸡生蛋、蛋生鸡。
这个请求本身很老了。hashicorp/terraform#10730("Unable to use var for ignore_changes or prevent_destroy")在 2016 年 12 月 14 日提出,2019 年 8 月 27 日关闭;如今仍然开着的正式请求 #25534("Allow variables in lifecycle block")是 2020 年 7 月 9 日提出的。写这篇文章时,#25534 上有 66 个 👍。反应数只是需求的一个弱信号,并不是精确的度量,而且 GitHub 上的反应数随时都会变,读的时候要留意这一点。
OpenTofu 1.12 做了什么 — 把求值的时机挪走
OpenTofu v1.12.0 在 2026 年 5 月 14 日发布(按 GitHub 发布元数据),松开了这个约束。用官方文档的说法,lifecycle 块的 prevent_destroy 参数现在可以引用同一模块内的其他符号 — 比如模块的输入变量。
有意思的是它是怎么松开的。PR #3474 的说明讲得很清楚。以前配置加载器(config loader)会立即对 prevent_destroy 表达式求值,所以它只能是返回布尔值的常量表达式。现在加载器会原样保存收到的表达式,把求值交给语言运行时。这样一来,就能引用同一模块内动态确定的值了。
也就是说,"必须在构建图之前就知道"这个前提本身并没有被动,而是把求值能推多晚就推多晚。在 tofu validate 阶段,依然会执行和旧加载器等效的布尔类型检查;用 nil 求值上下文无法求值的表达式,会被推迟到 plan 阶段在那里检查。PR 的说法是,plan 时点信息更多,报错信息也会因此更准确。
开头那个例子 — prevent_destroy = !var.allow_destroy_database — 在 OpenTofu 里现在可以直接用了。最初的请求 opentofu#2522 是 2025 年 2 月 14 日提出的,由这个 PR 关闭。
能引用什么,不能引用什么
这里得说老实话。"动态"不等于"什么都行"。
范围被限定在同一个模块之内。而且 PR #3474 明确表示,对棘手的情况 — 未知值(unknown values)、临时值(ephemeral values)、敏感值(sensitive values)、count.index 这类局部符号引用 — 有意做出了最保守的选择。理由是站得住的:以后版本要放得更宽随时都可以,但一旦发现放宽是个错误,已经允许的东西是收不回去的。放松规则是向后兼容的,收紧则不是。
由此能得出的实际结论是这样 — 不能靠引用资源属性来决定 prevent_destroy。资源属性在 apply 之前可能是 unknown,而 unknown 被保守地挡住了。这个功能是"用变量来定每个环境的保护级别",不是"看运行时状态来做判断"。
这种模式不是第一次出现。OpenTofu 早在 v1.11.0(2025 年 12 月 9 日)就已经把 enabled 元参数引入了 lifecycle 块,官方文档给出的 enabled 约束几乎是同一份清单 — 不能用未知值、敏感值、null 值、临时值、非布尔值,也不能和 count 或 for_each 一起用。lifecycle 块变得动态的方式,是有一条一致的线的:值可以是动态的,但必须在 plan 时点就能知道。
destroy = false — 这是另一码事
同一个 1.12 版本里,加入了一个叫 destroy 的新 lifecycle 元参数。名字听着像和 prevent_destroy 是一路的,但性格相当不同,而且实际上它对 state 管理的影响直接得多。
默认情况下,当资源从配置中消失、需要被替换,或者被 tofu destroy 显式删除时,OpenTofu 会销毁实际的基础设施对象。设成 destroy = false,计划的就不是销毁,而是遗忘(forget)— 实际对象原样留着,只从 state 里移除。
resource "aws_db_instance" "example" {
# ...
lifecycle {
destroy = false
}
}
官方文档给出的用例说得通:下线其余环境时想留下受监管的资源、替换过程中想留着旧实例方便回滚、或者要处理那种因为外部依赖本来就没法销毁的对象。要点是,它把原来靠手动敲 tofu state rm 完成的事,变成了一次可评审的配置变更。
但附带的条件不轻。我们一条条看。
第一,只接受常量。 文档上钉死了 — lifecycle.destroy 只接受常量布尔值(true 或 false)。刚说完 prevent_destroy 变成了动态的,而同一版本加入的 destroy 却不是动态的。这意味着 lifecycle 块并不是整体变成了动态,而是因参数而异 — 不知道这个不对称,用起来会犯迷糊。
第二,会赖在 state 里不走。 这个参数会存进 state。一旦应用了 destroy = false,在你明确改回 true 或者从配置里删掉这个选项之前,OpenTofu 都不会计划销毁那个资源。它是按最后一次应用过的配置,保守地做判断。对于单实例资源(没有 count、for_each),可以在 removed 块里写 destroy = true 来覆盖 state 里的这个值;但用了 count 或 for_each 的资源没有这个退路。
第三,会让 prevent_destroy 失效。 这一条最要紧。文档明确给出了警告:设置 destroy = false 之后,OpenTofu 会把 destroy 动作变成 forget 动作的一个变体,prevent_destroy 不会起任何作用。很容易把这两个参数都归进"安全装置"这同一个抽屉,以为都打开会更安全,但实际情况是后者会把前者关掉。
第四,把遗忘的资源找回来很麻烦。 正如文档警告的那样 — 一旦资源被遗忘(从 state 中移除),OpenTofu 就不再追踪它了。之后如果用同一个地址把它重新加回配置,OpenTofu 会尝试创建一个新资源,如果实际对象还活着,这次尝试就可能失败。要救回来,得做 import,或者换一个资源地址。
第五,退出码会变。 在存在 destroy = false 资源的情况下跑 tofu destroy,这些资源会被遗忘而不是销毁,命令随后会以非零退出码结束,用来提示有些资源没有被完全移除。CI 会把这读作失败。1.12 为此专门加了 tofu destroy -suppress-forget-errors。
还有一点 — 这并不是 OpenTofu 独有的发明。Terraform 也有自己的 removed 块,给它 destroy = false 同样能做到不销毁实际资源、只从 state 中移除。区别在于用在哪个环节。removed 块是一个在把资源移出配置时使用的重构工具;而 OpenTofu 的 lifecycle 参数是在资源留在配置里期间改变销毁行为。OpenTofu 的文档自己也划出了这条线 — 目的是重构就用 removed 块,要控制留在配置里的资源的销毁行为就用 lifecycle 的 destroy。
prevent_destroy 变成动态之后,依然堵不上的漏洞
动态 prevent_destroy 固然让人高兴,但它没有解决的那个漏洞,也得说清楚。
prevent_destroy 的保护,只在那个参数还留在配置里的时候有效。正如文档自己说的,把整个 resource 块从配置里删掉,prevent_destroy 的设置也会跟着一起消失,于是 OpenTofu 就会允许销毁。也就是说,这个安全装置挡得住"删除这个资源的 plan",挡不住"删除这个资源定义的提交"。想到误删资源最常见的路径恰恰就是后者,这个局限相当大。
变成动态并不会让这一点变好,表面上甚至可能变得更糟 — 现在 prevent_destroy 的值取决于变量,tfvars 里的一行,或者 CI 环境变量里的一个值,就成了关掉保护的开关。这个开关是放在要经过代码评审的地方,还是放在谁都能用流水线变量覆盖的地方,现在成了你自己的问题。这个功能给的是灵活性,而灵活性需要纪律来配。
PR #3474 还留了一个悬而未决的问题。动态 prevent_destroy 的效果该怎么用 tofu test 测试,PR 的作者在这个 PR 里完全没有回答,只是写下如果有人感兴趣,以后可以另外处理。也就是说,把保护逻辑靠变量分支出去之后,怎么测试那个分支,目前还没有标准做法。
Terraform 走了另一条路
差不多同一时期,Terraform 正在完全不同的地方解决同一个静态求值问题。
Terraform v1.15.0 在 2026 年 4 月 29 日发布,新功能列表里有这么一项 — 模块的 source 和 version 属性现在可以用变量和局部值了(#38217)。这也是在松开同一类约束:"必须在配置加载时点就知道的值"。要引入模块得先知道 source,如果这个值是变量,就得先对变量求值。方向是一样的,只是地方不同。
同一版本里还加入了给变量、输出块附加 deprecated 属性的功能(#38001)、用于精确内联类型转换的 convert 函数(#38160)、连 backend 块都检查的 terraform validate(#38021),以及 Windows ARM64 构建。而 lifecycle 块 — 前面引用的那句文档原话依然没变 — 依然只接受字面量。
有意思的是趋同也在同时发生。两个项目都在这一轮周期里为 s3 后端加入了 aws login 凭证支持(OpenTofu #3767, Terraform #37976)。分叉不只是互相分开,生态系统把同一种需求往上推的时候,两边有时也会并排造出同样的东西。
写这篇文章时是 2026 年 7 月 16 日,最新的稳定版是 OpenTofu 的 v1.12.4(7 月 13 日)和 Terraform 的 v1.15.8(7 月 8 日),Terraform 正在跑 v1.16.0 alpha。
那么,这个到底该不该用
用了值回成本的情况
- 想用一个模块同时覆盖 dev、staging、prod,只是保护级别按环境不同。这正是动态
prevent_destroy的用武之地,比起复制模块或者用count技巧,明显更干净。 - 有一套下线环境时只留下特定资源的流程,现在是靠手动执行
tofu state rm的操作手册在跑。destroy = false能把这套手动操作变成一次会体现在 plan 里的配置变更。
该推迟或该避免的情况
- 配置得同时在 Terraform 和 OpenTofu 上跑。一旦用了这个语法,这份配置在 Terraform 上就不再能用了。想在两个分叉之间留有余地的话,这是一张单程票 — 如果是公开发布的模块,这一点更要紧,因为用户里谁用哪一边,不是你能决定的。
- 如果真正想防的是"误删资源的事故",
prevent_destroy本来就堵不上那个漏洞(从配置里删掉块这条路径)。plan 评审、禁用-target、CI 门禁、state 备份这类流程层面的机制,依然是主体,这个功能只是辅助。 - 抱着"先打开总归安全"的心态去用
destroy = false的情况。它会赖在 state 里不走,会关掉prevent_destroy,想撤回还得靠 import。这是一个开起来容易、关起来难的开关。
结语
归纳一下。Terraform 的 lifecycle 块只接受字面量,是因为它是在构建依赖关系图的阶段处理的 — 这是一个从 2016 年就被指出来的结构性约束。OpenTofu 1.12 把 prevent_destroy 的求值从配置加载器搬到了语言运行时,从而松开了这个约束 — 只是把范围限定在同一模块之内,并且有意把 unknown、敏感、临时值以及 count.index 保守地挡在外面。为了避免不可挽回的失误而从窄处起步,我认为这个判断是对的。
同一版本里的 destroy = false,分量比名字听起来重得多。它只接受常量,会存进 state,会让 prevent_destroy 失效,想找回被遗忘的资源得靠 import,还会改变 tofu destroy 的退出码。有用,但打开之前得把这五条都弄清楚。
往大了看,这不是一个分叉"谁更强"的故事。两边都在推同一堵静态求值的墙,只是推的地方不一样 — OpenTofu 推的是 lifecycle,Terraform 推的是模块的 source 和 version。结果留给实践者的,不是一份功能清单,而是一个可移植性的决定。一旦用了这个语法,你的配置就只属于其中一边了。这样做是否可以接受,是比这个功能酷不酷更该先回答的问题。
参考资料
- OpenTofu v1.12.0 发布公告(2026-05-14)
- OpenTofu 1.12 CHANGELOG — 完整条目
- OpenTofu 文档 — 资源行为与 lifecycle 定制(
prevent_destroy、destroy) - OpenTofu 文档 —
enabled元参数(v1.11 引入) - opentofu/opentofu#3474 — 动态 prevent_destroy 实现 PR 与设计依据
- opentofu/opentofu#2522 — 最初的功能请求(2025-02-14)
- Terraform 文档 — lifecycle 元参数(字面量值约束)
- Terraform 文档 —
removed块 - Terraform v1.15 CHANGELOG(v1.15.0, 2026-04-29)
- hashicorp/terraform#25534 — Allow variables in lifecycle block(2020-07-09, 仍开着)
- hashicorp/terraform#10730 — Unable to use var for ignore_changes or prevent_destroy(2016-12-14)
- OpenTofu vs Terraform 2026 对比 — 许可证、State 加密、迁移(相关文章)