Reading Time: 4 minutes
In this blog, we are going to learn about how to use FOR loops in terraform.
INTRODUCTION
- As we know, Terraform is a declarative language.
- Infrastructure-as-code in a declarative language tends to provide a more accurate about deployed items.
- It is easier to reason about and makes it easier to keep the codebase small.
- However, without access to a full programming language, certain types of tasks become more difficult in a declarative language.
- For example, since declarative languages typically don’t have for-loops, how do we repeat a piece of logic — such as creating multiple similar EC2 Instances — without copy and paste?
- And if the declarative language doesn’t support if-statements, how can we conditionally configure resources, such as creating public IP addresses for frontend services, but not for backend services?
- Fortunately, Terraform provides a few primitives—namely, the
count
meta-parameter,for_each
andfor
expressions. - A lifecycle block called
create_before_destroy
, a ternary operator, plus a large number of functions—that allow us to do certain types of loops, if-statements, and other logic. - Here are the topics we’ll go over:
- Loops
Loops with for_each expressions
- The
for_each
expression allows you to loop over lists, sets and maps to create either multiple copies of an entire resource. - It also allows us to create multiple copies of an inline-block within a resource.
- Let’s first walk through how to use
for_each
to create multiple copies of a resource. - The syntax looks like this:
resource "<PROVIDER>_<TYPE>" "<NAME>" {
for_each = <COLLECTION>
[CONFIG ...]
}
- PROVIDER is the name of a provider (e.g., AWS).
- TYPE is the type of resource to create in that provider (e.g., instance).
- NAME is an identifier we can use throughout the Terraform code to refer to this resource (e.g., my_instance).
- COLLECTION is a set or map to loop over (lists are not supported when using for_each on a resource).
- CONFIG consists of one or more arguments that are specific to that resource.
- Within CONFIG, we can use
each.key
andeach.value
. - It helps us to access the key and value of the current item in COLLECTION.
For example, here’s how we can create the same three IAM users using for_each
:
resource "aws_iam_user" "example" {
for_each = toset(var.user_names)
name = each.value
}
- Note the use of
toset
to convert thevar.user_names
list into a set. - As
for_each
only supports sets and maps when used on a resource. - When
for_each
loops over this set, it will make each user name available ineach.value
. - The user name will also be available in
each.key
. - Though we typically only use
each.key
with maps of key/value pairs.
Once we have used for_each
on a resource, it becomes a map of resources, rather than just one resource (or a list of resources as with count
).
To see what that means, remove the original all_arns
and neo_arn
output variables, and add a new all_users
output variable:
output "all_users" {
value = aws_iam_user.example
}
Here’s what happens when we run terraform apply
:
$ terraform apply
(...)
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Outputs:
all_users = {
"morpheus" = {
"arn" = "arn:aws:iam::123456789012:user/morpheus"
"force_destroy" = false
"id" = "morpheus"
"name" = "morpheus"
"path" = "/"
"tags" = {}
}
"neo" = {
"arn" = "arn:aws:iam::123456789012:user/neo"
"force_destroy" = false
"id" = "neo"
"name" = "neo"
"path" = "/"
"tags" = {}
}
"trinity" = {
"arn" = "arn:aws:iam::123456789012:user/trinity"
"force_destroy" = false
"id" = "trinity"
"name" = "trinity"
"path" = "/"
"tags" = {}
}
}
- We can see that Terraform created three IAM users
all_users
output variable contains a map where the keys are the keys infor_each
(in this case, the user names) and the values are all the outputs for that resource.- If we want to bring back the
all_arns
output variable, we have to do a little extra work to extract those ARNs using thevalues
built-in function (which returns just the values from a map) and a splat expression:
output "all_arns" {
value = values(aws_iam_user.example)[*].arn
}
Which gives you the expected output:
terraform apply
(...)
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
all_arns = [
"arn:aws:iam::123456789012:user/morpheus",
"arn:aws:iam::123456789012:user/neo",
"arn:aws:iam::123456789012:user/trinity",
]
- The fact that we now have a map of resources with
for_each
rather than a list of resources as withcount
is a big deal, as it allows you to remove items from the middle of a collection safely.
For Each with mutiple line blocks
- Let’s now turn our attention to another advantage of
for_each
: - Its ability to create multiple inline blocks within a resource.
- For example, we can use
for_each
to dynamically generatetag
inline blocks for the ASG in thewebserver-cluster
module. - First, to allow users to specify custom tags, add a new map input variable called
custom_tags
:
variable "custom_tags" {
description = "Custom tags to set on the Instances in the ASG"
type = map(string)
default = {}
}
- How do we actually set these tags on the
aws_autoscaling_group
resource? - What we need is a for loop over
var.custom_tags
, similar to the following pseudo code:
resource "aws_autoscaling_group" "example" {
# (...)
# This is just pseudo code. It won't actually work in Terraform.
for (tag in var.custom_tags) {
tag {
key = tag.key
value = tag.value
propagate_at_launch = true
}
}
}
- The pseudo code above won’t work, but a
for_each
expression will. - The syntax for using
for_each
to dynamically generate inline blocks looks like this:
dynamic "<VAR_NAME>" {
for_each = <COLLECTION>
content {
[CONFIG...]
}
}
- where
VAR_NAME
is the name to use for the variable that will store the value each “iteration” (instead ofeach
). COLLECTION
is a list or map to iterate over, and thecontent
block is what to generate from each iteration.- We can use
<VAR_NAME>.key
and<VAR_NAME>.value
within thecontent
block to access the key and value, respectively, of the current item in theCOLLECTION
. - Note that when we are using
for_each
with a list, thekey
will be the index and thevalue
will be the item in the list at that index, - And when using
for_each
with a map, thekey
andvalue
will be one of the key-value pairs in the map. - Putting this all together, here is how we can dynamically generate
tag
blocks usingfor_each
in theaws_autoscaling_group
resource:
resource "aws_autoscaling_group" "example" {
# (...)
dynamic "tag" {
for_each = var.custom_tags
content {
key = tag.key
value = tag.value
propagate_at_launch = true
}
}
}
Conclusion
- In the above blog, we got to know about for-each loops in terraform and how to use for-each loops.