深入解析 Terraform Stacks 功能实现:从包结构到运行时求值架构 深入解析 Terraform Stacks 功能实现从包结构到运行时求值架构【免费下载链接】terraformTerraform enables you to safely and predictably create, change, and improve infrastructure. It is a source-available tool that codifies APIs into declarative configuration files that can be shared amongst team members, treated as code, edited, reviewed, and versioned.项目地址: https://gitcode.com/GitHub_Trending/te/terraformTerraform Stacks 是在一棵或多棵 Terraform 模块树之上构建的编排层orchestration layer它用独立的.tfcomponent.hcl语言描述组件、输入输出与内嵌子栈并规划、应用整个组件集合的变化。本文以仓库 internal/stacks/README.md 为骨架结合 运行时内部架构文档 与具体源码逐层梳理 Stacks 各功能包的分工、配置与运行时对象模型、求值阶段、表达式求值链路以及应用阶段的调度原理帮助你从使用过模块平滑迁移到理解 Stacks 实现。Terraform Stacks 在整个仓库中的定位在 go.mod 对应的大仓库中顶层已有服务于单个 Terraform 模块或模块树的整套机制例如 internal/addrs对象寻址、internal/configs模块语言加载解析与 internal/terraform模块运行时。internal/stacks目录下全部 Go 包共同实现Terraform Stacks 功能是上述机制面向 Stacks 语法的平行宇宙。从包级文档 doc.go 可以看到Stacks 语言使用.tfcomponent.hcl以及 JSON 变体.tfcomponent.json描述一组要一起规划并应用的组件component。它刻意与主 Terraform 模块语言部分相似但在 Stacks 仍较新时保持独立实现以便二者各自演进。这与 stacks 顶层 README 的定位描述一致Stacks 运行时包装了模块运行时因此 Stacks 的地址类型会复用更通用的模块地址类型。┌─────────────────────────────────────────────────────────────┐ │ Terraform Stacks │ │ 编排层在 0..N 棵 Terraform 模块树之上调度组件变更 │ ├─────────────────────────────────────────────────────────────┤ │ stackconfig Stacks 语言的加载 / 解析 / 静态解码 │ │ stackaddrs Stacks 专用寻址构建于 addrs 之上 │ │ stackplan/ Stacks 版本的 plan 与 state 模型、序列化 │ │ stackstate │ │ stackruntime 运行时对比期望态与实际态生成 plan、执行 apply │ │ tfstackdata1 内部 protobuf schema实现细节外部不可依赖 │ └─────────────────────────────────────────────────────────────┘五个核心功能包及其职责顶层 README 明确了五个主要组件下面逐一展开并结合源码印证。stackaddrsStacks 专用寻址stackaddrs是顶层包addrs的 Stacks 专用对等物包含用于指称 Stacks 语言与运行时内部对象的类型以及在不同地址类型间导航的逻辑。由于 Stacks 运行时包装了模块运行时部分 Stacks 专用地址类型会组合addrs中更通用的地址类型。从目录文件可直观看到它的覆盖面stack.go、stack_call.go、component.go对应栈、栈调用与组件地址input_variable.go、local_value.go、output_value.go对应命名值provider_config.go负责 provider 配置寻址reference.go与referenceable.go定义表达式引用与可被引用对象的基础设施——后者是运行时表达式求值的核心接口之一见下文表达式求值。此外还有面向删除语义的removed.go与面向命令行的targetable.go。stackconfigStacks 语言前端stackconfig实现 Stacks 语言的加载、解析与静态解码对标模块语言中的configs包。其 doc.go 说明它解码.tfcomponent.hcl/.tfcomponent.json文件中声明的组件集合。包内关键文件见目录 internal/stacks/stackconfig包括stack.go、component.go、embedded_stack.go表达栈、组件与内嵌子栈等语言对象input_variable.go、local_value.go、output_value.go表达命名值声明provider_config.go、provider_requirements.go表达 provider 声明与约束removed.go表达被移除对象的声明配合forget/removed语义typeexpr/子目录将 HCL 类型表达式翻译为cty类型parser/提供目录遍历加载.tfcomponent.hcl解析由stackconfigtypes提供基础类型。配置样例可参考仓库中的测试夹具例如 basics-bundle/root/main.tfcomponent.hcl。一个组件源如 with-single-input 测试夹具中的 single-input.tf仍是普通模块写法terraform { required_providers { testing { source hashicorp/testing version 0.1.0 } } } variable id { type string default null nullable true # 未提供时由 provider 生成 ID } resource testing_resource data { id var.id value var.input }stackplan 与 stackstateplan 与 state 的 Stacks 变体stackplan与stackstate共同提供 Stacks 版本plan和state概念下的模型及序列化/反序列化逻辑。二者通过from_proto.go/from_state.go与内部 protobuf 数据tfstackdata1互转。stackplan的 doc.go 点出其本质它承载的是栈级元计划meta-plan——实践中等价于多个传统意义上plans.Plan对象的集合。之所以模型略有不同是因为 Stacks 运行时需要把传统 plan 拆成更小的片段以事件流的方式流向调用方但模型保持同构以便与主 Terraform 语言运行时所期望的模型互转。stackstate中值得特别关注的是 statekeys 子包Stacks 状态是可变数据结构其存储策略完全委托给调用 Terraform Core 的一方。为了允许 Core增量地输出状态更新而非反复返回整个数据集每个可独立更新的状态元素都带一个对调用方不透明、对 Core 有意义的追踪键tracking key。调用方只需做逐字符字符串比较即可判断某次更新描述的是全新对象还是既有对象的替换因此这些键的内容必须在 Terraform Core 各版本间保持稳定。从 statekeys 目录 可见其覆盖 components、resources、outputs、variables 等对象类别并支持多种解析策略key_parse.go与未知键处理策略unrecognizedkeyhandling_string.go。stackruntime全部动态行为所在stackruntime处理 Stacks 的运行时行为包括基于期望状态与实际状态对比生成 plan再应用该 plan。Stacks 语言的一切动态行为都集中于此。此外还包括状态刷新refresh_instance.go、顶层plan_refresh_test.go、验证validate.go、销毁apply_destroy_test.go与钩子/遥测hooks.go、telemetry.go。该包对外暴露的入口文件为plan.go、apply.go、validate.go底层实现主要委托给其内部包stackeval求值器核心并经由 hooks 子包将组件实例、资源实例等状态变化以回调形式通知外部调用方。tfstackdata1内部数据格式与对外 API 的边界tfstackdata1是内部 protobuf schema 的 Go 表示用于跨运行保留 plan 与 state 数据对应 tfstackdata1.proto。顶层 README 明确强调这些格式是实现细节外部调用方不允许依赖公开接口是经由兄弟目录 internal/rpcapi 实现的Terraform Core RPC API。也就是说Stacks 对外的语言、plan、state 能力都以 RPC 协议暴露tfstackdata1只是内部的持久化载体。深入栈运行时对象模型与求值架构stackeval 的 README 是本仓库中最详尽的 Stacks 运行时架构说明面向未来的维护者。以下内容均依据该文档与源码整理。与模块运行时的根本差异显式图 vs 隐式数据流图传统模块运行时的工作方式是先显式构造依赖图dependency graph再并发遍历该图访问每个节点并请它自我求值节点间通过名为EvalContext的共享可变数据结构协作读写 state、plan 等元数据。Stacks 运行时解决的是同一问题——把各类计算与副作用排成合适顺序——但采用不同方式依赖一张在求值过程中动态构造的隐式数据流图。求值器仍有一个全局上帝对象Main但它是通往对象树的入口每个对象只封装语言中某一种概念的数据数据在对象间通过方法调用与返回值流动而不是通过共享可变状态。配置对象Config与动态对象Dynamic的两层设计包中存在大量成对的类型分别表示配置中的静态对象与它的动态实例。例如InputVariableConfig直接表示.tfcomponent.hcl中的一个variable块InputVariable表示该对象可能产生的多个动态实例当它位于被for_each调用多次的子栈内部时。分层原则是静态类型负责静态校验类任务例如检查表达式是否引用了根本不存在的配置对象实例。其目标是尽可能多地用静态检查发现问题——这样可在验证阶段尽早反馈也避免同一问题在多个实例上被重复报告。动态类型则负责一切需要动态表达式求值、以及与外部系统交互的工作。例如为某个组件创建 plan 必然是动态的因为要调用 provider 执行可能与远端服务器联网的规划操作而一切使用 plan 结果的操作也传递性地变成动态操作。调用Call与实例Instancefor_each 的三段式拆解在 Config/动态 之外还有一层Call vs Instance的区分。StackCall、Component、Provider都表示配置中可动态产生子对象的动态对象本身而StackCallInstance、ComponentInstance、ProviderInstance表示由for_each展开出的具体实例。以组件为例栈调用与 provider 配置同理该过程拆成三层职责类型代表核心职责ComponentConfig配置里的component块校验组件声明本身是否合法与任何动态信息无关Component处于特定栈上下文中的某个组件块实例处理组件位于被for_each的子栈内导致的多重实例化求值组件自身for_each表达式汇总各实例结果为component.foo这类引用提供映射值ComponentInstance组件自身for_each展开出的某一个实例求值可引用each.key/each.value因而随实例变化的参数执行组件核心动态行为——创建 plan、应用 plan、报告结果对应源码见 component_config.go、component.go 与 component_instance.go以及同样的三件套 stack_call_config.go、stack_call.go、stack_call_instance.go。对象单例约定与异步追踪从Main可达的几乎每个对象都必须按单例对待因为它们携带进行中的异步工作与已完成异步工作的结果两类追踪信息。保证单例的职责被下放到子对象的管理父对象Main负责实例化主栈root stack的Stack对象并记住它而所有子栈被记录在根栈对象内部状态中由根栈保证多次调用间唯一。如此递归下去除Main外每个对象都恰好由一个管理父对象负责——破坏该约定会导致重复工作甚至结果不一致当相关工作不是纯函数时。为便于维护约定所有模型类型的新实例都由未导出的构造函数产生如newStack且每个构造函数只能在管理父对象内唯一的一处被调用newStack是特例因为主栈的管理父是Main其余栈的管理父是父Stack因此有两个调用点但互不越权。单例对象实际保存在管理父对象内的未导出 map 中通常在首次被其他调用方请求时惰性创建随后缓存复用。...Config类型通常按Main对象全局单例因为静态定义本就唯一动态类型实际只在单个求值阶段内单例——ComponentInstance在创建 plan与应用既有 plan时的行为不同。求值阶段Evaluation Phases与对象工厂每个Main对象通常只服务于一个求值阶段对外表现为调用哪个工厂函数NewForValidating返回处于ValidatePhase的Main只能做静态验证任何动态求值都会失败NewForPlanning返回处于PlanPhase的Main绑定特定 prior state 与规划选项NewForApplying返回处于ApplyPhase的Main绑定一个栈级 plan其中包含 prior state、planned changes、规划期间给定的输入变量值等NewForInspecting返回处于InspectPhase的Main是特殊阶段用于实现较少使用的工具类似 Stacks 版的terraform console只绑定 prior state 并直接从状态返回值不规划也不应用变更该阶段也很适合做不依赖外部副作用的单元测试。从 evalphase_string.go 可以确认EvalPhase的可比较常量集合NoPhase、ValidatePhase、PlanPhase、ApplyPhase、InspectPhase。由于当前每个Main只有一个求值阶段按阶段分别追踪单例池在技术上略显冗余但实现依然强制所有表达式求值操作显式声明所处阶段——这既是对那些难以排查 bug 的保险也为未来可能的同一Main混合多阶段工作预留空间。工厂函数分别实现在 main_validate.go、main_plan.go、main_apply.go、main_inspect.go。表达式求值两个接口与五步流程表达式求值是语言运行时中最重要的横切行为主入口为EvalExpr见 expressions.go另有EvalBody一次求值动态 body 中的所有表达式以及返回求值上下文信息的扩展EvalExprAndEvalContext供诊断消息使用。求值过程围绕两个关键概念EvaluationScope由可对内部表达式求值的对象实现。每个Stack实际上是全局作用域Component、StackCall、Provider等子对象则作为子作用域扩展全局作用域引入each.key、each.value、self等局部上下文。其职责是把stackaddrs.Reference已被解码的引用表达式翻译成一个实现Referenceable的对象对应 reference.go 与 stackeval 的 expressions.go。Referenceable可被表达式引用的对象实现的接口。例如var.foo应指向InputVariable对象因此InputVariable实现Referenceable来决定该引用应取的实际值。实现的职责非常简单针对特定EvalPhase返回一个cty.Value注入表达式作用域。例如Component在PlanPhase返回其 plan 的全部输出值而在ApplyPhase则改返回最终 state 中的输出值——阶段差异正是通过Referenceable的各实现体现的。整体求值流程EvalExpr共五步分析表达式集合找出全部 HCL 符号引用hcl.Traversal调用stackaddrs.ParseReference把引用提升为高层地址类型并包进stackaddrs.Reference。语法非法的引用在此失败但此步没有动态符号表无法发现对不存在对象的引用把stackaddrs.Reference交给调用方选定的EvaluationScope实现通过EvaluationScope.ResolveExpressionReference判断地址指向的对象是否真实声明是则返回该对象。此步负责拦截语法合法但指向未声明对象的引用可被引用的对象必须实现Referenceable对收集到的每个Referenceable对象调用ExprReferenceValue传入调用方的EvalPhase。该方法返回cty.Value若上游出错导致无法给出具体值应返回某种 unknown 值理想带类型约束退而求其次为cty.DynamicVal让下游求值足以继续、调用栈得以全部展开从而在顶层汇总全部错误诊断把所有收集到的值装配成形状合适的hcl.EvalContext挂上常规函数库最后让原始表达式在该上下文中自我求值。此步的失败对应表达式自身非法例如类型无法转换的数相加等。诊断传播Check 前缀与非前缀方法的配对约定对象间数据流大多按需发生若某component块引用var.foo那么求值该表达式时Component/ComponentInstance会经由表达式求值器间接地请InputVariable为variable foo产出值只有此刻InputVariable才开始求值该值这可能又涉及求值另一个表达式如此递归。因为请求流是动态的、且多个不同请求方可能经由不同调用路径索要同一结果返回错误/警告诊断时必须保证只沿一条返回路径传播避免同一诊断重复上报。解决手法是可能返回诊断的操作拆成一对方法——带Check前缀的负责传播诊断不带前缀的负责安全取用例如InputVariable同时有Value与CheckValue后者返回(cty.Value, tfdiags.Diagnostics)前者只是包装后者并丢弃诊断。该策略依赖两条不变式每个可能失败的操作在失败时都能产出某种惰性占位结果供所有依赖它的下游安全展开而不产生新错误个别极端情况允许产生少量、能提供比原始错误更多信息的附加错误作为兜底每个函数只有一条代码路径负责调用Check...变体其余调用方使用非前缀版本并接受偶尔拿到占位结果。这与其他 Terraform 组件的诊断处理方式明显不同需要后续维护时额外小心保持上述不变式但统一的命名约定让规则更容易学习与维护。实际调用Check...变体的唯一代码路径是下一节的walk路径。静态与动态两种 Walk保证每个对象都被访问由于多数结果只在被请求时才产出若配置中没有其他对象引用var.foo它就永远没机会自我求值并暴露声明/定义中的错误。因此除InspectPhase外每个主求值阶段都至少关联一次walk遍历从Main可达的整棵相关对象树对每个对象调用阶段专属方法。两个 walk 驱动器遍历不同的对象子集见 walk_static.go 与 walk_dynamic.go驱动器使用阶段访问对象静态 walkValidatePhase、PlanPhase仅访问Config后缀类型静态配置对象动态 walkPlanPhase、ApplyPhase访问主动态对象与Instance后缀的动态实例对象walk 驱动器决定需要访问的对象并逐一回调各阶段在回调中调用不同方法ValidatePhase调用Validatable接口的Validate方法——只允许返回诊断不得有外部可见副作用PlanPhase调用Plannable接口的PlanChanges方法——返回任意数量的planned change对象汇入计划与任意数量诊断ApplyPhase调用Applyable接口的CheckApply方法——负责收集实际在其他地方调度的 apply 动作结果因为运行时希望对副作用密集的 apply 动作保持更多控制返回任意数量的applied change对象每个代表一次状态变更与任意数量诊断。熟悉模块运行时的读者会发现这大致可比拟构图 并发保序遍历差异在于 Stacks 的 walk 遍历的是从Main可达的对象树且不关心访问顺序——对象间动态数据流一个对象的方法阻塞等待另一对象的方法完成会自动产生合适的求值顺序。这种动态调度由控制流自然涌现任何依赖其他对象昂贵/副作用工作的操作都通过 packagepromising仓库路径 internal/promising/README.md的 promises/tasks 模型传递数据。Apply 阶段调度显式依赖图与 ChangeExec验证与规划阶段中工作顺序完全由控制流间自动拼装的动态数据流图驱动——这建立在这两个阶段不应修改 Terraform 之外的任何东西的前提上只需保证数据在恰当时间就绪。但apply 阶段处理外部可见的副作用其相对顺序至关重要例如某些远端 API 中先销毁一个仍被依赖的对象会报错或一直超时所以 Terraform 必须直接考虑操作序列杜绝该场景出现——即使依赖关系并非数据流自然暗示的。方案是引入额外的调度原语ChangeExec见 change_exec.go并利用规划阶段收集的显式依赖数据。实践中只有components代表有显式排序约束的操作——因为 Stacks 运行时内没有其他对象直接与 Terraform 的资源实例变更生命周期打交道。因此只需一张组件间依赖图即可保证正确。Applyable接口包含方法RequiredComponents必须返回某可应用对象依赖的全部组件集合。多数实现的RequiredComponents包装了一个基于Referrer接口的公共实现——后者工作于更低抽象层只处理 HCL 级表达式引用而不管引用对象的类型公共实现再把引用图抬升为组件图做法是剔除非组件节点、保留它们之间的边。plan 阶段推导出组件间关系后会把它作为 plan 的一部分保存使 apply 阶段可立即使用而无需重新构图。apply 阶段再用ChangeExec实际调度变更它在高层包装promising的概念监督每个组件实例 apply 的执行并把结果集中捕获供下游工作引用。每个组件实例对应一个 task阻塞在其依赖的每个组件的 promise 完成上从而显式保证组件实例变更按正确相对顺序被应用。由于ChangeExec只关心组件实例apply 阶段仍会像前一节所述执行一次并发动态 walk确保配置中所有其他对象都被访问并有机会报告问题与平时的显著区别是任何引用组件实例的对象会阻塞直到该组件实例由ChangeExec管理的 apply 阶段完成。除此之外其他对象类型仍按通常的数据流驱动调度决定求值顺序。为什么这些设计如此重要给使用者与贡献者的启示对只想使用 Stacks 的用户而言上述实现细节最直接的落点是静态校验前置绝大多数错误引用不存在的组件、未声明变量等在 validate 阶段即被捕获且不会因for_each多实例而重复刷屏plan/apply 分离且可靠组件间的执行顺序在规划阶段就被固化进 plan实际 apply 时即使数据流暗示不到排序关系销毁/创建顺序也不会出错面向 RPC 的边界plan 与 state 的持久化使用内部tfstackdata1格式任何外部工具应通过 Terraform Core RPC API 交互不要依赖内部格式。对阅读源码或贡献代码的开发者stackeval README 给出的维护约定值得牢记模型对象一律用未导出构造函数创建且只能有一个调用点动态对象仅在单个求值阶段内单例可能产生诊断的操作拆成Check前缀与非前缀两个方法并遵循唯一传播路径新增测试可用 mainbundle 合成源码包 的子目录搭配loadMainBundleConfigForTest辅助函数快速构造带 synthetic source address 的配置详见其测试数据组织说明。小结与延伸阅读总结要点Terraform Stacks 以stackconfig承担语言前端、stackaddrs承担寻址、stackplan/stackstate承担数据模型、stackruntime承担全部动态求值而tfstackdata1仅作为内部持久化格式运行时用隐式数据流图 树形 walk取代模块运行时的显式依赖图用Check/非前缀方法配对与 promises/tasks 保证诊断单路传播与自动调度并用ChangeExec 组件依赖图处理 apply 阶段的显式排序。感兴趣的读者可以继续在仓库内深入运行时内部架构权威文档internal/stacks/stackruntime/internal/stackeval/README.mdStacks 语言静态解码包文档与入口internal/stacks/stackconfig/doc.go栈级 plan元计划模型说明internal/stacks/stackplan/doc.go状态追踪键增量状态更新的不透明键说明internal/stacks/stackstate/statekeys/doc.go动态调度底层依赖的 promise/task 库internal/promising/README.md面向外部调用方的公开 RPC API 实现internal/rpcapi。【免费下载链接】terraformTerraform enables you to safely and predictably create, change, and improve infrastructure. It is a source-available tool that codifies APIs into declarative configuration files that can be shared amongst team members, treated as code, edited, reviewed, and versioned.项目地址: https://gitcode.com/GitHub_Trending/te/terraform创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考