Terraform — Architecture#

Architectural style#

Graph-Walking Engine with Plugin-Based Providers, Layered CLI, and Parallel Machine API

Terraform’s architecture is built around a declarative graph execution engine: every operation (plan, apply, destroy, import, validate) is modeled as a directed acyclic graph (DAG) walk. The graph is constructed fresh for each operation through a transform pipeline (series of GraphTransformer steps), then walked in parallel respecting dependency edges. Providers — the components that actually interact with cloud APIs — are isolated as out-of-process gRPC subprocesses reached through the hashicorp/go-plugin framework.

A layered structure sits above the engine: a CLI layer (command/) handles user interaction, a backend layer (backend/) abstracts state storage and whether to run locally or in HCP Terraform, and a config layer (configs/) parses HCL source into in-memory structures the engine can consume.

A parallel architectural track — the Stacks system — reimplements the same engine concept but targets a machine-facing gRPC API (rpcapi) rather than the CLI, uses a promise-based async evaluator, and has its own address types, config parser, and wire protocol.

Evidence: internal/terraform/graph_builder.go, internal/dag/walk.go, internal/providers/interface.go, commands.go, internal/rpcapi/server.go.


Component diagram (textual)#

┌────────────────────────────────────────────────────────────────────┐
│  main.go (realMain)                                                │
│  CLI Bootstrap: OTel → terminal → cliconfig → credentials →        │
│                 providerSrc → backendInit.Init() → cli.CLI.Run()   │
└──────────────────────────┬─────────────────────────────────────────┘
                           │
              ┌────────────▼────────────┐
              │  command.Meta (shared)  │  (value-copied per command)
              │  WorkingDir / Streams   │
              │  Services / ProviderSrc │
              └────────────┬────────────┘
                           │ instantiates
     ┌─────────────────────┼──────────────────────────┐
     │  command.*Command   │  (50+ subcommands)        │
     │  ApplyCommand       │  PlanCommand              │
     │  InitCommand        │  ValidateCommand          │
     └──────────┬──────────┴──────────────────────────┘
                │ selects and delegates to
     ┌──────────▼───────────────────────────────────────┐
     │  backend.Backend / backendrun.Running             │
     │  local.Local   |  remote.Remote  |  cloud.Cloud   │
     └──────────┬────────────────────────────────────────┘
                │ calls
     ┌──────────▼────────────────────────────────────────────────┐
     │  terraform.Context                                        │
     │  ┌──────────────────────────────────────────────────────┐ │
     │  │ Plan()  → planGraph() → PlanGraphBuilder             │ │
     │  │ Apply() → applyGraph() → ApplyGraphBuilder           │ │
     │  │ Import() → context_import.go                         │ │
     │  │ Validate() → context_validate.go                     │ │
     │  └────────────────┬─────────────────────────────────────┘ │
     │                   │ builds DAG via transform pipeline      │
     │  ┌────────────────▼─────────────────────────────────────┐ │
     │  │  dag.Graph + dag.Walker (parallel walk)              │ │
     │  │  Each vertex: GraphNodeExecutable.Execute(ctx, op)   │ │
     │  └────────────────┬─────────────────────────────────────┘ │
     │                   │ nodes call                             │
     │  ┌────────────────▼─────────────────────────────────────┐ │
     │  │  EvalContext / BuiltinEvalContext                     │ │
     │  │  (providers, provisioners, state, named values)       │ │
     │  └────────────────────────────────────────────────────────┘ │
     └───────────────────────────────────────────────────────────┘
                │
     ┌──────────▼─────────────────────────────────────────────────┐
     │  providers.Interface (gRPC subprocess)                    │
     │  plugin5 (tfplugin5.proto) + plugin6 (tfplugin6.proto)    │
     │  grpcwrap: providers.Interface → gRPC server impl         │
     └────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────┐
│  Parallel Track: Stacks (machine-facing)                          │
│  rpcapi → stacksruntime → stackeval (promise-based) →             │
│           stacksplugin (stacksproto1.proto)                       │
│  Entry: "rpcapi" CLI command → go-plugin server (gRPC)            │
└────────────────────────────────────────────────────────────────────┘

Core components#

1. CLI Bootstrap (main.go + commands.go)#

  • Package: root main
  • Responsibility: Initialize the full application: OpenTelemetry span, terminal detection, CLI config loading, credentials, provider installation source, backend registry. Register all subcommands into a map[string]cli.CommandFactory. Delegate to hashicorp/cli.
  • Key types: realMain(), initCommands(), command.Meta (shared across all commands as value copy)
  • Dependencies: internal/command, internal/backend/init, internal/terminal, internal/logging, internal/rpcapi

2. Command Layer (internal/command)#

  • Package: internal/command
  • Responsibility: Implement each user-facing CLI subcommand. Each command embeds command.Meta, which provides access to the working directory, backend selection, provider source, terminal streams, and output views. Commands parse flags via stdlib flag, delegate heavy lifting to the backend or directly to terraform.Context.
  • Key types: Meta (shared base struct), ApplyCommand, PlanCommand, InitCommand, TestCommand (and ~45 more). StateMeta for state-modifying commands.
  • Dependencies: internal/backend, internal/terraform, internal/configs/configload, internal/command/views, internal/tfdiags
  • Notable sub-packages: views/ (human vs JSON output abstraction), arguments/ (typed argument structs per command), format/ (legacy human-readable formatters), json*/ (machine-readable JSON output renderers)

3. Backend Layer (internal/backend)#

  • Package: internal/backend, internal/backend/backendrun, internal/backend/local, internal/backend/remote
  • Responsibility: Abstract where state lives and where operations execute. The Backend interface handles state management. The backendrun.Running interface handles plan/apply execution. The local backend runs the graph walk in-process; the remote backend streams operations to HCP Terraform.
  • Key types: backend.Backend (interface: ConfigSchema, PrepareConfig, Configure, StateMgr, Workspaces), backendrun.Running (interface: LocalRun, PlanWithUI, ApplyWithUI), local.Local (default in-process runner)
  • Dependencies: internal/terraform, internal/states/statemgr, internal/configs/configload
  • Notable: Nine remote-state backends (S3, GCS, Azure, Consul, k8s, PG, OCI, COS, OSS) are separate Go modules with their own go.mod, wired via replace directives to avoid cross-contaminating cloud SDK dependencies.

4. Core Engine (internal/terraform)#

  • Package: internal/terraform
  • Responsibility: The heart of Terraform. Implements the Context type whose methods (Plan, Apply, Import, Validate, Refresh) each build a specialized DAG and walk it. Graph nodes encapsulate the logic for each HCL object type (resource, provider, variable, output, module, etc.). The walk is parallel with dependency ordering enforced by the DAG.
  • Key types:
    • Context — orchestrator; holds plugin registry (contextPlugins), parallelism semaphore, stop hook
    • GraphBuilder interface — Build(ModuleInstance) (*Graph, tfdiags.Diagnostics)
    • BasicGraphBuilder — runs a list of GraphTransformer steps to build the graph
    • PlanGraphBuilder, ApplyGraphBuilder, EvalGraphBuilder, InitGraphBuilder — operation-specific builders
    • GraphNodeExecutableExecute(EvalContext, walkOperation) tfdiags.Diagnostics
    • GraphNodeDynamicExpandable — nodes that expand into sub-graphs at walk time (for count/for_each)
    • EvalContext / BuiltinEvalContext — per-module evaluation context threaded through graph walk
    • ContextGraphWalkerdag.Walker callback implementation that routes each node to its EvalContext
    • Hook — observer interface for UI progress reporting (pre/post resource CRUD)
  • Dependencies: internal/dag, internal/configs, internal/addrs, internal/states, internal/plans, internal/providers, internal/provisioners, internal/lang, internal/tfdiags

5. DAG Engine (internal/dag)#

  • Package: internal/dag
  • Responsibility: Pure graph library: add/remove vertices and edges, topological sort, Tarjan’s SCC (cycle detection), transitive reduction, parallel walk. No Terraform-specific logic — this is a reusable foundation.
  • Key types: Graph, AcyclicGraph, Walker (parallel walk with WalkFunc callback), Edge, Vertex (interface{}), NamedVertex (optional interface for display names)
  • Dependencies: internal/tfdiags (for diagnostic collection during walk); nothing else internal
  • Notable: Walker creates 2V goroutines — one per vertex for execution, one per vertex as a dependency waiter. Walk-time graph mutations are supported.

6. Provider Plugin System (internal/providers, internal/plugin, internal/plugin6, internal/grpcwrap)#

  • Package: internal/providers, internal/plugin, internal/plugin6, internal/grpcwrap, internal/tfplugin5, internal/tfplugin6
  • Responsibility: Isolate provider implementations as subprocesses. providers.Interface defines the 30-method contract for any provider (GetProviderSchema, ValidateProviderConfig, PlanResourceChange, ApplyResourceChange, ReadResource, etc.). Providers are launched via hashicorp/go-plugin (gRPC over stdin/stdout). Two wire protocols are maintained simultaneously: Protocol 5 (tfplugin5.proto) and Protocol 6 (tfplugin6.proto). The grpcwrap package adapts providers.Interface implementations to serve as gRPC servers (for test providers embedded in Terraform itself).
  • Key types: providers.Interface, providers.Factory (func() (Interface, error)), plugin.GRPCProvider (Protocol 5 client), plugin6.GRPCProvider (Protocol 6 client)
  • Dependencies: hashicorp/go-plugin, protobuf-generated stubs, internal/configs/configschema

7. Config Parser (internal/configs)#

  • Package: internal/configs
  • Responsibility: Parse HCL source files into in-memory Go types. Handles terraform {}, resource, data, provider, variable, output, locals, module, moved, import, check, removed blocks. Module tree loading (from disk or registry) is in internal/configs/configload.
  • Key types: Config (tree of modules), Module (single directory), Resource, Provider, Variable, Output, ModuleCall
  • Dependencies: hashicorp/hcl/v2, internal/addrs, internal/tfdiags

8. Address Types (internal/addrs)#

  • Package: internal/addrs
  • Responsibility: Define a rich type system for every referenceable object in Terraform: ResourceInstance, Provider, Module, ModuleInstance, InputVariable, OutputValue, LocalValue, PathAttr, etc. (~40 distinct address types). Used everywhere as typed identifiers — prevents address confusion bugs.
  • Key types: AbsResourceInstance, AbsProviderConfig, ModuleInstance, Provider, RootProviderConfig
  • Dependencies: github.com/zclconf/go-cty/cty, hashicorp/hcl/v2

9. State / Plan Data (internal/states, internal/plans)#

  • Package: internal/states, internal/plans
  • Responsibility: In-memory representations of current state and planned changes. states.State is the live resource inventory; plans.Changes is the diff. states/statemgr defines StateMgr interfaces (Locker, Reader, Writer, Refresher) implemented by each backend. Statefile v4 JSON serialization in states/statefile.
  • Key types: states.State, states.SyncState (mutex-wrapped for concurrent access), plans.Plan, plans.Changes, plans.ChangesSync, statemgr.Full

10. Diagnostics (internal/tfdiags)#

  • Package: internal/tfdiags
  • Responsibility: Unified diagnostic type system (wrapping HCL diagnostics and adding Terraform-specific ones). Diagnostics carry source locations, severity (error/warning), and rich structured context. Used as the primary error-propagation mechanism throughout the codebase instead of bare error.
  • Key types: Diagnostics (slice), Diagnostic (interface), AttributeValue, WholeContainingBody

11. Stacks Runtime (internal/stacks, internal/rpcapi)#

  • Package: internal/stacks, internal/rpcapi
  • Responsibility: A parallel execution model for multi-configuration orchestration (Stacks). Uses a promise-based concurrent evaluator (stackeval), its own HCL config format (.tfstack.hcl), its own address types (stackaddrs), and communicates via gRPC (stacksproto1.proto) rather than CLI. Entry point is the rpcapi “hidden” command which runs Terraform as a go-plugin server.
  • Key types: rpcapi.ServePlugin(), stacks/stackruntime, promising.Promise[T] (generic future type)

Data flow#

Classic terraform plan#

User runs: terraform plan

1. main.go:realMain()
   └── terminal.Init()                  // detect TTY, width
   └── cliconfig.LoadConfig()           // ~/.terraformrc
   └── credentialsSource()              // API tokens
   └── providerSource()                 // build installation search path
   └── backendInit.Init(services)       // register backend constructors
   └── initCommands()                   // build Commands map
   └── cli.CLI.Run()                    // dispatch to "plan"

2. command.PlanCommand.Run(args)
   └── arguments.ParsePlan(args)        // typed flag struct
   └── meta.Backend(backendConfig)      // instantiate + configure backend
       └── backend/init registry lookup // name → constructor
       └── backend.PrepareConfig()
       └── backend.Configure()
   └── backendrun.Operation{...}        // package up the operation request
   └── b.RunOperation(op)               // local or remote

3. backend/local.Local.RunOperation(op)
   └── local.LocalRun(op)
       └── configload.LoadConfig(dir)   // parse all .tf files → configs.Config
       └── statemgr.Lock()              // acquire state lock
       └── stateMgr.RefreshState()      // read current state from backend
   └── local.opPlan(op, lr, sm)
       └── terraform.NewContext(opts)   // create core engine
       └── c.Plan(config, state, opts)

4. terraform.Context.Plan()
   └── c.planGraph(config, state, opts)
       └── PlanGraphBuilder{...}.Build(root)
           └── BasicGraphBuilder.Steps[]  // ~15 GraphTransformer steps
               // Steps add: resource nodes, provider nodes, output nodes,
               // variable nodes, module expand nodes, dependency edges,
               // provider inheritance edges, transitive reduction, etc.
   └── dag.Walker.Update(graph)
   └── dag.Walker.Wait()               // parallel walk begins
       // For each vertex (in dependency order, parallel when possible):
       └── ContextGraphWalker.EnterPath()  // get/create BuiltinEvalContext
       └── node.Execute(ctx, walkPlan)
           // e.g. NodeAbstractResourceInstance.Execute():
           └── ctx.Provider(addr)           // get/launch provider subprocess
           └── provider.GetProviderSchema() // fetch schema (cached)
           └── evaluate HCL expressions    // resolve references from namedvals
           └── provider.PlanResourceChange(req)  // gRPC call to provider
           └── ctx.Changes().AppendResourceInstanceChange(change)

5. Result: plans.Plan{Changes, Config, State, ...}
   └── planfile.Create()               // write .tfplan file (zip)
   └── views.Plan.Render(plan)         // human or JSON output

Plugin lifecycle within a graph walk#

context_plugins.go: contextPlugins.startProvider(addr)
    └── Factory()(providers.Factory function)
        └── plugin.NewGRPCProvider(clientConfig)    // for Protocol 5
            └── hashicorp/go-plugin: exec subprocess
            └── negotiate gRPC handshake over stdin/stdout
            └── grpc.Dial() to plugin's listener
        └── return providers.Interface (gRPC client stub)
    └── cached in ContextGraphWalker.providerCache[addr]

Subsequent calls: provider.PlanResourceChange() → gRPC → subprocess provider binary

Initialization / Bootstrap#

The bootstrap sequence in main.go:realMain() is strictly linear and manual — no dependency injection framework:

  1. OpenTelemetryopenTelemetryInit(): optional OTLP exporter if TF_CLI_TELEMETRY_EXPORTER_OTLP_ENDPOINT set
  2. Terminalterminal.Init(): detect stdout/stderr/stdin TTY status and column width
  3. CLI Configcliconfig.LoadConfig(): parse ~/.terraformrc or $TERRAFORM_CONFIG_FILE
  4. Service Discoverydisco.NewWithCredentialsSource(credsSrc): HTTPS host service discovery (for registry, remote backend)
  5. Provider SourceproviderSource(config, services): build provider installation search path (registry, local mirror, filesystem mirror)
  6. Backend RegistrybackendInit.Init(services): register all backend constructors into a global map (name → InitFn)
  7. Command MapinitCommands(ctx, wd, streams, config, services, providerSrc, ...): construct single command.Meta value; build Commands map (50+ cli.CommandFactory closures capturing meta by value-copy)
  8. Checkpointgo runCheckpoint(ctx, config): async version check against HashiCorp’s checkpoint service
  9. CLI Dispatchcli.CLI{Args, Commands}.Run(): parse subcommand, run factory, execute

Dependency injection pattern: Manual. The command.Meta struct is populated once and value-copied into every command struct at registration time. There is no wire/dig/fx. The terraform.Context receives providers and provisioners as map-of-factories at construction time (ContextOpts.Providers map[addrs.Provider]providers.Factory).


Configuration#

Configuration exists at three levels:

1. CLI Configuration (~/.terraformrc)#

  • Parser: internal/command/cliconfig (HCL)
  • Key settings: credentials blocks (API tokens per host), provider_installation block (filesystem/registry mirrors, network exclusions), plugin_cache_dir, plugin_cache_may_break_dependency_lock_file
  • Override: TERRAFORM_CONFIG_FILE environment variable, TF_CLI_ARGS_* for per-command flag injection

2. Root Module Configuration (.tf files in working directory)#

  • Parser: internal/configs (HCL v2 via hashicorp/hcl/v2)
  • Consumed by: terraform.Context.Plan(config, ...) — the parsed *configs.Config tree is the configuration input to every operation
  • Variables: Set via -var, -var-file, TF_VAR_* environment variables, or interactive prompt

3. Backend Configuration (in .tf files + .terraform/terraform.tfstate)#

  • Parser: internal/backend (backend.Backend.ConfigSchema() + HCL)
  • Persistence: Backend selection is recorded in .terraform/terraform.tfstate (not to be confused with resource state)
  • Workspace isolation: Each workspace has its own state; backends control the naming convention

Feature flags / experiments#

  • Compile-time: main.experimentsAllowed ldflag controls whether experimental features are available
  • Runtime: internal/experiments package holds the experiment registry; individual experiments can be enabled per-module in HCL terraform { experiments = [...] }

Key design decisions#

1. Graph-per-operation (not a single universal graph)#

Each operation builds a fresh, operation-specific graph via a different GraphBuilder. The PlanGraphBuilder and ApplyGraphBuilder differ in which node types they add (e.g., ApplyGraphBuilder includes OrphanResourceInstanceTransformer and respects create_before_destroy ordering). This avoids a single god-graph that handles all modes with conditional logic scattered through nodes. The transform pipeline ([]GraphTransformer steps) makes the construction composable and traceable via GRAPH_TRACE logs.

Evidence: internal/terraform/graph_builder_plan.go, internal/terraform/graph_builder_apply.go, internal/terraform/graph_builder.go

2. Out-of-process providers (no in-process loading)#

Providers are always subprocesses — there is no in-process provider loading. The providers.Interface is implemented by a gRPC client stub that calls the provider binary over the go-plugin protocol. This provides complete memory and crash isolation (a provider panic cannot kill terraform), but has overhead: each gRPC call crosses process boundaries. The PreloadedProviderSchemas optimization in ContextOpts amortizes the schema-fetching overhead when running many operations in sequence.

Evidence: internal/plugin/grpc_provider.go, internal/providers/interface.go

3. Dual protocol versions in production simultaneously#

Terraform maintains Protocol 5 and Protocol 6 simultaneously with full feature parity. Providers compiled against either SDK work without modification. The grpcwrap adapters translate between the two wire formats. This reflects the real-world constraint of a large provider ecosystem where not all providers can be upgraded atomically.

Evidence: internal/tfplugin5/, internal/tfplugin6/, internal/grpcwrap/

4. All-internal architecture (zero exported packages)#

Every package outside the root main is internal/. This is a deliberate and permanent decision: Terraform core is not intended to be embedded as a library. External tooling integrates through the gRPC plugin protocol or the rpcapi interface. This eliminates API stability obligations and allows fearless refactoring of internal APIs without semver concerns.

Evidence: Structure analysis; internal/rpcapi docs note it is the intended machine-facing entry point

5. Diagnostics as the primary error type#

Terraform uses tfdiags.Diagnostics (a slice of Diagnostic) rather than error as its primary error propagation mechanism throughout the internal stack. This allows accumulating multiple errors in a single pass (a graph walk can discover 50 resource errors simultaneously) and attaching source location context (file/line from the HCL parser). Bare error returns appear only at the Go standard library boundary and in lower-level utility packages.

Evidence: internal/tfdiags/, virtually every internal package’s function signatures

6. Stacks as a parallel architecture (not an evolution)#

The Stacks system in internal/stacks/ is not a refactoring of the classic engine — it is a parallel implementation sharing only the DAG library, address primitives, and provider protocol. Stacks use promise-based async evaluation (internal/promising) instead of the goroutine-walker model, communicate via rpcapi gRPC instead of CLI, and have entirely separate config, state, and plan data types. This co-existence design allows HCP Terraform’s orchestration layer to use Terraform’s core as a library-via-RPC while the CLI experience remains unchanged.

Evidence: internal/stacks/stackruntime/, internal/promising/, internal/rpcapi/