Nested Loops in Terraform: Create a map from 2 lists

By , March 7, 2020 7:42 pm

Recently I encountered a Terraform task in which I had a list of roles and a list of policies and I needed to create a AWS resource for every combination of role-policy. In a “regular” programming language this would be a simple nested loop. Thankfully Terraform 0.12 added for_each and for attributes to declare recurring resources. But two problems remained:

  1. I needed some kind of way to nest these for declarations
  2. for_each attributes requires a map with a unique key

So let’s tackle these problems one at a time. Let’s we have 2 lists:

locals {
   ROLES = ["developer", "analyst", "manager"]
   POLICIES = ["arn:1", "arn:2", "arn:3"]
}

We need to iterate thru them in a nested way and produce objects with unique keys. It can go something like this:

locals {

  ROLES = ["developer", "analyst", "manager"]
  POLICIES = ["arn:1", "arn:2", "arn:3"]

  list = [for role_item in local.ROLES :
    [for policy_item in local.POLICIES : {
      "${role_item}-${policy_item}" = {
        role  = role_item
        policy = policy_item
      }
      }
    ]
  ]

}

output result {
  value = local.list
}

We iterate thru one list inside of the other producing list of objects where the key is unique combination of original values. This produces the result:

result = [
  [
    {
      "developer-arn:1" = {
        "policy" = "arn:1"
        "role" = "developer"
      }
    },
    {
      "developer-arn:2" = {
        "policy" = "arn:2"
        "role" = "developer"
      }
    },
    {
      "developer-arn:3" = {
        "policy" = "arn:3"
        "role" = "developer"
      }
    },
  ],
  [
    {
      "analyst-arn:1" = {
        "policy" = "arn:1"
        "role" = "analyst"
      }
    },
    {
      "analyst-arn:2" = {
        "policy" = "arn:2"
        "role" = "analyst"
      }
    },
    {
      "analyst-arn:3" = {
        "policy" = "arn:3"
        "role" = "analyst"
      }
    },
  ],
  [
    {
      "manager-arn:1" = {
        "policy" = "arn:1"
        "role" = "manager"
      }
    },
    {
      "manager-arn:2" = {
        "policy" = "arn:2"
        "role" = "manager"
      }
    },
    {
      "manager-arn:3" = {
        "policy" = "arn:3"
        "role" = "manager"
      }
    },
  ],
]

Wait, technically we produced a list of lists. It can’t be very useful in its current form. Let’s flatten it:

locals {

  ROLES = ["developer", "analyst", "manager"]
  POLICIES = ["arn:1", "arn:2", "arn:3"]

  list = flatten([for role_item in local.ROLES :
    [for policy_item in local.POLICIES : {
      "${role_item}-${policy_item}" = {
        role  = role_item
        policy = policy_item
      }
      }
    ]
  ])

}

output result {
  value = local.list
}

Ok this is better:

result = [
  {
    "developer-arn:1" = {
      "policy" = "arn:1"
      "role" = "developer"
    }
  },
  {
    "developer-arn:2" = {
      "policy" = "arn:2"
      "role" = "developer"
    }
  },
  {
    "developer-arn:3" = {
      "policy" = "arn:3"
      "role" = "developer"
    }
  },
  {
    "analyst-arn:1" = {
      "policy" = "arn:1"
      "role" = "analyst"
    }
  },
  {
    "analyst-arn:2" = {
      "policy" = "arn:2"
      "role" = "analyst"
    }
  },
  {
    "analyst-arn:3" = {
      "policy" = "arn:3"
      "role" = "analyst"
    }
  },
  {
    "manager-arn:1" = {
      "policy" = "arn:1"
      "role" = "manager"
    }
  },
  {
    "manager-arn:2" = {
      "policy" = "arn:2"
      "role" = "manager"
    }
  },
  {
    "manager-arn:3" = {
      "policy" = "arn:3"
      "role" = "manager"
    }
  },
]

Ok this is better, we got a flat list of objects, but we still need to convert it into a map. If you examine each element of the list you will see that it’s a map consisting of one key and one value. We can extract them using keys() and values() functions. These functions produce lists, but since we deal only with one key and one value per element we can just grab a result at index 0:

locals {

  ROLES = ["developer", "analyst", "manager"]
  POLICIES = ["arn:1", "arn:2", "arn:3"]

  list = flatten([for role_item in local.ROLES :
    [for policy_item in local.POLICIES : {
      "${role_item}-${policy_item}" = {
        role  = role_item
        policy = policy_item
      }
      }
    ]
  ])

  map = { for item in local.list :
    keys(item)[0] => values(item)[0]
  }

}

output result {
  value = local.map
}

And the result is:

result = {
  "developer-arn:1" = {
    "policy" = "arn:1"
    "role" = "developer"
  }
  "developer-arn:2" = {
    "policy" = "arn:2"
    "role" = "developer"
  }
  "developer-arn:3" = {
    "policy" = "arn:3"
    "role" = "developer"
  }
  "analyst-arn:1" = {
    "policy" = "arn:1"
    "role" = "analyst"
  }
  "analyst-arn:2" = {
    "policy" = "arn:2"
    "role" = "analyst"
  }
  "analyst-arn:3" = {
    "policy" = "arn:3"
    "role" = "analyst"
  }
  "manager-arn:1" = {
    "policy" = "arn:1"
    "role" = "manager"
  }
  "manager-arn:2" = {
    "policy" = "arn:2"
    "role" = "manager"
  }
  "manager-arn:3" = {
    "policy" = "arn:3"
    "role" = "manager"
  }
}

Bingo! Now this map can be used as a value for for_each in a terraform resource. E.g.

resource "aws_iam_role_policy_attachment" "role_policy" {
  for_each   = local.map
  role       = each.value.role
  policy_arn = each.value.policy
}

would produce 9 role-policy attachment resources.

UPDATE: Terraform offers setproduct() function that creates all possible combinations of given sets. Using it makes creating needed map even simpler:

map = {for item in setproduct(local.ROLES, local.POLICIES):
    "${item[0]}-${item[1]}" => {
      role = item[0]
      policy = item[1]
    }
}

will produce result identical to above in one go without needing nested loops.

Leave a Reply

Panorama Theme by Themocracy

Bitnami