Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

for_each cannot predict how many instances with lookup and resource reference #25700

Closed
kimor79 opened this issue Jul 29, 2020 · 10 comments
Closed
Labels
bug config explained a Terraform Core team member has described the root cause of this issue in code v0.12 Issues (primarily bugs) reported against v0.12 releases v0.13 Issues (primarily bugs) reported against v0.13 releases

Comments

@kimor79
Copy link

kimor79 commented Jul 29, 2020

Terraform Version

Terraform v0.12.29
+ provider.null v2.1.2

Terraform Configuration Files

locals {
  items = {
    foo = {
      some_key = "some_value",
      id = null_resource.main.id,
    }
  }
}

resource "null_resource" "main" {
}

resource "null_resource" "bar" {
  for_each = lookup(local.items, "bar", {})
}

Debug Output

Crash Output

Expected Behavior

I should see a plan similar to:

Terraform will perform the following actions:

  # null_resource.main will be created
  + resource "null_resource" "main" {
      + id = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Actual Behavior

I see a plan like:

Error: Invalid for_each argument

  on main.tf line 14, in resource "null_resource" "bar":
  14:   for_each = lookup(local.items, "bar", {})

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.

Steps to Reproduce

  1. terraform init
  2. terraform plan

Additional Context

If local.items doesn't contain any references to resources, the plan "works", ie I see the expected behavior if id = null_resource.main.id, is commented out.

References

@apparentlymart
Copy link
Member

Hi @kimor79! Sorry for this odd behavior, and thanks for reporting it.

It seems like the lookup function is being more conservative than it strictly needs to be:

if !mapVar.IsWhollyKnown() {
return cty.UnknownVal(retType), nil
}

Specifically, it's exiting early with an unknown value if there's an unknown value nested anywhere in the data structure of the first argument, rather than considering only the first nesting level.

Until that's improved, I think you could get this working by using the more general try function instead of the mapping-specific lookup function, like this:

  for_each = try(local.items.bar, {})

The documentation for try advises factoring out the normalization steps into a separate local value so that the rest of the configuration can just assume the structure is of the expected shape. I didn't try to do that here because the example you shared is clearly contrived anyway, but in your real configuration you might find it better to put the try in whatever expression is generating local.items, so that the for_each here can just refer to it and get the correct result without any further conditional operations:

  for_each = local.items.bar

@apparentlymart apparentlymart added bug config v0.12 Issues (primarily bugs) reported against v0.12 releases labels Jul 29, 2020
@kimor79
Copy link
Author

kimor79 commented Jul 29, 2020

Yes, this is a simplified example for the bug report and just to make sure it wasn't user error in the actual config :)

In my use case I'm for_each across AWS accounts and regions. Since I know those ahead of time I resorted to defining the complete map and just let the module(s) assume the key exists. I'll try try because it "feels" better to not have a bunch of empty map keys. Thanks!

@ashleydavies
Copy link

ashleydavies commented Jul 30, 2020

I had a very similar issue to this a while ago, IIRC because part of my data structure referred to an uninitialised data "aws_region" (which was to be initialised as part of the apply before it was required). Thanks for reporting!

My workaround (which looks like it may work for you to, but it's certainly very frustrating) was to do a targeted apply (with -target) on the resources things were depending on, but try sounds like a nicer solution.

@kimor79
Copy link
Author

kimor79 commented Jul 30, 2020

Ok, here's a simplified version of our actual config

locals {
  vpcs = {
    a = {
      name = "a",
      id   = module.account-a.vpc_id,
    },
  }

  peering = {
    b = {
      a = {
        vpc_id = local.vpcs["a"]["id"],
        name  = local.vpcs["a"]["name"],
      },
    },
  }
}

module "account-a" {
  source = "./account"

  peering = try(local.peering["a"], {})
  #peering = local.peering["a"]
}

module "account-b" {
  source = "./account"

  #peering = lookup(local.peering, "b", {})
  peering = try(local.peering["b"], {})
  #peering = local.peering["b"]
}
# ./account/main.tf

variable "peering" {}

module "vpc" {
  source = "../vpc"
}

module "peering" {
  for_each = var.peering

  source = "../peering"

  vpc_id = each.value["id"]
}

output "vpc_id" {
  value = module.vpc.id
}

output "peering" {
  value = { for k, v in var.peering : k => { id = module.peering[k].id } }
}
# ./vpc/main.tf

resource "null_resource" "main" {}

output "id" {
  value = null_resource.main.id
}
# ./peering/main.tf

variable "vpc_id" {}

resource "null_resource" "main" {
  triggers = {
    id = var.vpc_id
  }
}

output "id" {
  value = null_resource.main.id
}

this results in:

Error: Invalid for_each argument

  on account/main.tf line 8, in module "peering":
   8:   for_each = var.peering

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.

if I swap out the try for the hardcoded map, then it "works":

locals {
  peering = {
    a = {},
    b = {
      ...
    },
  }
}

module "account-N" {
  source = "./account"

  peering = local.peering["N"]
}

so it would seem that try has similar behavior to lookup

@danieldreier danieldreier added the new new issue not yet triaged label Aug 5, 2020
@jeremykatz
Copy link

I think I'm having the same issue, with terraform 0.13.0.

main.tf

locals {
  instances = toset(["1"])
}

provider null {}

resource null_resource r1 {
  for_each = local.instances
}

resource null_resource r2 {
  for_each = null_resource.r1
}

module m {
  source = "./mod"
  for_each = null_resource.r1
}

mod/module.tf

resource null_resource n { }
$ terraform apply

Error: Invalid for_each argument

  on main.tf line 17, in module "m":
  17:   for_each = null_resource.r1

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.

null_resource r2 has no problem with for_each's input coming from null_resource r1's map.
Using a module with for_each that depends on something raises the issue. The module can be empty and raise the same issue.

As @ashleydavies mentioned, using -target to create resource the module depends on first works.

@danieldreier danieldreier added explained a Terraform Core team member has described the root cause of this issue in code v0.13 Issues (primarily bugs) reported against v0.13 releases and removed new new issue not yet triaged labels Aug 13, 2020
@gogovan-vincentngai
Copy link

gogovan-vincentngai commented Aug 15, 2020

same here and use case is
i just want simply create memorystore at GCP and create the service_endpoint in k8s cluster based on each redis info

resource "google_redis_instance" "cache" {
  for_each            = var.redis["non_production"]
  name                = "test-${each.key}"
  tier                = "BASIC"
  authorized_network  = "sandbox-vpc"
  labels = {
    instance    = "${each.key}"
  }
}
# Plan remote module to create them 
module "create_mstore_endpoint_nonproduction" {
  for_each               = google_redis_instance.cache
  source                 = "../create_mstore_endpoint"
  memorystore_instance   = each.value.labels["instance"]         
  memorystore_ip         = each.value.host
  memorystore_port       = each.value.port
}

Error appear after plan

Error: Invalid for_each argument

  on main.tf line 16, in module "create_mstore_endpoint_nonproduction":
   for_each               = google_redis_instance.cache

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.

However if i first created redis and then re-execute the plan , it works...
but this is not we want...(should be same as using -target)

@gentksb
Copy link

gentksb commented Sep 30, 2020

@gogovan-vincentngai
I faced same issue.
Lookup function should work after other resource evaluation.

This is my case...

# moduleA
resource "foo" {
	...
	output = {
		name = this.name
		arn = this.arn
	}
}

# moduleB
resource "bar" {
 for_each = lookup(foo.output, "arn", false) == false ? [] : [""]
 ....
}

@kimor79
Copy link
Author

kimor79 commented Nov 12, 2020

my example, at least, seems to be working with tf-0.14 for both try and lookup:

$ terraform-0.14 version
Terraform v0.14.0-rc1
+ provider registry.terraform.io/hashicorp/null v3.0.0
$ terraform-0.14 plan

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # module.account-a.module.vpc.null_resource.main will be created
  + resource "null_resource" "main" {
      + id = (known after apply)
    }

  # module.account-b.module.peering["a"].null_resource.main will be created
  + resource "null_resource" "main" {
      + id       = (known after apply)
      + triggers = (known after apply)
    }

  # module.account-b.module.vpc.null_resource.main will be created
  + resource "null_resource" "main" {
      + id = (known after apply)
    }

Plan: 3 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

@pselle
Copy link
Contributor

pselle commented Jan 27, 2021

Thanks @kimor79! I've confirmed that this is fixed in 0.14 -- since we have no plans to do further work on older versions of Terraform, I'm closing this issue.

@pselle pselle closed this as completed Jan 27, 2021
@ghost
Copy link

ghost commented Feb 27, 2021

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues.

If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@hashicorp hashicorp locked as resolved and limited conversation to collaborators Feb 27, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
bug config explained a Terraform Core team member has described the root cause of this issue in code v0.12 Issues (primarily bugs) reported against v0.12 releases v0.13 Issues (primarily bugs) reported against v0.13 releases
Projects
None yet
Development

No branches or pull requests

8 participants