In my previous articles covering HashiCorp Terraform and OpenTofu, we focused on the architecture around security and state isolation. But we haven’t yet addressed the actual execution engine’s primary feedback mechanism: the plan command.
If you have ever run a complex deployment, you are familiar with the wall of text it generates—and the frustration of seeing the phrase (known after apply) repeated across dozens of resource attributes.
At first glance, it feels like the engine is hiding the most important data. Dynamic attributes like cloud-generated instance IDs, auto-assigned IP addresses, and generated security certificates cannot be evaluated until the cloud provider actually creates the resource. Consequently, the plan output splits your infrastructure into two distinct categories: the computed variables it must wait for, and the explicit configurations you provided.
It is within that small fraction of immediately visible data—the hardcoded strings, booleans, and explicit resource relationships—where the true value of the plan lies. This visible subset contains the structural changes, dependency shifts, and potential destructive actions (like resource recreation) that you must verify before committing to an apply.
To understand why the engine knows some values but must guess at others, we have to look under the hood at how the plan is compiled using a Directed Acyclic Graph (DAG) and mapped against the .tfstate file.
The Role of .tfstate
If the plan represents intent, the .tfstate file represents reality.
A common misconception is that the state file is just a backup artifact or a dump of your code. In reality, it functions as a critical private mapping database. The cloud providers have no concept of your local code variables or resource blocks; AWS only understands an instance ID like i-0123456789abcdef0. The .tfstate file bridges this gap by mapping your logical resource name (aws_instance.web) to the physical real-world provider ID.
When you look at a plan output, the engine relies entirely on the existing state file to perform two key operations:
- Drift Detection (The Refresh Phase):
Before showing you any modifications, the engine reads the IDs stored in your .tfstate file, queries the cloud provider’s API directly to see if those resources have changed outside of Git (manual console edits), and updates its local memory cache.
- Calculating the Delta:
By comparing your target code against the refreshed state snapshot, the engine isolates exactly what needs to be created, updated, or destroyed.
The values hidden behind (known after apply) do not exist anywhere yet—not in your code, and certainly not in your current .tfstate. They are blank slots waiting for API execution. Only after you approve the plan and run the apply phase will the cloud provider return those values, allowing the engine to write them into the updated .tfstate file as the new baseline for your next deployment.
Why Group plan and .tfstate Together?
You cannot understand what a plan is actually telling you without understanding the state file behind it. They are two sides of the same coin: plan is the delta engine, and .tfstate is the baseline.
The engine requires both to execute safely. If you only look at your .tf configuration files, you are looking at a static wishlist. The .tfstate file provides the grounding reality.
During a deployment lifecycle, the state file acts as the primary driver across both execution phases:
- In the
planphase:
The engine uses the .tfstate file to map your code to real-world infrastructure and check for drift. Without it, the engine wouldn’t know if a resource already exists or needs to be built from scratch.
- In the
applyphase:
The engine executes the changes and writes the newly minted realities—the values that were previously hidden behind (known after apply)—straight back into the .tfstate file.
Grouping them together reveals the core loop of Infrastructure as Code: code defines the target, state records the current reality, and the plan is the mathematical difference between them.
Under the Hood - The Directed Acyclic Graph (DAG)
To truly understand how the plan is calculated and why certain values remain (known after apply), we have to look at the engine’s core compiler. OpenTofu and Terraform do not read your code from top to bottom like a script. Instead, they treat your entire configuration directory as a single pool of declarations and construct a Directed Acyclic Graph (DAG).
Nodes: Every resource, data source, module, and variable block in your code becomes a node in this graph.
Edges: The lines connecting the nodes represent dependencies.
Dependencies can be implicit (e.g., when aws_instance.web references aws_subnet.frontend.id, creating an automatic dependency) or explicit (using the depends_on meta-argument). The term Acyclic is critical here: it means the graph can never loop back on itself. If Resource A depends on Resource B, and Resource B depends on Resource A, the compiler breaks, throws a cyclic dependency error, and refuses to generate a plan.
How the Graph Resolves the Plan
When you execute a plan, the engine performs a topological sort on this graph to calculate the execution order. It traverses the graph from the ground up, starting with independent nodes (like variables and providers) and moving upward to highly dependent nodes (like load balancers and auto-scaling groups).
This graph architecture directly determines what you see in the plan output:
- Parallel Execution:
Any nodes that sit on separate, unconnected branches of the graph are processed concurrently. The engine can safely create or modify these resources at the same time because they do not share dependencies.
- The “Known After Apply” Wall:
When a resource higher up the graph depends on an attribute generated by a resource lower in the graph (such as an auto-assigned private IP address), that specific branch stalls during the plan phase. The engine knows it must create the dependency first, but until that API call actually returns a real value during the apply phase, the dependent attribute must be marked as (known after apply).
By visualizing your infrastructure as a graph rather than a flat file, it becomes clear why modifying a single resource configuration can sometimes trigger a cascading waterfall of replacements throughout your plan.
Understanding the tight coupling between the Directed Acyclic Graph (DAG), the plan command, and the .tfstate file transforms how you write and maintain Infrastructure as Code.
When you treat your configurations as a dynamic graph, the (known after apply) make architectural sense. The state file is the grounding baseline that anchors your abstract code to physical cloud hardware.
Critical Note on Version Alignment:
Before executing a plan or interacting with an existing .tfstate file, ensure you are using the exact same version of the Terraform or OpenTofu CLI that last wrote to it. State files contain a schema version layout. Upgrading or downgrading engine versions across identical state backends can cause compatibility locking, deprecated feature mismatches, or syntax errors that corrupt the state.