Terraform loop patterns with for_each
Mon Oct 17 2022Posted by Cloudkollektiv at How to for_each through a list(objects) in Terraform 0.12.
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 combine 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
})) # This nested object is optional
default = []
}
resource "example" "example" {
name = ...
dynamic "route" {
for_each = {
for route in var.routes :
route.name => route
}
content {
name = route.value.name # <top_level_block>.value.<object_key>
}
dynamic "configuration" {
for_each = route.value.config != null ? [1] : [] # <top_level_block>.value.<optional_object_key>
content {
cache_enabled = route.value.config.cache_enabled
https_only = route.value.config.https_only
}
}
}
5. Using for_each as a conditional
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"
}
}
}