ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

OpenTofu 核心架构解析:从 CLI 命令到图执行的完整请求链路

OpenTofu 核心架构解析:从 CLI 命令到图执行的完整请求链路 OpenTofu 核心架构解析从 CLI 命令到图执行的完整请求链路【免费下载链接】opentofuOpenTofu lets you declaratively manage your cloud infrastructure.项目地址: https://gitcode.com/gh_mirrors/op/opentofu本篇技术指南以 OpenTofu 官方架构文档docs/architecture.md为骨架系统讲解 OpenTofu Core 的主要组件以及一次用户命令如tofu plan、tofu apply在数据与请求层面如何在各组件间流转。阅读完本文你将掌握从 CLI 入口、Backend、配置加载器、状态管理器到图构建器、图遍历与顶点求值的完整执行链路并了解count等动态展开场景背后的子图机制为深入阅读internal/源码打下基础。一次命令的完整请求流OpenTofu Request FlowOpenTofu 的架构文档用一张总览图即上文配图概括了一次用户命令在 Core 中的执行近似流程。图中每个实线方框代表一个独立子系统均在下文有对应章节展开。一个关键前提是这条完整链路并不适用于所有命令它主要适用于 OpenTofu 的核心工作流命令tofu plan、tofu apply以及少数其他命令。诸如tofu version、tofu fmt之类的命令走的是更轻量的路径。整条链路可以概括为四个阶段CLI 解析阶段命令实现读取并解析命令行参数、选项与环境变量构造出一个描述要做什么的backend.Operation对象Backend 调度阶段Operation 被交给当前选中的 backend由具备执行能力的 backendEnhanced真正执行该操作或由localbackend 包装后在本地进程内执行Core 执行阶段localbackend 用状态管理器取回当前状态、用配置加载器加载配置构造tofu.Context并调用Plan/Apply等方法图驱动阶段tofu.Context使用图构建器构造依赖图再通过图遍历器按必须在其后发生的依赖边顺序求值每个顶点。CLI 层command包与命令分发每次用户运行tofu程序时除根包中少量初始化引导逻辑图中未展示外执行会立即转入internal/command包中的某个命令实现。用户面对的命令名如plan、apply与command包中对应类型的映射关系记录在 cmd/tofu/commands.gomain包中。该文件同时承担了更底层的引导职责commandMain会创建工作目录对象workdir.NewWorkdirExplicit处理-chdir与TF_DATA_DIR、构造 provider 源providerSource、建立 CLI 配置cliconfig.Config、服务发现disco.Disco等最终通过command.RootCommander与commandToCli将命令树组装为 urfave/cli 的命令结构并运行。对于plan/apply这类工作流命令命令实现的核心职责是读取并解析所需的命令行参数、选项与环境变量用它们产出一个描述待执行动作的backend.Operation对象。一个operation由以下要素构成要素说明动作类型Type要执行的动作如 plan、apply见 internal/backend/operation_type.goWorkspace动作将在其中执行的 workspace 名称Variables该动作使用的根模块输入变量ConfigDir对于 plan 操作包含配置根模块的目录路径PlanFile对于 apply 操作要应用的已保存计划文件其他选项Targets-target地址、Excludes、ForceReplace、AutoApproveforce 标志、PlanMode、PlanOutPath等这个 Operation 随后被传递给当前选中的 backend。每个 backend 名称都对应 internal/backend/init/init.go 中backend.Init初始化的一张映射表里backend.Backend接口的一个实现。Backend 层状态存储与操作执行的分工backend 决定了 OpenTofu 将状态快照存储在哪里。但需要特别强调的是文档中的一条重要澄清backend 执行操作只是架构实现细节而不是 backend 的通用特性。作为 OpenTofu 特性术语backend 指的是决定状态快照存放位置的插件——在 internal/backend/init/init.go 中可以看到当前硬编码注册的全部 backendbackends map[string]backend.InitFn{ local: func(enc encryption.StateEncryption) backend.Backend { return backendLocal.New(enc) }, remote: func(enc encryption.StateEncryption) backend.Backend { return backendRemote.New(services, enc) }, // Remote State backends. azurerm: ... backendAzure.New(enc), consul: ... backendConsul.New(enc), cos: ... backendCos.New(enc), gcs: ... backendGCS.New(enc), http: ... backendHTTP.New(enc), inmem: ... backendInmem.New(enc), kubernetes: ... backendKubernetes.New(enc), oss: ... backendOSS.New(enc), pg: ... backendPg.New(enc), s3: ... backendS3.New(enc), // Terraform Cloud backendcloud 包内部实现细节 cloud: func(enc encryption.StateEncryption) backend.Backend { return backendCloud.New(services, enc) }, }其中只有默认的local、remote和cloud三个 backend 会执行操作其余都是纯远程状态存储 backend。backend/init还维护了RemovedBackends表记录了artifactory、etcd、etcdv3、manta、swift等已被移除的旧后端名称及提示信息。由于 backend 的 API 依赖复杂结构通过插件系统支持不现实因此 backend 是硬编码进 OpenTofu 的源码注释明确说明想要自定义 backend 只能通过重新编译实现。接口层面见 internal/backend/backend.gobackend.Backend最小接口包含ConfigSchema返回配置结构描述、PrepareConfig校验配置值并插入缺省值、Configure一次性设置后端字段、StateMgr按 workspace 返回状态管理器、DeleteWorkspace、Workspacesbackend.Enhanced在Backend之上增加Operation执行 refresh/plan/apply 等操作非阻塞通过返回的RunningOperation阻塞等待完成并负责为运行期间使用的状态加锁和ServiceDiscoveryAliasesbackend.Local增加LocalRun为console、import、graph等需要直接访问配置、变量等更多数据的命令提供支持。大多数 backend 并不实现Enhanced接口因此command包会把这些 backend 包装进一个local.Local实例使操作在tofu进程内部本地执行。这就是为什么你可以在不配置任何远程后端的情况下直接运行tofu plan——所有远程状态 backend 的操作最终都借道localbackend 完成。localbackend本地执行的枢纽localbackend 在执行操作时依次完成以下工作使用一个状态管理器state manager取回操作所指定 workspace 的当前状态。如果直接使用 local backend则该管理器是statemgr.Filesystem如果包装了其他 backend则使用该 backend 提供的实现使用配置加载器加载操作中指定的配置并完成初始处理/校验结合上述两者及 Operation 中的其他设置构造tofu.Context——真正执行 OpenTofu 操作的核心对象在 Context 上调用相应方法开始执行例如Context.Plan或Context.Apply这些方法随后会使用图构建器构造依赖图。localbackend 的实现在 internal/backend/local/ 目录下其中backend_plan.go、backend_apply.go、backend_refresh.go分别对应 plan、apply、refresh 三类操作的编排逻辑。配置加载器Configuration Loader顶层配置结构由 internal/configs 包中的模型类型表示整个配置根模块加上所有后代模块用一个configs.Config对象表示。configs包包含一些构造配置对象的底层功能但主要入口在子包 internal/configs/configload 的configload.Loader中。加载器负责处理所有子模块安装的细节在tofu init期间并在 backend 加载配置时再次定位这些模块。它接收根模块的路径递归加载所有子模块最终产出一个代表整个配置的configs.Config。OpenTofu 期望配置文件使用 OpenTofu 语言编写这是一种构建在 HCL 之上的 DSL。配置中有一部分内容在构建并遍历图之前无法解释因为它们依赖配置中其他部分的结果因此这部分配置会保留为低层的 HCL 类型hcl.Body与hcl.Expression允许 OpenTofu 在更合适的时机即顶点求值阶段再解释它们——这正是后文表达式求值一节存在的原因。状态管理器State Manager状态管理器负责为特定 workspace 存储和检索 OpenTofu 状态快照。每个管理器都是 internal/states/statemgr 包中接口的某种组合的实现绝大多数实用的管理器都实现了statemgr.Full描述的全部操作且由某个backend提供。那些较小的接口主要存在于其他函数签名中用于显式说明该函数可能对状态管理器执行哪些动作从架构角度看几乎没有理由编写一个不实现全部statemgr.Full的状态管理器。默认实现statemgr.Filesystem由localbackend 使用负责大多数用户熟悉的本地文件terraform.tfstateOpenTofu 用户在切换到远程状态前通常从它开始其他statemgr.Full实现用于实现远程状态各自通过所属 backend 对应的远程网络服务保存和检索状态。状态管理器以states.State对象的形式接受和返回状态快照。对象如何被序列化和存储完全由状态管理器决定不过在写作本文时所有状态管理器都使用相同的 JSON 序列化格式将序列化后的 JSON 字节存入某种任意的 blob 存储中。值得一提的配套设施是states.SyncState见 internal/states/sync.go由于图遍历会并发求值多个顶点共享的states.State对象需要并发保护绝大多数情况下代码使用SyncState这个辅助包装器来安全地实现共享状态的并发读写。图构建器Graph Builder图构建器由tofu.Context的方法如Plan或Apply调用用于产出表示该操作所需步骤及步骤间依赖关系的图。在大多数情况下OpenTofu 图的**顶点vertices**各自代表配置中的某个具体对象或由这些配置对象派生出的东西。例如配置中的每个resource块在 plan 图中都有一个对应的GraphNodeConfigResource顶点。注意OpenTofu Core 的术语使用并不完全一致在很多地方把图的vertices也称为nodes二者描述的是同一个概念。图中的**边edges**表示必须在其后发生happens after的关系定义了顶点被求值的先后顺序确保例如某个资源在其依赖的资源之后才被创建。每个操作都有自己的图构建器因为图构建过程各不相同plan 操作需要直接从配置构建图apply 操作则从被应用的计划所描述的一组变更中构建图。所有图构建器都基于一系列transforms工作它们是tofu.GraphTransformer接口的实现该接口只有一个方法Transform(context.Context, *Graph) error接收一个图并任意修改它。由于实现只需接收图并按其需要修改可用的 transform 种类非常丰富几个重要示例Transform作用ConfigTransformer为配置中的每个resource块创建一个图顶点StateTransformer为状态中当前跟踪的每个资源实例创建一个图顶点ReferenceTransformer分析配置找出资源与其他对象之间的依赖关系并为这些依赖创建必要的 happens after 边ProviderTransformer将每个资源/资源实例与恰好一个 provider 配置关联实现 provider 继承规则并创建 happens after 边确保 provider 在其所属资源被操作前完成初始化还有更多不同的图 transform可通过阅读不同图构建器的源码发现每个图构建器根据所执行操作的需求使用其中不同的子集。图构建的产物是一个tofu.Graph它可以交给图遍历器进一步处理。图遍历Graph Walk遍历图的过程会以一种尊重图中 happens after 边的方式访问每个顶点。遍历算法本身实现在底层 internal/dag 包中DAG 是Directed Acyclic Graph有向无环图的缩写具体是AcyclicGraph.Walk。不过OpenTofu 层面有意思的遍历功能实现在tofu.ContextGraphWalker中它在图遍历期间实现了一小组更高级的操作EnterPath对配置中的每个模块调用一次接收一个模块地址返回一个在该模块内跟踪对象的tofu.EvalContext。tofu.Context是整个操作的全局上下文而tofu.EvalContext是单个模块内处理的上下文也是各模块命名空间保持隔离的主要手段。每个顶点都被求值求值顺序保证尊重 happens after 边。如果可能图遍历算法会并发求值多个顶点因此顶点求值代码必须谨慎使用 mutex 等并发原语来协调对共享对象如states.State的访问。顶点求值Vertex Evaluation图遍历期间对每个顶点采取的动作称为execution执行。执行会运行一系列对该顶点类型有意义的具体动作。例如plan 操作中对代表某个资源实例的顶点求值包含如下高层步骤从EvalContext中取回该资源关联的 provider。由于资源节点与 provider 节点之间存在 happens after 边provider 此时应已由它自己的图顶点提前初始化完成从状态中取回与当前被求值的资源实例相关的部分求值配置中为该资源给出的属性表达式。这通常涉及取回其他资源实例的状态以便将它们的值复制或转换为当前实例的属性——这一协调由EvalContext完成将当前实例状态与资源配置一起交给 provider要求 provider 产出表示状态与配置差异的instance diff实例差异将该 instance diff 保存为本次操作正在构造的计划的一部分。Execute 与 EvalContext顶点的每个执行步骤都是tofu.GraphNodeExecutable接口的实现该接口只有一个Execute(context.Context, EvalContext, walkOperation) tfdiags.Diagnostics方法。与图 transform 类似这些实现的行为差异很大图 transform 可以对图采取任何动作而Execute实现可以对EvalContext采取任何动作。实际处理区别于测试中使用的tofu.EvalContext实现是tofu.BuiltinEvalContext它通过EvalContext接口方法提供对插件、当前状态和当前计划的协调访问。典型的Execute实现包括NodePlannableResourceInstance.Execute——处理 plan 操作NodeApplyableResourceInstance.Execute——处理主 apply 操作NodeDestroyResourceInstance.Execute——处理主 destroy 操作。一个顶点必须成功完成后图遍历才会开始对具有 happens after 边的其他顶点求值。求值可能因一个或多个错误而失败此时图遍历被中止错误返回给用户。表达式求值Expression Evaluation对大多数顶点类型而言顶点求值的一个重要部分是求值与该顶点关联的配置块中的任何表达式——这补齐了配置加载器未处理的那部分配置即前文提到的保留为低层 HCL 类型的部分。表达式求值的高层过程为分析依赖分析配置表达式确定它们引用哪些其他对象。例如aws_instance.example[1]引用配置中resource aws_instance example块创建的某个实例。该分析由lang.References执行或更常使用其辅助包装器lang.ReferencesInBlock/lang.ReferencesInExpr取回被引用对象的数据从状态中取回被引用对象的数据创建 HCL 求值代码可引用的值查找表准备内置函数表准备内置函数表供 HCL 求值引用执行 HCL 求值让 HCL 对每个属性的表达式hcl.Expression对象对照数据和函数查找表进行求值。实际中步骤 2 至 4 通常用lang.Scope上的方法一次性完成最常用的是lang.EvalBlock或lang.EvalExpr。表达式求值产生一个以cty.Value表示的动态值。这个 Go 类型代表 OpenTofu 语言中的值此类值最终会被传递给 provider 插件。子图Sub-graphscount的动态展开机制某些顶点在求值步骤完成后还有一个特殊行为顶点实现会获得机会构建另一个独立的图该图将作为该顶点求值的一部分被遍历。最典型的例子是resource块设置了count参数的情况。此时plan 图最初为每个resource块只包含一个顶点但该图随后会动态展开产生一个包含count所请求的每个实例各自顶点的子图。也就是说aws_instance.example的子图可能包含aws_instance.example[0]、aws_instance.example[1]等顶点。这是必要的因为count参数可能引用其他对象而这些对象的值在构造主图时尚未可知但在主图中其他顶点求值期间会变为已知。这个特殊行为适用于实现了tofu.GraphNodeDynamicExpandable接口的顶点对象该接口只有一个DynamicExpand方法返回将被视为接收节点动态子图的新图。这样的顶点拥有自己嵌套的图构建器、图遍历和顶点求值步骤行为与主图的对应章节描述相同区别仅在于构造子图时使用了哪些图 transform以及子图中节点适用哪些求值步骤。总结用一张时序图串联全文将全文内容串联起来一次tofu plan的典型旅程是tofu plan │ (cmd/tofu 引导 command 包解析参数) ▼ command 构造 backend.Operation动作plan、workspace、变量、ConfigDir… │ ▼ 当前 backend 被包装为 local.Local除非本身是 local/remote/cloud │ ▼ local backendstatemgr 取回状态 configload 加载配置 │ ▼ 构造 tofu.Context → 调用 Context.Plan │ ▼ 图构建器ConfigTransformer/StateTransformer/ReferenceTransformer/ProviderTransformer… ▼ tofu.Graph → dag.AcyclicGraph.Walk 并发遍历ContextGraphWalker / EvalContext ▼ 每个顶点 GraphNodeExecutable.Execute → 表达式求值lang.Scope→ provider 产出 instance diff ▼ (cout 资源动态展开子图 → 对每个实例求值) ▼ 生成 plans.Plan 返回给命令层展示这套架构的最大价值在于模块间清晰的职责边界CLI 只负责把用户意图翻译成 Operationbackend 只负责状态存储与操作编排图构建器负责把配置/状态/引用关系折叠成可执行的有向无环图遍历器与求值器负责并发、安全地把每个顶点翻译成对 provider 的实际调用。理解这条链路后再阅读internal/tofu下context_plan.go、context_apply.go、transform.go、execute.go以及internal/dag、internal/lang等源码就能按图索骥、事半功倍。【免费下载链接】opentofuOpenTofu lets you declaratively manage your cloud infrastructure.项目地址: https://gitcode.com/gh_mirrors/op/opentofu创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表