TrademarkTrademark
Features
Documentation

Dynamic Expressions in Terraform: Some Real-World Examples

In Terraform, an expression refers to or computes values within configurations.
Brendan ThompsonNovember 25, 2022
Dynamic Expressions in Terraform: Some Real-World Examples
Key takeaways
  • Dynamic expressions in Terraform compute values that change based on inputs or environment, letting your code decide how many resource instances to create.
  • A conditional expression on for_each, such as var.is_highly_available ? { enabled = true } : {}, creates a resource instance only when the condition is true.
  • The try and compact functions let you collect resource IDs into a list without caring whether one, two, or many instances were provisioned.
  • An if filter inside a for_each comprehension can conditionally create instances, for example checking that an instance is enabled and its region is in an allowed list.

In Terraform, an expression refers to or computes values within a configuration. That can be something as simple as a literal value like "hello, world!", or as involved as conditionally returning exported attributes from a resource or data source.

This post looks at dynamic expressions: expressions whose result depends on the environment or on the inputs you pass in, so it can change from one run to the next.

We'll start with a simple example. The goal is to use a conditional expression to decide whether one or two instances of a resource get created.

In our examples we will be using the scratch provider to demonstrate the concepts.

variable "is_highly_available" {
  type        = bool
  description = <<DESC
    (Optional) if the solution is highly available
    [Default: false]
  DESC
  default     = false
}
 
resource "scratch_string" "primary" {
  in = format("Primary Instance - var.is_highly_available = '%s'", var.is_highly_available)
}
 
resource "scratch_string" "secondary" {
  for_each = var.is_highly_available ? { enabled = true } : {}
 
  in = format("Secondary Instance - var.is_highly_available = '%s'", var.is_highly_available)
}

In the above example we have a variable defined is_highly_available this is something that we would set when running our code to allow for our resources to be configured in a highly available fashion. The first (or primary) instance does not do any conditional checks as we can safely assume we always want the primary instance to be provisioned. The secondary instance as you can see has a conditional as the value for its for_each.

var.is_highly_available ? { enabled = true } : {}

On the left-hand side we check whether var.is_highly_available is true. If it is, we create a single secondary instance; if it's false, we don't create one at all.

Now suppose we need to do something with the IDs our resources produce, and we don't want to care whether there are one, two, or a hundred instances. In the example below we want to act on a list of the id property. The try and compact functions give us a dynamic expression here. If only the primary instance is provisioned, the list returns a single value; if the secondary instance exists too, we get both IDs back.

resource "scratch_list" "dependent_resource" {
  in = compact([
    scratch_string.primary.id,
    try(scratch_string.secondary.*.id, "")
  ])
}

Let's look at another example, where we have the following local block:

locals {
  instances = {
    primary = {
      enabled = true
      region  = "australiaeast"
    }
    secondary = {
      enabled = true
      region  = "australiasoutheast"
    }
    tertiary = {
      enabled = true
      region  = "australiacentral1"
    }
  }
}

This describes our instances. We might pass it in via an input variable, or keep it static in local variables as we have above. We'll create the instances and use a dynamic expression to work out whether each one should be created. To start, we just check whether the instance is enabled.

resource "scratch_string" "this" {
  for_each = {
    for k, v in local.instances :
    k => v
    if v.enabled
  }
 
  in = each.key
}

This produces three instances, since they're all marked as enabled. The dynamic part is the if check on the for_each, which runs a conditional (or a set of conditionals, as we'll see in a moment) against our input. What if we also had a restriction on where resources could be deployed?

locals {
  allowed_regions = ["australiaeast", "australiasoutheast"]
}

We'll use this list to further influence Terraform's decision on when a resource should be created. Here it is in action.

resource "scratch_string" "this" {
  for_each = {
    for k, v in local.instances :
    k => v
    if v.enabled && contains(local.allowed_regions, v.region)
  }
 
  in = each.key
}

As you can see we now have the && operator in place and are checking to see if our region is on our allowed list! This is where the power of dynamic expressions starts to come to life for me.

Closing Out

Dynamic expressions let our Terraform code make some of these decisions for us. Instead of hard-coding how many instances of a resource exist, we turn them on or off with conditional expressions. We can also merge the results together without tracking how many instances ended up being created.

About the author
Brendan Thompsonsolutions engineer at Scalr
Brendan Thompson is a solutions engineer at Scalr, specializing in Terraform and cloud infrastructure.