user6308605
user6308605

Reputation: 731

What is the workaround of using for each and count together in Terraform?

I have two conditions need to be fulfilled:

  1. Grant users permission to specific project-id based on env. For example: my-project-{env} (env: stg/prd)
  2. I want to loop over the variables, instead of writing down repetitive resource for each user.

Example:

variable some_ext_users {
  type = map(any)
  default = {
    user_1 = { email_id = "[email protected]" }
    user_2 = { email_id = "[email protected]" }
  }
}

To avoid repetitive resource made on each user (imagine 100++ users), I decided to list them in variable as written above.

Then, I'd like to assign these user GCS permission, e.g:

resource "google_storage_bucket_iam_member" "user_email_access" {
  for_each = var.some_ext_users
  count    = var.env == "stg" ? 1 : 0
  provider = google-beta
  bucket   = "my-bucketttt"
  role     = "roles/storage.objectViewer"
  member   = "user:${each.value.email_id}"
}

The error I'm getting is clear :

Error: Invalid combination of "count" and "for_each" on ../../../modules/my-tf.tf line 54, in resource "google_storage_bucket_iam_member" "user_email_access": 54:
for_each = var.some_ext_users The "count" and "for_each" meta-arguments are mutually-exclusive, only one should be used to be explicit about the number of resources to be created.

My question is, what is the workaround in order to satisfy the requirements above if count and for_each can't be used together?

Upvotes: 0

Views: 814

Answers (2)

Martin Atkins
Martin Atkins

Reputation: 74169

The rule for for_each is to assign it a map that has one element per instance you want to declare, so the best way to think about your requirement here is that you need to write an expression that produces a map with zero elements when your condition doesn't hold.

The usual way to project and filter collections in Terraform is for expressions, and indeed we can use a for expression with an if clause to conditionally filter out unwanted elements, which in this particular case will be all of the elements:

resource "google_storage_bucket_iam_member" "user_email_access" {
  for_each = {
    for name, user in var.some_ext_users : name => user
    if var.env == "stg"
  }

  # ...
}

Another possible way to structure this would be to include the environment keywords as part of the data structure, which would keep all of the information in one spot and potentially allow you to have entries that apply to more than one environment at once:

variable "some_ext_users" {
  type = map(object({
    email_id     = string
    environments = set(string)
  }))
  default = {
    user_1 = {
      email_id     = "[email protected]"
      environments = ["stg"]
    }
    user_2 = {
      email_id     = "[email protected]"
      environments = ["stg", "prd"]
    }
  }
}

resource "google_storage_bucket_iam_member" "user_email_access" {
  for_each = {
    for name, user in var.some_ext_users : name => user
    if contains(user.environments, var.env)
  }

  # ...
}

This is a variation of the example in the "Filtering Elements" documentation I linked above, which uses an is_admin flag in order to declare different resources for admin users vs. non-admin users. In this case, notice that the if clause refers to the symbols declared in the for expression, which means we can now get a different result for each element of the map, whereas the first example either kept all elements or no elements.

Upvotes: 1

lxop
lxop

Reputation: 8595

You could control the user list according to the environment, rather than trying to control the resource. So, something like this:

resource "google_storage_bucket_iam_member" "user_email_access" {
  for_each = var.env == "stg" ? var.some_ext_users : {}
  provider = google-beta
  bucket   = "my-bucketttt"
  role     = "roles/storage.objectViewer"
  member   = "user:${each.value.email_id}"
}

Upvotes: 1

Related Questions