Terraform 0.13.0 - for_each loop in module

Hi,

I want to create Azure groups and roles (users already exist) using terrform. I have working code, but wanted to make use of the for_each for modules in terraform 0.13.0.

My original root module calls a child module multiple times (ie the root module has multiple module blocks calling the same module), and feeds in different user/group/role variables for each module block. My .tfvars file has a long list of users/groups/roles which is required.

Original modules are like this, and I have a module block for group1, one for group2 and so on. Thet three variables grab from .tfvars and pass that var into the module.

module "group1" {
  source = "../modules"
  group = var.group1-group 
  users = var.group1-users
  roles = var.group1-roles
}

Using for_each in a module block, I wanted to have just one module block. So far I have this:

module "groups" {
  source = "../modules"
  for_each = var.groups 
  /*above is a new "set" variable I defined in tfvars that holds "group1" "group2" and so on*/
  group = format("%s%s%s","var.",each.key,"-group") 
  users = format("%s%s%s","var.",each.key,"-users")
  roles = format("%s%s%s","var.",each.key,"-roles")
}

The problem is the “format” command creates strings like:
“var.group1-group”
“var.group1-users”
“var.group1-roles”

and the actual string is passed to the module rather than the variable contained var.group1-groups. Basically I want to get:
var.group1-group and not “var.group1-group”
and so on for all the variables.

Any ideas - or do I need to rethink my entire design to make use of for_each.

Regards,
Scott

If I understand the goal, format("%s%s%s",“var.”,each.key,"-group") is attempting to employ indirection. Eg

variable "a" { default = "b" }
variable "b" { default = "c" }
output "out" { value = "${var.${var.a}}" } # Hoping for "c"

This isn’t a for_each issue. The same thing would happen with a single direct instantiation of the groups module using format. I don’t know if terraform is directly capable of doing this.

The way I use for_each in similar situations is to pass a map of objects that define each of the variables for a single instantiation of the module.

module "mod" {
  source = "./somewhere"
  for_each = {"a" = {"first" = "1",   "second" = "2"}, 
              "b" = {"first" = "one", "second" = "two"}
  v1 = each.value["first"]    # "1" for instance "a", "one" for instance "b"
  v2 = each.value["second"]   # "2" for instance "a", "two" for instance "b"
 }
}
1 Like

Hi, Thanks for the reply. I don’t think I described what I am trying to do in a concise way, so will try again (or I did and I just didn’t understand your reply correctly).

Code is here:

I have a .tfvars file like this which is used by the root module:

Cog-Operator-group = “Cog-Operator”
Cog-Operator-users = [
“user1@email.com”,
“user2@email.com”,
]
Cog-Operator-roles = [
“Reader”,
]

Cog-Admin-group = “Cog-Admin”
Cog-Admin-users = [
“user3@email.com”,
“user4@email.com”,
]
Cog-Admin-roles = [
“Virtual Machine Administrator Login”,
“Security Admin”,
]

I currently have a root module that has this:

module “Cog-Operator” {
source = “…/modules”
subscriptions = var.subscriptions
group = var.Cog-Operator-group
users = var.Cog-Operator-users
roles = var.Cog-Operator-roles
}

module “Cog-Admin” {
source = “…/modules”
subscriptions = var.subscriptions
group = var.Cog-Admin-group
users = var.Cog-Admin-users
roles = var.Cog-Admin-roles
}

So I just pass the various .tfvars “group/user/roles” variables into a child module.
What I want to do is alter the root module so rather than have 2xblocks to call the child module twice, it just 1xblock that “loops” and calls the child module twice, and passes the Cog-Operator vars the first time and the Cog-Admin vars the second time.

I have a feeling this should be really easy, but cannot get my head around what to do.

Thanks again for you earlier reply.

Regards,
Scott

Hi Scott! This is a great use case for module for_each. To use it, you’ll need to construct a map of the variables you want to pass into each module instance.

Starting with your original example:

module "Cog-Operator" {
  source = "../modules"
  subscriptions = var.subscriptions
  group = var.Cog-Operator-group
  users = var.Cog-Operator-users
  roles = var.Cog-Operator-roles
}

module "Cog-Admin" {
  source = "../modules"
  subscriptions = var.subscriptions
  group = var.Cog-Admin-group
  users = var.Cog-Admin-users
  roles = var.Cog-Admin-roles
}

This can be replaced with:

module "Cogs" {
  for_each = {
    "Operators" = {
      "group" = var.Cog-Operators-group,
      "users" = var.Cog-Operators-users,
      "roles" = var.Cog-Operators-roles,
    },
    "Admin" = {
      "group" = var.Cog-Admin-group,
      "users" = var.Cog-Admin-users,
      "roles" = var.Cog-Admin-roles,
    }
  }
  source = "../modules"
  subscriptions = var.subscriptions # the same for all instances
  group = each.value["group"]
  users = each.value["users"]
  roles = each.value["roles"]
}

I hope this is clear! I’ve created an example repository which is somewhat similar to yours, just in case that helps to clarify.

Take a look at this commit migrating my example to for_each. In this case I move the map structure into the terraform.tfvars file, which keeps the main code easier to read. Note also that I’m specifying the type of this variable in variables.tf to help make sure I don’t typo one of the attribute names.

Does this make sense to you?

Hi,

I think that is clear to me. I will try it out over the next few days (got to do some other work first…).

Thanks very much.

Scott