Skip to main content

Three flavors of Terraform iteration

·941 words·5 mins
Terraform Platform

I was writing a Terraform module to create a Google Cloud Load Balancer with an arbitrary set of GKE services as backends. To achieve this, I needed to learn about 3 different methods of iteration that are supported in Terraform, when to use each of them. If you’d like to better understand the many flavors of iteration available in Terraform, this article can help!

A bit of background
#

My goal with this terraform module is to turn a list of kubernetes_service objects into a google_compute_backend_service. This requires a few intermediate objects, and some json parsing in the terraform. If you’re curious about the outcome of this work, you can check out the Reliable App Platforms github repository.

While I did eventually find the information I needed in the Terraform docs (each of which is linked below), I felt there was little comparison between the different types and when to use each of them, so I put together this brief introduction.

These three flavors of iteration all have their uses - choosing the right one depends on the circumstances. I have used all three of these methods, sometimes in the same piece of Terraform. Let’s get started!

Resources: for_each
#

If you’ve done much with Terraform before, you’ve probably encountered the for_each style of iteration. For_each is supported by all resources, and is a useful way to create a resource for each item in a list.

For example, to create kubernetes_service data objects from a list of kubernetes service names, you can do this:

data "kubernetes_service" "services" {
  for_each = toset(["one", "two", "three"])
  metadata {
    name = each.value
  }
}

# you can now refer to these services individually:
# data.kubernetes_service.services["three"]

This type of iteration is great if you need to turn a list into a set of resources (for example, make a set of Cloud Storage buckets from a list of names). The terraform docs cover the details of for_each.

If you’re not creating resources, you should consider one of the other methods, like a for expression.

Lists: for expressions and splat expressions
#

For Expressions are commonly used to filter items from a list, or change the shape of your data.

For example, suppose you have a list of regional Cloud Storage Buckets like this:

buckets = [
  { name = "bucket1", region = "US-EAST4" },
  { name = "bucket2", region = "EUROPE-WEST3" },
]

You would rather have this as a map indexed by region, so your application uses the closest regional bucket. This can be done like so:

buckets_by_region = { for b in buckets: b.region => b }
# buckets_by_region["US-EAST4"].name == "bucket1"

Or perhaps you want to select only the buckets located in Europe:

european_buckets = [ for b in buckets: b if startswith(b.region, "EUROPE") ]

If you want the list of regions in which there is a bucket, you could use a For expression:

regions_with_buckets = [ for b in buckets : b.region ]

Or use the alternative syntax for this, which is called a Splat Expression:

regions_with_buckets = buckets[*].region

‘For’ and ‘Splat’ expressions are great for data manipulation like filtering, and creating maps from an unordered list. They are also great for getting data in the right “shape” for one of the other iteration flavors.

Repeated blocks: dynamic blocks
#

The last type of iteration is useful for tricky scenarios where you need to repeat an inner block. I came across this with the backend block of a google_compute_backend_service. I wanted to produce a section like this:

resource "google_compute_backend_service" "my_service" {
  # other fields omitted for brevity
  backend { group = "group1" }
  backend { group = "group2" }
}

Because the repeated block here is not the resource itself, we cannot use a for_each to repeat it. To repeat an inner block, we need a new type of iteration, which Terraform calls dynamic blocks. These are a variant of for_each, but instead of creating resources, they create blocks.

resource "google_compute_backend_service" "my_service" {
  dynamic "backend" {
    for_each = ["group1", "group2" ]
    iterator = "thing"
    content {
      group = thing.value
    }
  }
}

The above will create one backend block for each item in var.backends. The iterator argument can be used to name the temporary object in each iteration.

This method is really helpful when you need to repeat a block inside a resource (rather than the resource itself).

Bonus: iteration helpers with terraform_data objects
#

While I was doing all this iteration, I tried to use local variables in a Terraform locals block. Unfortunately, local variables do not allow for_each expressions, so I needed another solution.

In the past, I’ve used null_resource for this, but as of Terraform 1.4, the terraform_data resource type is preferred.

Because I’m only using this as an iteration-safe locals block, I avoided the use of triggers_replace, and just used the input argument to hold my per-loop local variables.

resource "terraform_data" "iter-helpers" {
  for_each = var.backends
  input = {
    svc_port = tostring(each.value.service_obj.spec.0.port.0.port)
  }
}

To use the terraform_data objects, you can reference the output attribute:

resource "some_resource" "list_of_things" {
  # iterate through our list of helpers
  for_each = terraform_data.iter-helpers
  # and select the svc_port attribute
  port = each.value.output.svc_port
}

This gives me a “fake” resource that I can iterate over with any of the above iteration flavors.

Conclusion
#

I hope this quick introduction helped you understand the differences between for_each, for/splat expressions, and dynamic blocks, so you can choose the right flavor of iteration next time you’re working with terraform.

If you’re interested in Platform Engineering (sometimes with Terraform), you can follow me on Medium, or check out the Google Cloud Community publication for a broader range of topics.