ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

OpenTofu 资源实例变更生命周期:从 Provider 协议到计划与应用(Plan/Apply)的完整实现指南

OpenTofu 资源实例变更生命周期:从 Provider 协议到计划与应用(Plan/Apply)的完整实现指南 OpenTofu 资源实例变更生命周期从 Provider 协议到计划与应用Plan/Apply的完整实现指南【免费下载链接】opentofuOpenTofu lets you declaratively manage your cloud infrastructure.项目地址: https://gitcode.com/gh_mirrors/op/opentofuOpenTofu 的资源实例变更生命周期描述了 Provider 与 OpenTofu Core 之间围绕一次资源变更所进行的全部交互从用户编写的配置Configuration与上一次运行保存的状态Previous Run State出发经由 Provider 协议上的数个核心 RPC 调用完成规划plan与应用apply最终产出将被持久化保存的新状态New State。本文将基于 docs/resource-instance-change-lifecycle.md 的系统阐述并结合仓库源码与测试验证逐层拆解每种状态对象的含义、Provider 协议六大函数的契约约束ValidateResourceConfig、PlanResourceChange、ApplyResourceChange、UpgradeResourceState、ReadResource、ImportResourceState以及嵌套块与导入等特殊场景的处理规则帮助你既能写出符合协议契约的 Provider也能深入理解 OpenTofu Core 在规划与应用阶段对 Provider 行为施加的底层约束。OpenTofu 资源实例变更生命周期示意图一次变更的总体目标状态环上的闭环资源实例变更生命周期整体上是一个循环过程其总目标可以概括为拿到一份配置Configuration与一份上次运行状态Previous Run State使用资源类型专属的规划逻辑将两者合并产出一份计划状态Planned State在 apply 阶段修改远端系统使其与计划状态一致最终产出新状态New State并保存到状态快照中使其成为下一次运行的上次运行状态。这个闭环在下一次tofu plan/tofu apply时重新开始且环上的每一次转动都会经过ReadResource对远端对象做一次“尽力而为”的漂移检测详见下文ReadResource一节。关于 OpenTofu 在没有任何特殊指令时的默认规划行为Create / Delete / Update / Replace / Read / No-op 及其降级顺序可进一步阅读 docs/planning-behaviors.md它与本文描述的 Provider 协议函数共同构成了“默认行为 特殊行为”的完整规划模型。需要特别强调的是资源实例变更涉及的所有操作对象都遵循所选资源类型的 schema即GetProviderSchema返回的资源类型描述。无论是配置、先验状态还是计划状态其对象形状都必须与当前 schema 兼容这正是后面UpgradeResourceState存在的原因之一。生命周期中的各类状态对象在进入协议函数之前先明确整个流程中出现的每一种对象值它们之间的关系是理解后续契约的基础对象含义关键性质Configuration配置用户在配置中写下的值经过自动类型转换以匹配资源类型 schema 之后的对象未定义的属性为null若参数值来源于其他资源实例的未知结果则该值也可能是未知unknownPrior State先验状态Provider 对最近一次读取时远端对象状态的表示是ReadResource的输出也是下一次PlanResourceChange的输入之一Proposed New State提议的新状态OpenTofu Core 用内置逻辑对 Configuration 与 Prior State 做初步合并的结果作为 Provider 规划的起点核心逻辑围绕 schema 中标为computed的属性展开见下文Initial Planned State初始计划状态规划阶段plan产出的计划结果基于可能不完整的配置可能含未知值创建Final Planned State最终计划状态apply 阶段依赖全部就绪后再次规划的结果此时配置应已完全已知New State新状态Provider 在 apply 阶段对远端系统实际修改后产出的结果必须完全已知因为它代表系统实际状态Previous Run State上次运行状态上一次运行 New State 的原样副本不含 OpenTofu 之外发生的变更可能符合旧版 schema与当前 schema 不兼容Upgraded State升级后的状态由 Previous Run State 经 Provider 指定的升级逻辑转换而来数据结构符合当前 schema但语义上仍是“上次运行结束时的远端系统”Import ID / Import Stub State导入既有对象到 OpenTofu 管理时的输入与占位状态属于导入流程的专有概念后文专节讲解其中几个容易混淆的点值得展开“未知值unknown”的使用时机远端系统在 apply 阶段才决定的值、或由其他尚未知晓的资源实例配置值推导出的值Provider 必须以未知值标记在计划状态对象中。未知值的最终落定发生在 apply 阶段。Computed 属性的合并规则OpenTofu Core 在构造 Proposed New State 时如果配置中的属性值非null则取配置值否则回退到 Prior State 中的值。这正是帮助 Provider“保留其先前选定的值”的关键机制。源码层面schema 中Required、Optional、Computed三个布尔标志的组合语义定义在 internal/configs/configschema/schema.goRequired配置中必须提供非null值Optional可提供也可省略省略时默认为nullComputed不得在配置中设置值只能由 Provider 决定通常来自远端 API 返回值OptionalComputed用户可自行设置若配置使其为null显式或省略则由 Provider 决定最终值例如提供默认值。其余组合都是无效的没有意义。Provider 协议 API 函数总览以下五个加导入共六个函数是 Provider 协议中负责“规划并应用一次变更”的核心 API。本文按协议版本 6 的函数命名展开协议版本 5 拥有同一组操作只是个别名称略有差异——OpenTofu 恰好利用版本 6 的机会整理了此前一些别扭的命名。对应的.proto定义可参考 docs/plugin-protocol/tfplugin6.1.proto 与 docs/plugin-protocol/tfplugin5.0.proto。历史原因原始 OpenTofu SDKterraform-plugin-sdk系列在违反某些假设时豁免错误信息但违反这些契约仍往往会在下游引发连锁错误因为 OpenTofu 的整个工作流都依赖这些契约被满足。术语约定下文中的“属性attribute”指资源类型 schema 中描述的具名属性schema 还可能包含嵌套块nested blocks其中含有自己的属性集合所有约束对嵌套属性递归适用。在深入每个函数之前先看 OpenTofu Core 一侧的调用入口。Core 与 Provider 之间的 gRPC 桥接层实现在 internal/plugin/grpc_provider.go其中GRPCProvider实现了 internal/providers 包定义的providers.Interface每个方法负责把 Core 内部的请求/响应类型与协议消息互相转换。与本文主题对应的桥接方法包括ValidateResourceConfiginternal/plugin/grpc_provider.go、UpgradeResourceStateinternal/plugin/grpc_provider.go、ReadResourceinternal/plugin/grpc_provider.go、PlanResourceChangeinternal/plugin/grpc_provider.go、ApplyResourceChangeinternal/plugin/grpc_provider.go与ImportResourceStateinternal/plugin/grpc_provider.go。规划引擎对 Provider 的实际调用则位于 internal/engine/planning 目录。ValidateResourceConfigschema 之外的定制校验ValidateResourceConfig仅接收Configuration对象并针对其属性值返回错误或警告诊断diagnostics。它是 Provider 对 schema 施加自定义校验规则的机会用于表达仅靠 schema 无法表达的约束。需要遵守的要点理论上 Provider 可以在这里制定任何规则但实践中应避免对未知值报告错误。OpenTofu Core 会在求值的不同阶段多次调用本函数并保证最终会以完全已知的配置调用一次使 Provider 有机会补抓那些在规划阶段最初未知、后来才暴露出来的问题。若 Provider 打算在某个 optionalcomputed 属性于配置中为null时选择默认值则它必须容忍该属性在配置中为未知值才能在后来的 plan/apply 阶段获得选择默认值的机会。校验步骤本身不产出新对象因此不能修改用户提供的配置。桥接实现会在调用前先通过GetProviderSchema取得 schema并将配置以msgpack.Marshal编码后随ValidateResourceTypeConfig_Request发送给插件进程随后把返回的诊断转换为 Core 内部格式见 internal/plugin/grpc_provider.go。PlanResourceChange预测一次 apply 的效果PlanResourceChange的目的是预测后续 apply 的大致效果让 OpenTofu 能够向用户渲染计划diff并将可预测的那部分结果通过配置表达式向下游传播。该操作可以基于Configuration、Prior State、Proposed New State的任意组合做决策只要其结果满足以下硬性约束配置中非null的属性要么精确保留配置值要么返回先验状态中的对应值。后者适用于“变更不具备功能意义”的情形例如 JSON 字符串仅发生了空白位置变化的序列化差异。schema 中标为 computed 且配置中为null的属性Provider 可以设为预期类型的任意值。computed 属性若在计划新状态中有已知值Provider 在ApplyResourceChange返回的新状态中必须保持该值不变否则必须返回错误解释为何变化。若最终结果将在 apply 阶段确定则应在计划中将其设为未知值。关键机制PlanResourceChange每次运行会被调用两次。第一次调用在规划阶段在 OpenTofu 向用户打印 diff 请求确认之前。此刻任何变更都尚未应用配置中可能包含未知值——它们是对“来源于其他资源实例未知值”的表达式的占位符。第一次调用的结果即Initial Planned State。用户接受计划后apply 阶段会再次调用PlanResourceChange这次保证传入的Configuration 完全已知上游依赖的值已经纳入考虑。第二次调用的结果即Final Planned State。OpenTofu Core 会对比最终与初始计划状态除上述约束外还强制以下两条Initial Planned State 中已知的值在 Final Planned State 中必须完全一致Initial Planned State 中未知的值第二次可以保持未知也可以取任何符合该未知值类型约束的已知值。Final Planned State正是接下来传给ApplyResourceChange的对象。从源码看规划引擎对资源实例的完整处理包括刷新、升级状态、调用 PlanResourceChange位于 internal/engine/planning/plan_managed.go。此外Provider 也可以在响应PlanResourceChange时激活某些“Provider 驱动的特殊行为”例如将 Update 升级为 Replace、或将等价序列化差异归类为 No-op这些行为模型在 docs/planning-behaviors.md 的 “Provider-driven Behaviors” 一节有系统归纳。ApplyResourceChange让远端系统与计划一致ApplyResourceChange负责调用远端系统使远端对象与Final Planned State一致。操作过程中Provider 应决定 Final Planned State 中所有未知属性的最终值从而产出New State对象。它同时接收Prior State以便在远端 API 支持部分更新partial update时通过检测未变化的部分实现对特定区域的更“外科手术式”的精准修改。New State 的契约约束Final Planned State 中已知的属性新状态中必须完全相同。特别地如果远端 API 对同一值返回了不同序列化形式Provider必须保留用户在配置中书写的形式不得返回 Provider 归一化后的形式。Final Planned State 中未知的属性新状态中必须取符合该未知值类型约束的已知值New State 中不允许存在任何未知值。完成每个资源实例的ApplyResourceChange以及相关的记账工作后一次 OpenTofu 运行即告完成OpenTofu Core 将New State保存进整个配置的状态快照供下次运行使用。用户再次运行 OpenTofu 时该New State会原样成为Previous Run State并进入UpgradeResourceState的处理流程。UpgradeResourceState跨版本 schema 的兼容桥梁由于资源实例的状态值会从一个运行持久化到下一个运行保存在状态快照中OpenTofu Core 必须处理这样的情况用户自上次运行后升级到了更新版本的 Provider而新 Provider 对该资源类型的 schema 与旧版本不兼容。处理方式OpenTofu Core 首先调用UpgradeResourceState并将Previous Run State以原始形式传入——在当前协议版本中即状态快照中保存的原始 JSON 数据结构。OpenTofu Core无法访问 Provider 资源类型的历史 schema 版本因此数据解码必须由 Provider 本函数自己完成。Provider 可以使用任何合适的逻辑将数据形状更新为符合当前 schema。虽然 Core 无法强制但 Provider只应改变数据结构形状不应改变数据语义尤其不应试图把 OpenTofu 之外对远端对象的修改塞进状态数据。函数返回Upgraded State它携带与 Previous Run State 相同的信息但以符合当前 schema 版本的方式表达从而使 Core 能够在后续步骤中完整地与之交互。源码佐证规划引擎在正式规划前会先比较prevRoundState.SchemaVersion与当前 schema 版本——若状态版本比当前 Provider 还新会直接报错 “Resource instance managed by newer provider version”否则构造providers.UpgradeResourceStateRequest携带TypeName、旧版本号Version与RawStateJSON并调用providerClient.UpgradeResourceState随后用checkAndMarshalUpdatedState校验并序列化升级结果见 internal/engine/planning/plan_managed.go。这印证了文档中“由 Provider 自行解码旧数据”的设计——Core 只负责把原始 JSON 原样交还 Provider。ReadResource尽力而为的漂移检测尽管 OpenTofu 通常期望对绑定到资源实例的远端对象拥有独占控制权但实践中用户可能在 OpenTofu 之外修改这些对象使 OpenTofu 的记录变得过时。ReadResource要求 Provider 尽最大努力检测这类外部变更并将其描述出来使 OpenTofu Core 能以最新的 Prior State作为下一次PlanResourceChange的输入。这始终是“尽力而为”的操作原因包括某些远端对象具有只写write-only属性无法得知远端系统当前存储的值底层 API 可能出现了当前 Provider 版本不知道如何询问的新特性。OpenTofu Core 期望 Provider 对每个属性仔细区分以下两种情形归一化Normalization远端 API 返回的数据与 Prior State 记录的形式不同但含义未变。此时 Provider 应返回 Prior State 中的精确值保留用户在配置中书写的形式从而避免在配置的其他位置引发不必要的级联变更。漂移Drift远端 API 返回的数据与 Prior State 记录的内容实质不同说明远端系统行为已不再匹配配置此前的要求。此时 Provider 应返回远端系统的值丢弃 Prior State 中的值。当 Provider 这样做时若 OpenTofu Core 判定检测到的变更可能是下游资源实例某个计划动作的原因它可能会向用户报告这是一次在 OpenTofu 之外进行的变更。该操作返回的Prior State将作为下一次PlanResourceChange的输入——至此闭环完成流程重新开始。配置中嵌套块Nested Blocks的处理嵌套块是仅存在于配置中的构造因此块的数量不能在规划或应用过程中动态改变配置中出现的每个块都必须在计划新状态与新状态中有对应的嵌套对象否则 OpenTofu Core 会报错。如果 Provider 想要报告 apply 期间隐式创建的、由嵌套块所表示的子对象类型的新实例例如计算实例在未显式指定时被自动创建了默认网络接口则必须通过嵌套块旁边单独的computed属性来完成。这通常是一个对象列表或对象映射其中混合包含配置中嵌套块描述的对象以及远端系统隐式创建的额外对象。协议版本 6 还引入了**结构类型属性structural-typed attributes**的新概念它采用属性式语法却按嵌套块式语义解释。使用结构类型属性的 Provider 必须遵循与相同嵌套模式下嵌套块类型一致的规则。导入行为Import Behavior与 ImportResourceState主变更生命周期关注的是整个生命周期都由 OpenTofu 驱动包括对象的初始创建的对象。作为帮助用户以 OpenTofu 替代既有流程或软件的辅助能力OpenTofu 还支持收养adopting既有的对象使其纳入 OpenTofu 的管理而无需先重建。使用该功能时用户需要提供希望将既有对象绑定到的资源实例地址既有对象标识符的字符串表示其语法由 Provider 按资源类型自行定义——这就是Import ID。导入过程把用户的Import ID兑换成一个特殊的Import Stub State它扮演Previous Run State的占位符仿佛该对象就是由一次先前的 OpenTofu 运行创建的一样。ImportResourceState 契约ImportResourceState接收用户的Import ID用它验证给定对象是否确实存在若存在则检索足够的数据以产出Import Stub State。OpenTofu Core 在ImportResourceState返回后总会将返回的Import Stub State交给常规的ReadResource操作继续处理。因此实际上Provider 可以只填充ReadResource完成工作所需的最小属性子集让常规函数负责填充其余数据以匹配远端系统当前的实际设置。与ReadResource只是尽力而为地检测外部变更的原因相同Provider 可能无法为所有资源类型完整支持导入。此时 Provider 开发者必须在以下两种方案中选择仅执行部分导入Provider 可以在ImportResourceState与后续ReadResource均完成后将某些属性在 Prior State 中保持为null。这种情况下用户可在配置中提供缺失值促使下一次PlanResourceChange计划将该值更新为配置值。Provider 的PlanResourceChange必须准备好处理属性在Prior State 中为null的情况并妥善应对。返回错误说明为何无法导入。这是最后手段因为它会使用户无法将既有对象纳入 OpenTofu 管理。不过如果某个对象的设计不适合导入明确诚实地告知用户必须随收养过程一起替换该对象往往比执行一次“导入后 OpenTofu 却无法有意义地管理该对象”的导入提供更好的用户体验。将生命周期串起来一次典型运行的完整时序综合以上所有环节一次典型的tofu plantofu apply运行中单个受管资源实例经历的协议调用序列可以归纳如下校验ValidateResourceConfig以配置为输入进行定制校验可能在求值各阶段被多次调用最终保证一次完全已知的调用。状态升级若上次运行状态的 schema 版本低于当前 schemaUpgradeResourceState将 Previous Run State原始 JSON转换为 Upgraded State。刷新ReadResource尽力检测 OpenTofu 之外的外部变更产出最新的 Prior State。第一次规划PlanResourceChange配置可能含未知值产出 Initial Planned StateOpenTofu 将其渲染为用户可见的 diff。用户确认后apply 阶段再次调用PlanResourceChange配置完全已知产出 Final Planned State并校验其与 Initial Planned State 的已知/未知一致性。应用ApplyResourceChange使远端系统匹配 Final Planned State落定所有未知值产出完全已知的 New State。持久化New State 保存入状态快照成为下一次运行的 Previous Run State若涉及导入场景则第 1 步之前还会先经过ImportResourceState→ReadResource的导入子流程。对应 OpenTofu 的命令行入口tofu plan与tofu apply的实现分别位于 internal/command/plan.go 与 internal/command/apply.go底层状态对象含 schema 版本与原始 JSON的定义可查看 internal/states 目录。想要从 Provider 开发者视角验证上述契约行为的可以在 internal/tofu 目录的大量测试例如 internal/tofu/context_apply2_test.go中看到通过 mock Provider 的PlanResourceChangeFn等手段对协议函数的模拟与断言。总结理解生命周期是写好 Provider 的前提资源实例变更生命周期是 OpenTofu 一切受管资源行为的骨架。无论是开发新 Provider、排查计划与预期不符的问题还是深入理解 OpenTofu Core 的规划引擎把握以下要点都至关重要环上的每一种状态对象都有明确语义尤其要区分“可能含未知值”的计划状态与“必须完全已知”的新状态PlanResourceChange每轮运行调用两次初始计划与最终计划两次之间的已知/未知一致性由 Core 强制校验ApplyResourceChange返回的新状态必须与 Final Planned State 的已知值完全一致未知值必须落定UpgradeResourceState是跨 Provider 版本升级的兼容桥梁数据解码必须由 Provider 自己完成ReadResource区分归一化与漂移两种情形是保持配置、状态与远端系统三方一致的关键嵌套块数量不可在规划/应用期间改变隐式创建的对象需通过旁边的 computed 属性表达导入通过ImportResourceState产出 Import Stub State再交由常规ReadResource补全且允许“部分导入 后续计划补齐”的降级路径。掌握这份契约你便拥有了在 Provider 协议层docs/plugin-protocol 下的.proto文件与 OpenTofu Core 规划引擎internal/engine/planning之间自由穿梭的完整地图。【免费下载链接】opentofuOpenTofu lets you declaratively manage your cloud infrastructure.项目地址: https://gitcode.com/gh_mirrors/op/opentofu创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进