Loops and iterations
There are three different ways to iterate within HCL. The most common are two meta-arguments, for_each
and count
, which operate on a resource, module, or data source block. At the same time, the third option uses the for
expression, which operations on any collection.
Count
The count
meta-argument is Terraform’s oldest method of iterating resources: an oldie but a goodie. The count
meta-argument is excellent when you want to provision the same block multiple times and have no unique identifier to key off of. In this situation, you will use the item’s index in a list to determine its uniqueness. This approach can pose challenges in the future if the items in the list need to change in such a way that would cause the indices of each item to change.
The best way to manage this is to treat your list as append-only, as this will avoid replacing related resources. Adding or removing items from the middle of the list will cause all the items below that item to shift their index, resulting in destruction and recreation.
For example, if you want to provision a five-node cluster, you wouldn’t remove a specific node from the cluster when you scale down. You would reduce the number of nodes. You don’t care which nodes get removed. You only care how many there are. In this situation, it is ideal to use count
:
resource "aws_instance" "node" { count = var.node_count # the rest of the configuration }
For each
An alternative to count
is the for_each
meta-argument, which allows you to create multiple blocks from a map
collection. This approach can be a distinct improvement over the count
technique because the order of the items in the collection does not matter—only the key. If you update the code to remove the key, Terraform will remove the corresponding item. If the item changes order with other items in the collection, it will not affect Terraform’s plan.
This approach is only possible with a map
collection as the source of the iteration because, with a map
collection type, each item must have a key that uniquely identifies it amongst its peers.
As a result, using for_each
works well when deploying to multiple regions as, typically, you wouldn’t have more than one deployment in the same region; hence, the region name makes an excellent unique key for the map
that drives the for_each
loop. You can add or remove regions without worrying about shifting the index of the items in the collection:
locals { regions = { westus = { node_count = 5 } eastus = { node_count = 8 } } }
Consider the preceding map
configuration. Using this as the collection, we can drive any number of resources, data sources, or modules:
module "regional_deployment" { for_each = local.regions node_count = each.value.node_count # the rest of the configuration }
In the preceding code, we see that we are setting the for_each
source to be the map stored in local.regions
. We then can use the each
prefix anywhere within the module block to access either the key or the value using each.key
and each.value
, respectively. No matter the value’s type, we can address it how we normally would, using each.value
as a reference to the object.
For expressions
The for
expression is a way of iterating within Terraform that does not require you to attach it to a block (i.e., resource, data source, or module). You can use the for
expression to construct in-memory objects to apply object transformations to streamline block-based iteration or for output.
Iterating over a list
When iterating over a list
, you must specify only one parameter to the for
expression. This parameter will represent each item within your list
so that you can access each item within the output block:
region_names_list = [ for s in var.regions : upper("${s.region}${s.country}")]
In the preceding example, we are iterating over all the objects in var.regions
. As we do, during each iteration, the current value is accessible in the s
parameter. We can use the output block to generate any object we desire to be created in the new list that this for
expression will create.
Iterating over a map
When iterating over a map
, you must change how you structure your for
expression. You must specify two instead of one parameter declared immediately after the for
keyword:
region_array_from_map = [ for k, v in var.regions : { region = k, address_space = v.address_space node_count = v.node_count } ]
In the preceding example, you’ll see that we specify two parameters for the for
expression: k
and v
. We chose these names as a convention to help us remember what these variables mean within the scope of the for
expression. k
represents the map’s key, while v
represents the value. The value can be any type, be it a primitive, collection, or complex object. If we want to access the value
object, we access it based on its type. In this example, the value is a complex object with two attributes. In the for
expression’s output block, we specify the structure of the object we want each item in the resulting array to have.
In this case, we are creating an array of objects with three attributes: region
, address_space
, and node_count
, essentially flattening the original map into an array of objects. The output looks like this:
region_array_from_map = [ { "address_space" = "10.0.1.0/24" "node_count" = 5 "region" = "eastus" }, { "address_space" = "10.0.0.0/24" "node_count" = 8 "region" = "westus" }, ]
Outputting a list
The for
expression will always output either a list
or an object. You can select the output type you want by the character in which you wrap the for
block. If you wrap the for
expression in square brackets, then the expression will output a list
:
region_list = [for s in var.regions : "${s.region}${s.country}"]
The preceding for
expression will produce the following output:
region_list = [ "westus", "eastus", ]
Sometimes, the names of the module or resource outputs don’t align precisely with other resources’ desired inputs. Therefore, using a for
expression and outputting a list can help transform these incongruent output values into a format convenient for consumption within another part of your code.
Outputting an object
Wrapping the for
expression with curly braces will output an object:
locals { region_config_object = { for s in var.regions : "${s.region}${s.country}" => { node_count = s.node_count } } }
This approach will output an object with attributes for each item in the list of regions in the regions
input variable. Each attribute will take the name of the concatenation of the region and country names, and its value will be an object with a single attribute called node_count
. The output will look like this:
region_config_object = { "eastus" = { "node_count" = 8 } "westus" = { "node_count" = 8 } }
Outputting an object can be very useful in scenarios where you need to generate a JSON or YAML payload. You can reference this payload in another resource or output it so another tool can extract that value from Terraform using the terraform
output
command.
Converting a list to a map
One common problem is converting a list into a map. This is needed because, while a list is sometimes the most concise way of storing a simple collection, it cannot be used with the for_each
iterator. Therefore, if you want to have your cake and eat it too, you need to convert that list into a map. This can be done with a simple for
expression that iterates over the list in memory and outputs a map:
locals { foo_list = ["A", "B", "C"] foo_map = { for idx, element in local.foo_list : element => idx } }
In the preceding code, we are invoking the for
expression and outputting an object using curly braces ({}
). We are taking each element within the list and setting it as the key of our map
and taking the element’s index within the list
and setting it as the value. It’s important to note that this will only work when the items in the list
are not duplicates.
Now that we know how to loop, swoop, iterate, and cross-mojinate, we can avoid the pitfalls of copypasta by leveraging Terraform’s three extremely powerful iterators—count
, for_each
, and for
—to build dynamic collections of resources, data sources, or anything really!
We are nearing the end of our journey into the depths of HCL. Next, we will look at a few more language expressions that help us cope when we want to use dynamic collections and conditional logic to jazz up our modules!