Terraform: for_each on a list of resources

Terraform provides a very simply way to use for_each to iterate over a list of resources. If you have a list of strings, use the toset() function to convert the list to a set of strings.

Example: assign a unique role on each resource

My use case is setting up a number of dev environments in Google Cloud Platform. The number may change as the size of the dev team increases, so I don’t want to hard-code the number of resources anywhere in my Terraform code. The number of environments is stored in the num_envs variable. For this example, I want to create a group of resources (Google Cloud Storage buckets). Each environment has its own service account, and I want each environment’s service account to have a specific role on that environment’s bucket. I also want to leverage official Google-supported Terraform modules whenever possible.

First, I define a local variable with a list of names (strings) for “n” environments, so that I don’t have to repeat that code in every resource. Then, I use a module to create an array of service accounts, and assign any desired project roles. Finally, I create the Cloud Storage buckets:

locals {
  bucket_names = [
    for i in range(1, var.num_envs + 1) : format("dev%d-bucket", i)
  ]
}

module "service_accounts" {
  source     = "terraform-google-modules/service-accounts/google"
  version    = "4.1.1"
  project_id = var.project

  names       = local.bucket_names
  description = "Service account for dev environment"
  project_roles = [
    "${var.project}=>roles/your_roles"
  ]
}

module "cloud_storage_buckets" {
  source  = "terraform-google-modules/cloud-storage/google"
  version = "3.1.0"

  project_id    = var.project
  prefix        = var.environment
  location      = "us"
  names         = local.bucket_names
  storage_class = "STANDARD"

  # admins = module.service_accounts.iam_emails_list
  # set_admin_roles = true
}

Note the two commented-out lines in module cloud_storage_buckets. If you un-comment those lines, every service account will be granted the storage.objectAdmin role on every bucket. That doesn’t provide any isolation between the environments, so it’s a no-go. Instead, we’ll use for_each to create an IAM binding so that one service account has one role on its corresponding bucket:

resource "google_storage_bucket_iam_binding" "storage_object_admins" {
  # for_each = { for name in local.sftp_names : name => name }
  for_each = toset(local.sftp_names)
  bucket   = "dev-us-${each.key}"
  members  = [ "serviceAccount:${each.key}@${var.project}.iam.gserviceaccount.com" ]
  role     = "roles/storage.objectAdmin"
}

We have a list of names, but we need a map to use for_each, right? Not quite-it’s easy to forget that for_each also iterates over a set of strings! The first, commented-out line shows how you could construct a map from a list with a for expression. There may be more complex use cases where you have to do that, but it’s a rookie mistake in this case. It’s much simpler and easier to read if you just use the built-in toset function to turn that list of strings into a set of strings.

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.