DEV Community

Kashif Manzer
Kashif Manzer

Posted on

Your plan says (known after apply). OpenTofu 1.13 lets you talk back

Every person who has reviewed an infrastructure plan knows the feeling.

You run tofu plan on a Friday afternoon. The output scrolls past. A third of the values you actually care about say the same thing:

+ id = (known after apply)
Enter fullscreen mode Exit fullscreen mode

So you squint. You reason from experience. You say "yes, that looks right" to a plan that is, in the strictest sense, guessing. Then you apply, and the guessing continues on your side until the resources exist.

OpenTofu 1.13 shipped on September 30 (with a 1.13.1 patch on October 1, security support to August 2027) and it does something about that. It gives you a way to tell the planner what you already know.

Why the plan can't know

First, the mechanism, because it explains everything else.

OpenTofu's plan is a prediction. It walks your configuration, calls the providers, and works out what would change without changing anything. That last part matters: it cannot call the cloud API to actually create things, because creating things is the whole point of apply.

And that is where the unknowns come from. Plenty of values only exist after the remote system acts. An instance's ID, an ARN, the IP a load balancer will be assigned: these are chosen by the cloud API during apply. At plan time, OpenTofu has no way to compute them, so it marks them (known after apply) and does the best it can with the rest.

This is honest engineering. The problem is that unknowns poison everything they touch. If a value is unknown, any expression built on it is usually unknown too. Compare an unknown VPC ID to null and the result is unknown. Feed it into another module and the plan for that module goes murky. One unknown upstream turns into a wall of unknowns downstream, and you are back to squinting.

Tell the planner what you know

Here is the new idea. You, the module author, often know things the provider cannot promise.

Take a VPC ID from the AWS provider. During plan it is unknown. But you know something about it: it will never be null, and every real VPC ID starts with vpc-. The provider's API documentation guarantees this. OpenTofu just had no way to hear it.

The 1.13 assume... family of functions is a megaphone for exactly that:

output "vpc_id" {
  value = assumenotnull(
    assumestringprefix(
      aws_vpc.example.id,
      "vpc-",
    )
  )
}
Enter fullscreen mode Exit fullscreen mode

Read it as a sentence: "I assume this is not null, and I assume it is a string starting with vpc-."

Now any expression that compares this output to null or to the empty string gets a known answer at plan time (false), instead of an unknown. If a downstream module validates that the VPC ID starts with vpc-, that validation now runs during the plan phase, where you can see the mistake before anything is created.

Two more worth knowing: convert tells OpenTofu the expected type of a value it cannot infer (for example, the result of jsondecode on something dynamic), so downstream attribute access gets a known type. And assumeequal declares the expected final value outright.

The catch: your promise gets checked

This is the part that matters most, so say it plainly.

The planner takes your word for it during plan. But at apply time, once the real values arrive, OpenTofu checks. If you promised that an unknown string starts with vpc- and it turns out to start with subnet-, the assumestringprefix call fails during the apply.

That is the right design. An unchecked assumption would turn the plan into fiction. This way the hint is verified by reality, and the cost of lying to the planner is a failed apply rather than silent breakage.

The rule of thumb: only promise what the provider or the vendor API documentation actually guarantees. "The docs say VPC IDs always look like this" is a promise. "In my experience they usually look like this" is not.

A few other things in 1.13

This was a short release cycle. The team cut it short on purpose to line up with the Go release schedule, which stretches the security support window to August 2027. So besides the unknown-value work, there are a couple of early experiments and some housekeeping:

  • Built-in linting, first pass. tofu plan -lint=all runs four rules: variables missing a type, count used where the newer enabled meta-argument fits, unused variables, unused locals. Very early. Expect it to grow.
  • Symbol Libraries, experimental. A way to share values, functions, and type aliases across configurations, separate from modules. The shape is still evolving.
  • Windows on ARM64 is now an official platform.
  • Before you upgrade: base64gzip output changed slightly, which can make OpenTofu propose diffs on resources that use it. WinRM support for provisioners is gone (use OpenSSH for Windows). This is the last series with official 32-bit builds, and macOS builds now need Ventura or newer.

Why this is bigger than it looks

The quiet problem with (known after apply) was never just aesthetics. Unknown values blind automated checks. Some teams run the machine-readable plan through policy-checking software, and every unknown forces a choice: trust optimistically and risk a violation, or block conservatively and hold up changes that would have been fine.

The assume... functions give module authors a way to sharpen the plan's vision, one honest guarantee at a time. They do not remove uncertainty. They let you say, precisely, where the uncertainty ends.

Have you ever been burned by approving a plan full of unknowns? What was the one that got you?

Top comments (0)