The "for_each" value depends on resource attributes that cannot be determined until apply

As so many people before me (and most probably after me), I ran into the following dreaded problem:

The "for_each" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. To work around this, use the -target argument to first apply only the resources that the for_each depends on.

I did some digging and I must admit that no explanation really gives full insights into what the actual problem is. I thought I saw someone mentioning somewhere that for_each can’t rely on variable parameters, which seems a bit silly as that is maybe one of the only reasons it’s useful to use (or at least a very important one).

I’ll illustrate with an example. In my code I’m creating a number of service accounts:

resource "google_service_account" "service_account_one" {
  project     = var.project_id
  account_id  = "sa-one"
}

resource "google_service_account" "service_account_two" {
  project     = var.project_id
  account_id  = "sa-two"
}

Now I want to give these service accounts a set of roles on particular objects. For example, granting them access to a storage bucket. Instead of copy/pasting the same resource 3 times, I thought I’d be smart and solve it with a locals list.

locals {
  service_account_emails = [
    google_service_account.service_account_one.email,
    google_service_account.service_account_two.email,
  ]
}

And then give them the desired IAM role:

resource "google_storage_bucket_iam_member" "configuration_access" {
  for_each = toset(local.service_account_emails)
  bucket   = google_storage_bucket.configuration_bucket.name
  member   = "serviceAccount:${each.value}"
  role     = "roles/storage.admin"
}

But, as you already guessed:

╷
│ Error: Invalid for_each argument
│ 
│   on ../../../main.tf, in resource "google_storage_bucket_iam_member" "configuration_access":
│  345:   for_each = toset(local.service_account_emails)
│     ├────────────────
│     │ local.service_account_emails is tuple with 2 elements
│ 
│ The "for_each" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. To work around this, use the -target argument to first apply only the resources that the for_each depends on.
╵

So, can someone shed some light on what is happening in the background here? As you can see, it’s a fixed list with 2 elements, so I have no idea why Terraform can’t predict how many elements are going to be created.

This isn’t the first time I’m running into this problem. I have other examples where it’s problematic as well, especially with flattened lists and passing in variables into a module.

Is there work underway to solve this problem or at least give a better error message and guidance on how to solve this?

Hi @debakkerbjorn,

The problem here is not that terraform cannot determine the length of the list, in fact you could use count = length(local.service_account_emails) and it should work just fine. The problem is that the values of the list are unknown, so there is no way to determine what the index key for each of your resource instances is going to be.

The simplest resolution here, since you already are statically defining the structure of the values, is to use a map with predefined keys rather than a set.

locals {
  service_account_emails = {
    one = google_service_account.service_account_one.email,
    two = google_service_account.service_account_two.email,
  }
}
1 Like

Wanted to say that this solution saved me a lot of headache trying to solve this for_each problem. Changing it to a map with the keys did indeed satisfy Terraform to ignore the “unknown” number of objects generated from my other modules I was passing in. Thank you!

Rather than manually declaring keys, you can do something like this

resource "google_storage_bucket_iam_member" "configuration_access" {
  for_each = {for i, val in local.service_account_emails: i => val}
  bucket   = google_storage_bucket.configuration_bucket.name
  member   = "serviceAccount:${each.value}"
  role     = "roles/storage.admin"
}

Honestly, I think the many, many issues I see after Googling this error are largely caused by the suggestion in the documentation to use toset(). Converting your list to a map instead of a set is simply better.

I’ve put my concern in another discussion here, though honestly I don’t know if this is the right forum for it.

@jkemsley,

While valid, the drawback to your approach is that you are directly coupling the resource instances to the order of the values in service_account_emails. This means that any change to the order will destroy and recreate the resources, and adding or removing any values from the list will destroy and recreate all values later in the list. What you have is exactly equivalent to count, only formatting the indexes as strings:

resource "google_storage_bucket_iam_member" "configuration_access" {
  count =  length(local.service_account_emails)
  bucket   = google_storage_bucket.configuration_bucket.name
  member   = "serviceAccount:${local.service_account_emails[count.index]}"
  role     = "roles/storage.admin"
}

Yep, you’re right on that. I was mistaken on what was happening inside for_each. Thanks for catching that