ViggyNash
ViggyNash

Reputation: 611

Terraform 0.12 nested for loops

I am trying to implement nested for loops using Terraform 0.12's new features in order to loop through AWS IAM users, each of which can have one or more policies attached. The variable used to represent this list is of type map(list(string)) and looks something like this:

{
  "user 1" = [ "policy1", "policy2" ],
  "user 2" = [ "policy1" ]
}

Getting the list of users to create is easy enough via keys(), but since there is currently no mechanism for nesting looped resource creation in Terraform, the policy attachments have to happen as a singular loop independent of each user. So, I am attempting to construct a list of user:policy associations from the map input that would look something like this based on the example above:

[
  [ "user1", "policy1" ],
  [ "user1", "policy2" ],
  [ "user2", "policy1" ]
]

I am attempting construct that list and store it in a local variable like so, where var.iam-user-policy-map is the input map:

locals {
  ...
  association-list = [
    for user in keys(var.iam-user-policy-map):
    [
      for policy in var.iam-user-policy-map[user]:
      [user, policy]
    ]
  ]
  ...
}

However, I am getting errors when attempting to access the values in that nested list. I am trying to access the user portion of the association with the reference local.association-list[count.index][0] and the policy with local.association-list[count.index][1], but on running terraform plan it errors out:

Error: Incorrect attribute value type

  on main.tf line 27, in resource "aws_iam_user_policy_attachment" "test-attach":
  27:   user = local.association-list[count.index][0]

Inappropriate value for attribute "user": string required.


Error: Incorrect attribute value type

  on main.tf line 27, in resource "aws_iam_user_policy_attachment" "test-attach":
  27:   user = local.association-list[count.index][0]

Inappropriate value for attribute "user": string required.


Error: Invalid index

  on main.tf line 28, in resource "aws_iam_user_policy_attachment" "test-attach":
  28:   policy_arn = "arn:aws-us-gov:iam::aws:policy/${local.association-list[count.index][1]}"
    |----------------
    | count.index is 0
    | local.association-list is tuple with 2 elements

The given key does not identify an element in this collection value.


Error: Invalid template interpolation value

  on main.tf line 28, in resource "aws_iam_user_policy_attachment" "test-attach":
  28:   policy_arn = "arn:aws-us-gov:iam::aws:policy/${local.association-list[count.index][1]}"
    |----------------
    | count.index is 1
    | local.association-list is tuple with 2 elements

Cannot include the given value in a string template: string required.

What am I doing wrong?

Upvotes: 34

Views: 72911

Answers (3)

vladimirror
vladimirror

Reputation: 1068

I am not sure where I got this answer from, but this one worked for me.

locals {
  schemas    = [
                 "PRIVATE",
                 "PUBLIC",
                 "MY_SCHEMA",
               ]
  privileges = [
                 "CREATE TABLE",
                 "CREATE VIEW",
                 "USAGE",
               ]
  # Nested loop over both lists, and flatten the result.
  schema_privileges = distinct(flatten([
    for schema in local.schemas : [
      for privilege in local.privileges : {
        privilege = privilege
        schema    = schema
      }
    ]
  ]))
}

 resource "snowflake_schema_grant" "write_permissions" {
  # We need a map to use for_each, so we convert our list into a map by adding a unique key:
  for_each      = { for entry in local.schema_privileges: "${entry.schema}.${entry.privilege}" => entry }
  database_name = "MY_DATABASE"
  privilege     = each.value.privilege
  roles         = "DAVE"
  schema_name   = each.value.schema
}

Upvotes: 2

exabugs
exabugs

Reputation: 336

If you need a map for 'for_each', 'merge' is convenient.

variable "iam-user-policy-map" {
  default = {
    "user 1" = ["policy1", "policy2"],
    "user 2" = ["policy1"]
  }
}

locals {
  association-map = merge([
    for user, policies in var.iam-user-policy-map : {
      for policy in policies :
        "${user}-${policy}" => {
          "user"   = user
          "policy" = policy
        }
    }
  ]...)
}

output "association-map" {
  value = local.association-map
}

Outputs:

association-map = {
  "user 1-policy1" = {
    "policy" = "policy1"
    "user" = "user 1"
  }
  "user 1-policy2" = {
    "policy" = "policy2"
    "user" = "user 1"
  }
  "user 2-policy1" = {
    "policy" = "policy1"
    "user" = "user 2"
  }
}

Example for_each usage:

resource "null_resource" "echo" {
  for_each = local.association-map
  provisioner "local-exec" {
    command = "echo 'policy - ${each.value.policy}, user - ${each.value.user}'"
  }
}

https://github.com/hashicorp/terraform/issues/22263#issuecomment-969549347

Upvotes: 16

Martin Atkins
Martin Atkins

Reputation: 74219

The for expression in your local value association-list is producing a list of list of lists of strings, but your references to it are treating it as a list of lists of strings.

To get the flattened representation you wanted, you can use the flatten function, but because it would otherwise group everything into a single flat list I'd recommend making the innermost value an object instead. (That will also make the references to it clearer.)

locals {
  association-list = flatten([
    for user in keys(var.iam-user-policy-map) : [
      for policy in var.iam-user-policy-map[user] : {
        user   = user
        policy = policy
      }
    ]
  ])
}

The result of this expression will have the following shape:

[
  { user = "user1", policy = "policy1" },
  { user = "user1", policy = "policy2" },
  { user = "user2", policy = "policy2" },
]

Your references to it can then be in the following form:

user = local.association-list[count.index].user
policy_arn = "arn:aws-us-gov:iam::aws:policy/${local.association-list[count.index].policy}"

Upvotes: 59

Related Questions