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.