Terraform complex variable definition

Viewed 1781

The Context: let me start saying I know "flat is better than nested" but in this case using flat variable structure lacks of semanatics so I created a nested structure as follows:

users-config = [
{
    team = "team1"
    custom-policies = [
        {
            policy-name = "p1"
            policy-json = "<some long text in json>"
        },
        {
            policy-name = "p2"
            policy-json = "<some long text in json>"
        }
    ]
    groups = [
        {
            group-name = "g1"
            policies-attached = ["p1", "p2"]
        }
    ]
    users = [
        {
            name = "u1"
            user-name = "user.name"
            email = "user-name@example.com"
            groups = ["g1"]
        }
    ]
}
]

This shows the policies, groups and users created for a specific team.

The Challenge: Creating this structure in terraform variable definition is quite simple, basically (many fields ommited):

variable "users-configf" {
type = list(object(
    {
        team = string
        custom-policies = list(object({...}))
        groups = list(object({...}))
        users = list(object({...}))
    }
))
}

But I have been using different combination of for_each / for expressions and I have not achieved traverse over this structure to create the resources. Basically I will pick some pieces from the structure to create the resources (at least that's my intention obviously code is not working):

resource "aws_iam_group" "test-groups" {

  for_each = [for uc in var.users-config: for g in uc.groups: g.group-name]
  name = each.value
}

The Expectation: I would traverse the structure and create the resources as needed (groups, policies, users, user-group membership and policies-groups attachments)

As I mentioned at the begining of the question, I can achieve this by creating flat variable definitions for users, groups, policies and creating the bindings between them but I'm looking for a descriptive variable. Any suggestion is welcome, thanks in advance...

1 Answers

Regardless of the shape of your input, the key requirement for for_each is that the map you assign to it must have one element per instance you want to create, and so you can either pre-flatten the data structure at the source, or you can write some Terraform expressions to derive several differently-flattened data structures from your original nested data structure.

The common way to flatten data structures in Terraform is to use the flatten function, whose documentation includes the section Flattening nested structures for for_each.

For example, we can use flatten to build a flat list of all of the groups across all of the users-config elements like this:

locals {
  all_groups = flatten([
    for idx, config in var.users_config : [
      for g in config.groups : merge(
        g,
        { users_config_index = idx },
      )
    ]
  ])
}

The result of the above would be a list like the following, given the input you showed in your question:

[
  {
    group_name         = "g1"
    policies_attached  = ["p1", "p2"]
    users_config_index = 0
  },
]

To make this compatible with the expectations of for_each it will need one final transform into a map which has a unique key for each of the elements. If you know that your group names will be unique across all of the user config elements then you could just use the group_name values directly, but if not then you'll need to combine the users_config_index with the group_name to make them all unique:

resource "aws_iam_group" "example" {
  for_each = {
    for grp in local.all_groups : "${grp.users_config_index}:${grp.group_name}" => grp
  }
 
  name = each.value.group_name
  path = "/groups/${each.value.users_config_index}"
}

Note that by including a list index in the key you make the instances subject to changes if the ordering of the items in the list is changed later, so I would suggest avoiding including the indices in the keys if possible but I've shown it here only to make a more complete example that covers all of the decisions that go into for_each usage.

Related