Terraform: Loops with For Expressions

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 and for 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 and each.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 the var.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 in each.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 in for_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 the values 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 with count 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 generate tag inline blocks for the ASG in the webserver-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 of each).
  • COLLECTION is a list or map to iterate over, and the content block is what to generate from each iteration.
  • We can use <VAR_NAME>.key and <VAR_NAME>.value within the content block to access the key and value, respectively, of the current item in the COLLECTION.
  • Note that when we are using for_each with a list, the key will be the index and the value will be the item in the list at that index,
  • And when using for_each with a map, the key and value will be one of the key-value pairs in the map.
  • Putting this all together, here is how we can dynamically generate tag blocks using for_each in the aws_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.

Written by 

Am a technology enthusiast having 3+ years of experience. I have worked on Core Java, Apache Flink, Apache Beam, AWS, GCP, Kafka, Spark, MySQL. I am curious about learning new technologies.

Discover more from Knoldus Blogs

Subscribe now to keep reading and get access to the full archive.

Continue reading