Error: Values Cannot Be Determined Until Apply
This article is part of our Terraform in Depth series, where we dive into advanced details of Terraform that require a deeper understanding of Terraform and longer explanation than are required for our other Terraform articles.
Terraform Errors When Planning: Values Cannot Be Determined Until Apply
One of the more frustrating errors you can encounter when using Terraform is
an error message referring to a value "that cannot be determined until
apply". These are often referred to as "unknown at plan time" errors, in
part because they show up when running terraform plan
.
Error: Invalid count argument
The "count" value depends on resource attributes that cannot be determined
until apply, so Terraform cannot predict how many instances will be created.
These errors always occur in the context of creating a variable number of resources, and they can be confusing, because it can appear to the user that the value in question should be known at plan time. In fact, the same code will sometimes work and sometimes not, depending on the specific details of how it is used and the state of the infrastructure. This is a particular problem for authors of reusable Terraform modules, because they need to be aware of potential problems that may occur when their module is used in contexts they did not anticipate.
In this article, we will explain what these errors mean, why they occur, and how to avoid them.
The Two-Phase Execution Model
To begin with, Terraform implements a two-phase execution model.
- The first phase is the "plan" phase, where Terraform determines what changes are necessary to achieve the desired state.
- The second phase is the "apply" phase, where Terraform makes the changes determined to be required during the plan phase.
The rationale for this two-phase model is to allow Terraform to show you what
changes it will make before it makes them. Terraform is designed so that it
make no changes during the plan phase, making it always safe to run terraform plan
.
Then, during the apply phase, it will only make the changes you approved
from the plan phase.
The error message above only occurs during the plan phase, and it means that some value that Terraform needs to know in order to plan the changes is not known while executing the plan phase. It implies that the value is properly defined, but that it depends on some value that will be generated during the apply phase.
When Does an Unknown Value Cause a Plan to Fail?
Terraform always requires you to approve any changes before it makes them, but it does not always show you the exact details of the changes it will make.
Unknown Individual Attribute Values are Allowed in a Plan
It is impractical for Terraform to compute every detail of the changes it will
make during the apply phase, and therefore some details can be declared
"unknown" at plan time but still allow the plan to succeed and be approved.
In general, the value of a resource attribute is allowed to be unknown, and
the plan will show that the attribute will change, but show that the new value
is unknown, or, more specifically: (known after apply)
.
For example, consider the case where you want to create a new compute instance and then add it as a target to a load balancer. Terraform will not know the specific IP address of the compute instance until it is created, so it cannot show you the exact details of how it will be added as a target to the load balancer. Instead, it will show you that it will add a target to the load balancer, and you will have to approve that change without knowing the exact details. (The alternative would be to require that you create the compute instance in one configuration, obtain its IP address from that configuration, and then use that IP address in a second configuration to add it as a target to the load balancer. If you want that level of control, you can set up your configurations to work that way, but in most cases, people prefer to manage as much as possible with a single configuration.)
Resource Creation and Destruction Always Require Explicit Approval
Terraform always requires explicit approval to create or destroy a resource.
It also allows you to create a number of resources of the same type using a
parameter with count
or for_each
to determine how many to create. For
example:
resource "aws_instance" "bastion" {
count = length(var.availability_zones)
# ...
}
This feature is also used in practice to create a resource conditionally:
resource "aws_instance" "bastion" {
count = var.bastion_enabled ? 1 : 0
# ...
}
Terraform needs to know how many resources to create during the plan phase or the plan will fail with an error message like the one at the beginning of this article.
What is Known and Unknown at Plan Time, Part 1: The Obvious
In the planning phase, Terraform knows the current state of the infrastructure, and some information provided to it via variables and data sources, but it does not know the future state of the infrastructure. (The exact amount of data available at plan time, particularly from data sources, and the freshness of the data from data sources, has varied over time as Terraform has matured, with the general direction being that more data is available at plan time and less data remains unknown. For example, you can tell Terraform to create a new compute instance, and you can tell it what IP address to assign to that instance, at which point Terraform with know the IP of the instance at plan time. Alternatively, you can not supply an IP address, and the cloud provider will assign one, but then Terraform does not know the IP address at plan time, either. In either case, Terraform will not know the specific instance ID until it creates the instance.
It is important to note that:
- In terms of known versus unknown,
null
was not a special value prior to Terraform version 1.6. If you create a resource, its ID is not known at plan time. Even if you know that successful creation of the resource will result in a non-null ID, Terraform may not, and a test likeid == null
may fail as being unknown at plan time. - Because this behavior is changing in Terraform, but some people are still using older versions or switching to open source forks due to licensing issues, it is important for authors of reusable modules to be aware that this limitation may exist for many users but be invisible to the module author because they are testing their code with a newer version of Terraform.
- Passing values into or out of a module usually does affect whether Terraform
knows the value is
null
or not at plan time. For example, if you had a module that would create an EBS volume when an instance is created, you might have a snippet like this:
resource "aws_ebs_volume" "example" {
count = module.ec2_instance.bastion.id != null ? 1 : 0
# ...
}
In this case, Terraform will not know the value of
module.ec2_instance.bastion.id
at plan time. Passing the instance ID into
a module does not change that. (You could, in Terraform version 1.6,
declare
the input non-nullable (nullable = false
), and then Terraform would know
that the value is not null
at plan time, but if you did that, then you would
never get the id == null
condition and always create the EBS volume, so
that is not a real solution, if you want to make the creation of the EBS
volume conditional)
It is on Hashicorp's roadmap to allow resource providers to declare attributes to be null or non-null at plan time, but you should not rely on that. Rather, you should be aware that your testing may not turn up this issue because you are using later versions of Terraform and providers, but you should still guard against it.
Using the value of a module input to conditionally create resources is a
common source of issues in a reusable module. When the
module is called with a configured value, as can be common when testing, the
module works fine, but if the value is not known at plan time, which is
common in actual use when the value is computed from other resources,
the module will fail. Using a random_integer
with a keeper
of timestamp()
can help you simulate the behavior of a value that is not known at plan
time during testing and catch these kinds of issues ahead of time.
Best practice is to either use a separate boolean input (e.g.
ebs_volume_enabled
) to condition the resource creation, or to take the
optional value as an element of a list and use the length of the list to
determine whether to create the resource. See Use feature flags or lists to
control optional behaviors
for more information.
What is Known and Unknown at Plan Time, Part 2: The Less Obvious
The State of the Infrastructure Is Known at Plan Time
Hopefully it is obvious to see why Terraform would complain about not knowing how many null resources to create in the following example:
resource "random_integer" "example" {
min = 1
max = 2
}
resource "null_resource" "example" {
count = random_integer.example.result
}
(Here we use random_integer
to represent a computed value unknown at
plan time, and null_resoure
to represent a dependent resource, so
that you can easily try these examples on your own.)
Terraform does not know if it should create one or two null resources until
it knows the value of random_integer.example.result
. However, if you run
terraform apply -target=random_integer.example -auto-approve
so that the random_integer.example
resource is created, it becomes
part of the infrastructure, and Terraform has a known value for
random_integer.example.result
at plan time. Therefore, if you run
terraform apply
after that, it will succeed.
This is both the good news and the bad news:
- The good news is that Terraform will not complain about theoretically unknown values in most cases where it can figure out the value during the plan phase. (Not in all cases, though, as we will see below.) This means that if you use a module that uses an input variable to determine how many resources to create, and you provide a value for that input variable that is known at plan time, then Terraform will not complain.
- The bad news is that Terraform will not complain about theoretically unknown values in most cases where it can figure out the value during the plan phase. This means that you can write Terraform code that will work in some cases and not in others, and you may not realize it because it works when you try it.
Known Values Can Be Transformed Into Unknown Values in Non-Obvious Ways
The Results are the Same, but the Path to Get There is Different
The following example is a bit more subtle. Using the same random_integer
resource as above, say we want to choose from 1 of 2 configurations,
rather than directly affect the number of resources created. For
example, we want to configure subnet IDs based on whether the
resource is in public or private subnets. Consider the
following code:
locals {
visibility = random_integer.example.result == 1 ? "public" : "private"
config_map = {
public = {
subnets = ["subnet-0abcd1234efgh5678", "subnet-1abcd1234efgh5679"]
}
private = {
subnets = ["subnet-2abcd1234efgh5678", "subnet-3abcd1234efgh5679"]
}
}
}
resource "null_resource" "example" {
count = length(local.config_map[local.visibility].subnets)
}
This will fail, even though the value of random_integer.example.result
is
irrelevant to the number of resources created. Regardless of the value
of random_integer.example.result
, Terraform should create 2 resources.
However, Terraform is not so sophisticated that it can figure out
that the keys of the possible maps yield lists of the same length,
regardless of which map is chosen. Instead, it will say that the value of
random_integer.example.result
is unknown, so the element of local.config_map
is unknown, and so on.
This is what we referred to above as where Terraform will complain about theoretically unknown values in some cases where it actually could figure out the value during the plan phase.
One Solution to This Particular Problem
locals {
visibility = random_integer.example.result == 1 ? "public" : "private"
config_map = {
public = {
subnets = ["subnet-0abcd1234efgh5678", "subnet-1abcd1234efgh5679"]
}
private = {
subnets = ["subnet-2abcd1234efgh5678", "subnet-3abcd1234efgh5679"]
}
}
subnets = [local.config_map[local.visibility].subnets[0], local.config_map[local.visibility].subnets[1]]
}
resource "null_resource" "example" {
count = length(local.subnets)
}
We saw before that Terraform could not deduce that the length of
local.config_map[local.visibility].subnets
was 2 regardless of the value of
local.visibility
. However, by explicitly creating a list with 2 entries,
Terraform knows the length of the list, and the plan will succeed. This is
not the only way to get around the problem, but it is a common one.
Explicit Transformations of Lists
One good thing about using the length of a list to determine the count is
that the length of the list can be known even if the values of the list are
not. As we saw in the previous example, the length of local.subnets
was
known even if the subnet IDs in the list were not, ant that was sufficient.
On the other hand, transformations of a list with unknown values can make the length of the list once again unknown.
compact
will removenull
values from a list andflatten
will remove empty nested lists (length(flatten(1, [], 2)
is 2), so the length of the list will become unknown unless all of the values are known, even if you as a human can tell there are nonull
values or empty nested lists.distinct
andtoset()
will remove duplicate values from a list, so again the length of the list will become unknown unless all of the values are known.sort
, prior to Terraform version 1.6, would make the length of the list unknown unless all of the values were known.
When providing a list that will be used to determine the number of resources to create, it is important to avoid using any transformations that can cause the length of the list to become unknown.
Implicit Transformations of Maps
For reasons detailed in Terraform Count vs For Each,
it is usually preferable to use for_each
rather than count
to create
multiple resources. However, when using for_each
, it is required that all
of the keys be known at plan time. If you use a list of strings to make the
keys, such as via zipmap
or a for
expression, then the list is implicitly
transformed into a set via the equivalent of distinct(compact(list))
. As
explained above, this will make the
length of the list unknown unless all of the values are known at plan time.
In general, keys to map inputs should be user-supplied configuration values given as inputs, and not computed values. Most of the benefits of using maps over lists are lost if you use computed values as keys, so only use lists where it is likely that users can supply the keys.