← Back to all posts

Terraform: Creating multiple resources with iteration

#Terraform #OpenTofu #GoogleCloud

It's easy to forget that you can actually still program (a little) with Terraform. One way I like is to use the count and for_each meta arguments with resources.

Let's for example create an simple instance in GCP

resource "google_compute_disk" "simple_disk" {
  name  = "simple_boot_disk"
  type  = "pd-standard"
  zone  = "us-central1-a"
  size  = 20
  image = "ubuntu-os-cloud/ubuntu-2204-lts"
}

resource "google_compute_instance" "simple_instance" {
  name         = "simple-instance"
  machine_type = "n4-standard-2"
  zone         = "us-central1-a"

  boot_disk {
    auto_delete = "true"
    device_name = "simple_boot_disk"
    mode        = "READ_WRITE"
    source      = google_compute_disk.simple_disk.self_link
  }
}
# Terraform resouces that will be created:

google_compute_disk.simple_disk
google_compute_instance.simple_instance

This works! Terraform will create a boot disk and also an instance using that boot disk. But what if we want to create multiples of the same resources? To do this we can use the count meta argument.

resource "google_compute_disk" "simple_disk" {
  count = 2

  name  = "simple_boot_disk_${count.index}"
  type  = "pd-standard"
  zone  = "us-central1-a"
  size  = 20
  image = "ubuntu-os-cloud/ubuntu-2204-lts"
}

resource "google_compute_instance" "simple_instance" {
  count = 2

  name         = "simple-instance-${count.index}"
  machine_type = "n4-standard-2"
  zone         = "us-central1-a"

  boot_disk {
    auto_delete = "true"
    device_name = "simple_boot_disk"
    mode        = "READ_WRITE"
    source      = google_compute_disk.simple_disk[count.index].self_link
  }
}
# Terraform resouces that will be created:

google_compute_disk.simple_disk["0"]
google_compute_disk.simple_disk["1"]
google_compute_instance.simple_instance["0"]
google_compute_instance.simple_instance["1"]

Perfect! This is an easy way to tell terraform that we want multiples of similar resources. Notice that when we need to reference them in our terraform we have to pass the index like this resource_type.name[index] to tell terraform which one we want to access.

Now what if we wanted to have multiple resources, but they have different arguments for some of the settings? While count gets the job done for simple situations, for_each allows us to iterate over maps or sets of strings.

Lets try this again but instead setup a map with a local and iterate over it with for_each.

locals {
  simple_servers = {
    server1 = {
      disk_type    = "pd-standard"
      disk_size    = 20
      zone         = "us-central1-a"
      machine_type = "n4-standard-2"
    }
    server2 = {
      disk_type    = "pd-ssd"
      disk_size    = 100
      zone         = "us-central1-b"
      machine_type = "n4-standard-8"
    }
  }
}

resource "google_compute_disk" "simple_disk" {
  for_each = local.simple_servers

  name  = "simple_boot_disk_${each.key}"
  type  = each.value.disk_type
  zone  = each.value.zone
  size  = each.value.disk_size
  image = "ubuntu-os-cloud/ubuntu-2204-lts"
}

resource "google_compute_instance" "simple_instance" {
  for_each = local.simple_servers

  name         = "simple-instance-${each.key}"
  machine_type = each.value.machine_type
  zone         = each.zone

  boot_disk {
    auto_delete = "true"
    device_name = "simple_boot_disk"
    mode        = "READ_WRITE"
    source      = google_compute_disk.simple_disk[each.key].self_link
  }
}
# Terraform resouces that will be created:

google_compute_disk.simple_disk["server1"]
google_compute_disk.simple_disk["server2"]
google_compute_instance.simple_instance["server1"]
google_compute_instance.simple_instance["server2"]


Awesome! We now have way more flexability while customizing our resources. By default for_each will set the index of each resource to the key of the current element being iterated over. So when we want to reference another resource that is created with our map, we use the same syntax as before but with each.key as the index. Since the key for our current resource will match the key of the referenced resource.

Lastly, in some specific situations you might want to control the index in our for_each. To do this you use the syntax below.

for_each = {
  for item in local.items :
  item.name => item
}

This looks a little confusing but we can break it down. We are creating our own for loop over a map named local.items. Then we are setting our own index with one of item values item.name and finally pointing the index at the item in the map.

We are essentially dynamically creating our own map in our for_each loop! Like I mentioned before this is extreamly situational but it's good to know how to do this.

Lets update our main code to do this and use the zone as our index.

locals {
  simple_servers = [
    {
      disk_type    = "pd-standard"
      disk_size    = 20
      zone         = "us-central1-a"
      machine_type = "n4-standard-2"
    },
    {
      disk_type    = "pd-ssd"
      disk_size    = 100
      zone         = "us-central1-b"
      machine_type = "n4-standard-8"
    }
  ]
}

resource "google_compute_disk" "simple_disk" {
  for_each = {
    for server in local.simple_servers :
    server.zone => server
  }

  name  = "simple_boot_disk_${each.value.zone}"
  type  = each.value.disk_type
  zone  = each.value.zone
  size  = each.value.disk_size
  image = "ubuntu-os-cloud/ubuntu-2204-lts"
}

resource "google_compute_instance" "simple_instance" {
  for_each = {
    for server in local.simple_servers :
    server.zone => server
  }

  name         = "simple-instance-${each.value.zone}"
  machine_type = each.value.machine_type
  zone         = each.zone

  boot_disk {
    auto_delete = "true"
    device_name = "simple_boot_disk"
    mode        = "READ_WRITE"
    source      = google_compute_disk.simple_disk[each.key].self_link
  }
}
# Terraform resouces that will be created:

google_compute_disk.simple_disk["us-central1-a"]
google_compute_disk.simple_disk["us-central1-b"]
google_compute_instance.simple_instance["us-central1-a"]
google_compute_instance.simple_instance["us-central1-b"]

Note: since we set the index to the value of zone, we can access that value with either each.value.zone or each.key