How to escape a for loop variable inside a module path?

I have a for expression that mostly does what I like:

output "string_ids" {
  value = { for client in ["client_a", "client_b"] : client => module.string_client_a[0].id }
}

As you can see, I’ve hard-coded “client_a” into the result of the for loop’s object (as well as the list). So of course, the result isn’t perfect:

string_ids = {
  client_a = "i-exampleEc2Id"
  client_b = "i-exampleEc2Id"
}

I have read in types and values that terraform supports bracket notation in place of dot notation in order to escape similar kind of variables, so I tried that.

value = { for client in ["string_client_a", "string_client_b"] : client => module["client"][0].id }

I moved my string to the list for testing (so that the module path should resolve). However, I get an error when I try this:

The "module" object does not support this operation.

I think there’s something different about the example use-case for this notation, but there’s something I am not understanding here. It would be helpful if the error could report the kind of operation that the compiler is interpreting.

In cases where a map might contain arbitrary user-specified keys, we recommend using only the square-bracket index notation (local.map["keyname"] ).

Anyone have an idea that can help me to escape the for loop variable/key inside a module path?

Or really, if there’s a better way to create outputs/data/etc. that represents my child module ID’s mapped to the client name, that’s ultimately what I’m trying to solve; in a dynamic/scalable map of course (1 output, or other resource, call creates map where values are child module output values). I’ve tried googling that but haven’t found any interesting examples.

Sorry, this isn’t possible. Terraform doesn’t support dynamic references to blocks, as a consequence of building a dependency graph between blocks early on in processing, before expressions are evaluated.

The only way you could make this work would be by all of your client module instances being just one module block with for_each describing each client. This would assume every client is very similar, to the point where it would be OK to model them this way.

Ah ok. Thank you for that great explanation.

That is a shame. I have a couple of child modules that get repeated a lot (building instances that use the same infrastructure), but there are just a couple parameters that I sometimes change as each instance may need a little bit more storage, for example. Maybe I can look into seeing if I can find a way to remap those parameters inside of the for_each call to the child module as exceptions to a default list. I’m imagining something using merge function and some variables.

Thanks again for your help! I think I have some good direction on my plan moving forward.

You can construct a mapping of your different module objects in a local value and then refer to that in your for expressions:

locals {
  clients = {
    a = module.client_a
    b = module.client_b
  }
}

This extra level of indirection can work because the local value itself becomes an extra node in the dependency graph to let Terraform still see what the correct order of evaluation will be: this local value depends on all of the output values of both modules, and anything that refers to this local values will depend on the local value and so will depend indirectly on all of the output values of both modules.

The key detail is that the expression I showed above still statically refers to the individual modules in a way that Terraform’s data flow analysis can see, but then it constructs a new mapping with keys of your choice that you can then access dynamically because they don’t affect the dependency graph:

local.clients[any_local_symbol]

Okay, I don’t mean to resurrect this, but I do want to put out the resolution to my problem. It took me quite a while, but I was able to convert all of my individual child resource calls into a single call utilizing for_each, then building a custom set of variable inputs using flatten.

After doing all of that work, 1 thing we stand to gain is the ability to output based on the aggregated local state. I’ll give an example.

locals {
  app_clients = [
    for client in keys(var.app_config) : client
    # Extracts the client project id from app_clients. 
  ]
}
output "iws_clients" {
  value       = local.app_clients
  description = "Prints a list of app clients by project name."
}

So, ultimately it’s not necessary to do everything that I did, but I did want to keep or create 1 source of truth for my client state, and this method forced an even tighter coupling between my app_config client configuration collection and my module call to create each subsequent client environment.

Hope that helps someone down the road!

edit > messed up my code block formatting.