Read OSS

Terraform 的架构:代码库全景导览

中级

前置知识

  • 熟悉 Go 语言(接口、包、goroutine)
  • 具备基本的 Terraform 使用经验(资源、provider、plan/apply 工作流)

Terraform 的架构:代码库全景导览

Terraform 是全球使用最广泛的基础设施工具之一,然而真正深入研究过其内部实现的工程师却出奇地少。整个代码库是一个单一的 Go 模块——名副其实的单体架构——在 internal/ 目录下包含约 65 个包。理解其架构,能让你在调试 provider 问题、提交补丁,或在 Terraform 内部机制之上构建工具时更加得心应手。

本文将为你建立贯穿本系列后续深度解析所需的整体认知框架。我们会从 main() 到 provider 插件调用,逐步梳理关键包与核心抽象。

启动流程:main.go

一切从 main.go#L65-L352 开始。realMain() 函数是一段约 290 行的线性启动流程,在分发到具体 CLI 命令之前,它会依次初始化所有子系统。核心流程如下:

flowchart TD
    A["realMain()"] --> B["OpenTelemetry init"]
    B --> C["Logging & panic handler"]
    C --> D["terminal.Init() — detect TTY"]
    D --> E["cliconfig.LoadConfig() — read .terraformrc"]
    E --> F["disco.NewWithCredentialsSource() — service discovery"]
    F --> G["providerSource() — build provider chain"]
    G --> H["backendInit.Init() — register 14 backends"]
    H --> I["extractChdirOption — handle -chdir"]
    I --> J["initCommands() — wire ~40 commands"]
    J --> K["cli.CLI.Run() — dispatch to command"]

这一顺序是经过刻意设计的。遥测(Telemetry)最先初始化(第 70 行),以便顶层 span 能够覆盖完整的执行过程。终端检测(第 113 行)也在早期执行,因为输出格式化取决于 stdout 是否为 TTY。CLI 配置加载(第 139 行)故意安排在 -chdir 处理之前,这样 TERRAFORM_CONFIG_FILE 中的相对路径就能相对于真实的工作目录来解析。

有一个细节值得特别关注:第 212 行backendInit.Init(services) 会将全部 14 个内置 backend 注册到一个全局 map 中。这一操作只在启动时执行一次,此后 backend 列表不再变动。我们将在第 5 篇文章中探讨为何 backend 是硬编码的,而非可插拔的。

提示: 如果你需要调试 Terraform 的启动过程,可以设置 TF_LOG=TRACE 来查看每个步骤的日志输出。启动流程中有大量 [INFO][TRACE] 级别的 log.Printf 调用,埋点非常详尽。

命令注册与共享 Meta

启动完成后,commands.go#L56-L114 中的 initCommands() 会构造一个 command.Meta 结构体,供所有命令共享:

meta := command.Meta{
    WorkingDir:          wd,
    Streams:             streams,
    View:                views.NewView(streams).SetRunningInAutomation(inAutomation),
    Services:            services,
    ProviderSource:      providerSrc,
    ProviderDevOverrides: providerDevOverrides,
    UnmanagedProviders:  unmanagedProviders,
    ShutdownCh:          makeShutdownCh(),
    // ...
}

这个 Meta 随后被传入 Commands map(第 122–451 行)中每个命令的工厂闭包。该 map 包含约 40 个条目,其中包括 "state list""workspace new" 等子命令。

classDiagram
    class Meta {
        +WorkingDir
        +Streams
        +View
        +Services
        +ProviderSource
        +ShutdownCh
    }
    class PlanCommand {
        +Meta
        +Run(args) int
    }
    class ApplyCommand {
        +Meta
        +Destroy bool
        +Run(args) int
    }
    class InitCommand {
        +Meta
        +Run(args) int
    }
    Meta <|-- PlanCommand
    Meta <|-- ApplyCommand
    Meta <|-- InitCommand

这里有一个颇为优雅的设计:destroy 命令本质上就是将 Destroy 标志置为 trueApplyCommand,详见 commands.go#L135-L140

"destroy": func() (cli.Command, error) {
    return &command.ApplyCommand{
        Meta:    meta,
        Destroy: true,
    }, nil
},

这种通过行为标志复用命令实现的模式在代码库中随处可见。它在保持命令结构体数量可控的同时,提供了丰富的 CLI 功能面。

包结构:internal/ 目录全览

Terraform 的包大致按层次组织。以下是最重要的包的结构导览:

层次 职责
CLI command/command/views/command/arguments/ 解析参数、管理 backend、渲染输出
Backend backend/backend/local/backend/init/cloud/ 状态存储、操作执行
核心引擎 terraform/ 图构建与遍历、plan/apply 编排
dag/ 通用 DAG 库:顶点、边、并行遍历
配置 configs/configs/configload/ HCL 解析、模块树组装
状态 states/states/statemgr/ 内存状态模型、持久化、锁机制
计划 plans/plans/planfile/ 变更追踪、plan 序列化
Providers providers/plugin/grpcwrap/ provider 接口、gRPC 桥接、协议转换
发现 getproviders/ Registry 客户端、文件系统镜像、多源支持
地址 addrs/ 命名系统:Provider、Module、Resource 等
诊断 tfdiags/ 带源码位置的富错误/警告信息
语言 lang/ HCL 表达式求值、内置函数
类型 cty(外部库) 配置值的动态类型系统

internal/terraform/ 包是整个系统的核心。它包含编排所有操作的 Context 类型、构建依赖图的图构建器、在图遍历过程中执行的节点类型,以及为节点提供 provider 和状态访问能力的 EvalContext 接口。

flowchart LR
    CLI["command/"] --> Backend["backend/local/"]
    Backend --> Core["terraform/Context"]
    Core --> GraphBuilder["terraform/GraphBuilder"]
    GraphBuilder --> DAG["dag/"]
    Core --> Providers["providers/Interface"]
    Providers --> GRPC["plugin/GRPCProvider"]
    Core --> State["states/SyncState"]
    Core --> Configs["configs/Config"]

提示: 建议优先熟悉 addrs 包。它的类型在代码库中无处不在——作为 map 的键、图节点的标识符以及资源定向的条件。理解 addrs.AbsResourceInstanceaddrs.Provider,能让你阅读几乎所有其他包时事半功倍。

端到端请求流程:terraform plan

接下来看看用户执行 terraform plan 时到底发生了什么。理解这条流程至关重要,因为它贯穿了架构的每一个层次。

第 1 步:CLI 解析。 internal/command/plan.go#L22-L118 中的 PlanCommand.Run() 通过 arguments 包解析参数,创建视图,准备 backend,并构建操作请求。

第 2 步:Backend 分发。 操作被发送到 backend 的 Operation() 方法。本地执行时,最终会进入 internal/backend/local/backend_plan.go#L23-L27 中的 local.opPlan()

第 3 步:Context 创建。 opPlan 加载配置和状态,通过 NewContext() 构造 terraform.Context,然后调用 Context.Plan()

第 4 步:图构建。 Plan() 委托给 internal/terraform/context_plan.go#L180-L194 中的 PlanAndEval(),后者创建 PlanGraphBuilder 并调用 Build()

第 5 步:图遍历。 构建好的图通过 dag.Walker 并行遍历,每个顶点执行自己的 Execute() 方法。

第 6 步:Provider 调用。 NodePlannableResourceInstance 等资源节点通过 gRPC 调用 provider.PlanResourceChange() 来计算变更差异。

sequenceDiagram
    participant User
    participant CLI as PlanCommand
    participant Backend as local.opPlan
    participant Ctx as terraform.Context
    participant Graph as PlanGraphBuilder
    participant Walker as dag.Walker
    participant Node as NodePlannableResourceInstance
    participant Provider as GRPCProvider

    User->>CLI: terraform plan
    CLI->>Backend: RunOperation(opReq)
    Backend->>Ctx: NewContext(opts)
    Backend->>Ctx: Plan(config, state, planOpts)
    Ctx->>Graph: Build(rootModule)
    Graph-->>Ctx: *Graph
    Ctx->>Walker: graph.Walk(walker)
    Walker->>Node: Execute(evalCtx, op)
    Node->>Provider: PlanResourceChange(req)
    Provider-->>Node: planned state + diff
    Node-->>Walker: diagnostics
    Walker-->>Ctx: accumulated changes
    Ctx-->>Backend: *plans.Plan
    Backend-->>CLI: RunningOperation
    CLI-->>User: plan output

这条流程是 Terraform 的骨干。apply 流程与之几乎完全相同,区别仅在于使用 ApplyGraphBuilder 并调用 provider.ApplyResourceChange()。validate、import 和 refresh 也遵循同样的模式:构建图、遍历图、收集结果。

命令模式

每个操作命令(planapplyrefreshimport)都遵循相同的结构模式:

  1. 将参数解析为有类型的 arguments 结构体
  2. 创建命令专用视图(人类可读格式或 JSON 格式)
  3. 通过 PrepareBackend() 准备 backend
  4. 通过 OperationRequest() 构建 Operation 请求
  5. 通过 RunOperation() 执行操作
  6. 根据执行结果返回退出码

这种高度统一的结构意味着,只要深入理解了一条命令,就能举一反三地理解其他任何命令。各命令之间的差异在于:使用哪个图构建器、哪些节点类型填充图,以及调用哪些 provider 方法。

后续内容

本文为你绘制了全景地图,系列后续文章将带你深入具体的地形:

  • 第 2 篇 深入图引擎——通用 DAG 库、并行 walker,以及构建 plan 和 apply 图的 transformer 流水线。
  • 第 3 篇 跟踪一个资源实例完整的 plan 和 apply 生命周期。
  • 第 4 篇 解析 provider 插件系统及其基于 gRPC 的架构。
  • 第 5 篇 剖析状态管理与 backend 抽象层。
  • 第 6 篇 深入 CLI 层、视图系统和诊断系统。
  • 第 7 篇 介绍配置加载与表达式求值机制。

每篇文章都建立在本文所构建的认知框架之上,建议将这份包结构图和请求流程图保存下来,随时备查。