I work a lot with iterators in Terraform, they always gave me bad headaches. Therefore I identified five of the most common iterator patterns (code examples are given below), which helped me construct a lot of nice modules (source).
- Using for_each on a list of strings
- Using for_each on a list of objects
- Using for_each to combine two lists
- Using for_each in a nested block
- Using for_each as a conditional
Using for_each
and a list of strings is the easiest to understand, you can always use the toset()
function. When working with a list of objects you need to convert it to a map
where the key is a unique value. The alternative is to put a map inside your Terraform configuration. Personally, I think it looks cleaner to have a list of objects instead of a map in your configuration. The key usually doesn't have a purpose other than to identify unique items in a map, which can thus be constructed dynamically. I also use iterators to conditionally deploy a resource or resource block, especially when constructing more complex modules.
1. Using for_each on a list of strings
locals {
ip_addresses = ["10.0.0.1", "10.0.0.2"]
}
resource "example" "example" {
for_each = toset(local.ip_addresses)
ip_address = each.key
}
2. Using for_each on a list of objects
locals {
virtual_machines = [
{
ip_address = "10.0.0.1"
name = "vm-1"
},
{
ip_address = "10.0.0.1"
name = "vm-2"
}
]
}
resource "example" "example" {
for_each = {
for index, vm in local.virtual_machines:
vm.name => vm # Perfect, since VM names also need to be unique
# OR: index => vm (unique but not perfect, since index will change frequently)
# OR: uuid() => vm (do NOT do this! gets recreated everytime)
}
name = each.value.name
ip_address = each.value.ip_address
}
3. Using for_each to make the Cartesian product of two lists
locals {
domains = [
"https://example.com",
"https://stackoverflow.com"
]
paths = [
"/one",
"/two",
"/three"
]
}
resource "example" "example" {
# Loop over both lists and flatten the result
urls = flatten([
for domain in local.domains : [
for path in local.paths : {
domain = domain
path = path
}
]
]))
}
4. Using for_each on a nested block
# Using the optional() keyword makes fields null if not present
variable "routes" {
type = list(
name = string
path = string
config = optional(object({
cache_enabled = bool
https_only = bool
}))
default = []
}
resource "example" "example" {
name = ...
dynamic "route" {
for_each = {
for route in var.routes :
route.name => route
}
content {
# Note: <top_level_block>.value.<object_key>
name = route.value.name
}
dynamic "configuration" {
# Note: <top_level_block>.value.<optional_object_key>
for_each = route.value.config != null ? [1] : []
content {
cache_enabled = route.value.config.cache_enabled
https_only = route.value.config.https_only
}
}
}
5. Using for_each as a conditional (particularly for dynamic blocks)
variable "deploy_example" {
type = bool
description = "Indicates whether to deploy something."
default = true
}
# Using count and a conditional, for_each is also possible here.
# See the next solution using a for_each with a conditional.
resource "example" "example" {
count = var.deploy_example ? 0 : 1
name = ...
ip_address = ...
}
variable "enable_logs" {
type = bool
description = "Indicates whether to enable something."
default = false
}
resource "example" "example" {
name = ...
ip_address = ...
# Note: dynamic blocks cannot use count!
# Using for_each with an empty list and list(1) as a readable alternative.
dynamic "logs" {
for_each = var.enable_logs ? [] : [1]
content {
name = "logging"
}
}
}
if
,for
orwhile
statements. You can simulate these however, to some extent, in some cases thanks to thecount
property. Read this excellent article about how to reproduce conditions or loops with Terraform: blog.gruntwork.io/… – Weinrebfor_each
blocks: 0.12.6 Changelog. For what it's worth, if/else conditionals have been in Terraform for a couple of versions now: Terraform docs: Conditional Expressions – Organize