Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Terraform 0.12 nested for loops

Tags:

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?

like image 392
ViggyNash Avatar asked May 08 '19 18:05

ViggyNash


People also ask

How do you use nested loops in Terraform?

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 .

How do you write a loop in Terraform?

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.

Can we use if else in Terraform?

As you (probably) know, Terraform doesn't support if statements.

What is foreach in Terraform?

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.


2 Answers

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}" 
like image 141
Martin Atkins Avatar answered Sep 30 '22 03:09

Martin Atkins


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

like image 23
exabugs Avatar answered Sep 30 '22 04:09

exabugs