Pass For_each output to another Module

I’d like to know if what I am trying to do is poaaible.

I have 2 modules that I call. The first creates multiple EC2’s (using for_each) and outputs the EC2_id.

I then want to pass that output to another module and do a for_each to get the instance ID for volume attachment.

EC2 module called
For_each used
Output multiple EC2 id’s
EBS module called (ec2_id output input as a var)
loop through ec2_id for EBS attachment

The above is a simplified version. Is it possible to pass the output of a for_each to another module?

Hi @jprouten,

A resource with for_each set appears in expressions as a map of objects whose keys are the same as the keys in the for_each collection. Aside from being of an object type derived automatically from the resource type schema, these objects are just normal values that you can use in Terraform expressions in all of the same ways that you could use objects (and maps of objects) you created yourself.

However, since modules typically aim to encapsulate details of the resources they declare, it’s more common to derive a simpler map from the resource’s map which only includes the details that the external caller needs. In your case, it sounds like that’s just the EC2 instance ids.

To achieve that, your EC2 module might include an output value declared like this:

output "instance_ids" {
  value = tomap({
    for k, inst in aws_instance.example : k => inst.id
  })
}

The above uses a for expression to project the map of objects from aws_instance.example into a simpler map of strings, where the keys are the same as the instance keys but the values are just the instance ids, and not any of the other resource attributes.

You can then match this in your EBS module by declaring a variable of the same type to receive the value:

variable "ec2_instance_ids" {
  type = map(string)
}

resource "aws_volume_attachment" "example" {
  for_each = var.ec2_instance_ids

  # ...
}

This can work because var.ec2_instance_ids is a map and so it’s compatible with the requirements of for_each. The volume attachments will have the same instance keys as the EC2 instances they are attaching to, so you’ll be able to more easily see the correlations between them in the plan output.

Then in the root module you can connect those two together, something like this:

module "ec2" {
  source = "modules/ec2"

  # ...
}

module "ebs" {
  source = "modules/ebs"

  ec2_instance_ids = module.ec2.instance_ids
  # ...
}
1 Like

Perfect. That worked, thanks!

In the for_each I need to pass in both the instance ID and the Instance Availability zone.
I can see it’s possible to construct this in my outputs, but I can’t find a good example or explanation on how to do it.

How would I create an output that had both the Instance ID and the AvailabilityZone?

When you need to return more than one value for the same object then that’s a good use for object types. The principle will be similar but we’ll change the output value and the input variable to use a map of an object type rather than a map of strings:

output "instances" {
  value = tomap({
    for k, inst in aws_instance.example : k => {
      id                = inst.id
      availability_zone = inst.availability_zone
      private_ip        = inst.private_ip
    }
  })
}
variable "ec2_instance_ids" {
  type = map(object({
    id                = string
    availability_zone = string
  }))
}

resource "aws_volume_attachment" "example" {
  for_each = var.ec2_instance_ids

  instance_id = each.value.id
  # ...
}

I also included private_ip on the output value in order to illustrate something about object type constraints: the variable’s type constraint only includes id and availability_zone, but that’s okay because Terraform will accept any object type that has at least the specified attributes, and so it’ll just ignore that extra private_ip attribute during type conversion.

That’s not very important for this limited case, but it can be useful if you have a number of other modules that need information about instances but that each need different information: you can make the module return all of the information all of the modules need but have each module only specify on the specific attributes it needs.

Worked a treat. Thanks!

looks pretty straightforward but where and how are we assigning value to var.ec2_instance_ids from output "instances"...?

I have a resource that I create using for_each, I need an attribute from the objects created with for_each in another module in the same file. I am trying this:

output "my_resource_id"{
            value = values(module.my_module)[*].id
}

but where I’m using it like:

arr = [for a in module.my_module[*].my_resource_id: a]

it is not working :frowning:

EDIT: Here is the example of what I’m trying to work out.

module "azuread_application_terraform" {
  source = "git::ssh://git@bitbucket.org/example//azuread/azuread_application"

  for_each = local.principals_map

  name = each.key
}

#local.principals_map is a map with names of the applications to register as a key

Next I create few key vaults:

  source = "git::ssh://git@bitbucket.org/example//azurerm/azurerm_key_vault"

  for_each = toset(var.teams)

    name                       = each.key
    location                   = var.location
    resource_group_name        = var.resource_group_name
    tenant_id                  = var.cloud_tenant_id
    soft_delete_retention_days = var.kv_soft_delete_retention_days
    purge_protection_enabled   = var.kv_purge_protection_enabled
    sku_name                   = var.kv_sku_name
  
    tags = local.tags
}

#teams is a map with name of key vault and 

This is where the fun starts. Now I need to grant access to the application registration created above to these key vaults. I am trying to get application’s object_id and respective key vault as follows:

  app_ids = { for m in local.all_principals : module.azuread_application_terraform[m.name].object_id => {
    "app_name" : m.name
    "kv_id" : module.azurerm_key_vault_primary[m.team].id,
    "application_secret" : module.azuread_application_terraform[m.name].application_secret
    }
  }
}

The access code is below:

module "azurerm_key_vault_access_policy" {
  source = "git::ssh://git@bitbucket.org/example//azurerm/azurerm_key_vault_access_policy"

  for_each = local.app_ids

  key_vault_id = each.value.kv_id
  tenant_id    = var.cloud_tenant_id
  object_id    = each.key

  secret_permissions      = ["Get", "List", "Set", "Delete", "Purge"]
  key_permissions         = ["Get", "List"]
  certificate_permissions = ["Get", "List"]
}

On running terraform plan it complains that app_ids is invalid for the for_each:

Error: Invalid for_each argument
│
│   on 002-file.tf line 81, in module "azurerm_key_vault_access_policy":
│   81:   for_each = local.app_ids
│     ├────────────────
│     │ local.app_ids will be known only after apply
│
│ 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.

Any work around this?

Could you share some dummy code which also show the output definition within the module itself?