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?
Terraform does however support nested loops when creating local data structures, and it has a flatten function which can flatten the resulting list-of-lists. We can combine these two features to create a flat list of objects suitable for use with for_each .
Using the count meta-argument The count meta-argument is the simplest of the looping constructs within Terraform. By either directly assigning a whole number or using the length function on a list or map variable, Terraform creates this number of resources based on the resource block it is assigned to.
As you (probably) know, Terraform doesn't support if statements.
for_each is a meta-argument defined by the Terraform language. It can be used with modules and with every resource type. The for_each meta-argument accepts a map or a set of strings, and creates an instance for each item in that map or set.
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}"
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With