Terraform Opinion #23: Use list of objects over map of maps

Lately, I’ve seen quite a bit of Terraform snippets that use a static map of maps to directly configure resources.

For example:

locals {
  map_of_maps = {
    name1 = {
      attribute1 = "name1-value1"
      attribute2 = "name1-value2"
      attribute3 = "name1-value3"
    }
    name2 = {
      attribute1 = "name2-value1"
      attribute2 = "name2-value2"
      attribute3 = "name2-value3"
    }
    name3 = {
      attribute1 = "name3-value1"
      attribute2 = "name3-value2"
      attribute3 = "name3-value3"
    }
  }
}

resource "some_resource" "this" {
  for_each = local.map_of_maps

  attribute1 = each.value.attribute1
  attribute2 = each.value.attribute2
  attribute3 = each.value.attribute3
  }

This is seemingly straightforward.

But, say I want to transform the map of maps into seperate collection types like another map, list, object, etc.

locals {
  #  map_of_attribute1_value_to_name = {
  #    "name1-value1" = "name1"
  #    "name2-value1" = "name2"
  #    "name3-value1" = "name3"
  #  }
  map_of_attribute1_value_to_name = merge(
    [for k, v in local.map_of_maps :
      { for i in [values(v)[0]] : i => k }
    ]...
  )

  #  map_of_attribute1_value_to_object = {
  #    "name1-value1" = {
  #      "attribute1" = "name1-value1"
  #      "attribute2" = "name1-value2"
  #      "attribute3" = "name1-value3"
  #      "name" = "name1"
  #    }
  #    "name2-value1" = {
  #      "attribute1" = "name2-value1"
  #      "attribute2" = "name2-value2"
  #      "attribute3" = "name2-value3"
  #      "name" = "name2"
  #    }
  #    "name3-value1" = {
  #       "attribute1" = "name3-value1"
  #       "attribute2" = "name3-value2"
  #       "attribute3" = "name3-value3"
  #       "name" = "name3"
  #    }
  #  }
  map_of_attribute1_value_to_object = {
    for k, v in local.map_of_maps :
    v.attribute1 => {
      name       = k
      attribute1 = v.attribute1
      attribute2 = v.attribute2
      attribute3 = v.attribute3
    }
  }

  #  set_of_attribute1_values = toset([
  #    "name1-value1",
  #    "name2-value1",
  #    "name3-value1",
  #  ])
  set_of_attribute1_values = toset(
    [for k, v in local.map_of_maps : v.attribute1]
  )

  #  set_of_names = toset([
  #    "name1",
  #    "name2",
  #    "name3",
  #  ])
  set_of_names = toset(keys(local.map_of_maps))
}

Notice each transform is very different from the other.

It actually takes some time to infer the results without further comments.

Rigid imperative transforms on a map of maps like local.map_of_attribute1_value_to_name and local.map_of_attribute1_value_to_object are likely to increase in complexity making maintenance painful over time.

I highly recommend starting with a list of objects over a map of maps.

An object is a nice flat structure to build an abstraction.

A list of objects provides declarative transforms to organize data into the structure we want for building other resources and modules.

Consider these transforms resulting in the same collection types from the map of maps example.

locals {
  list_of_objects = [
    {
      name       = "name1"
      attribute1 = "name1-value1"
      attribute2 = "name1-value2"
      attribute3 = "name1-value3"
    },
    {
      name       = "name2"
      attribute1 = "name2-value1"
      attribute2 = "name2-value2"
      attribute3 = "name2-value3"
    },
    {
      name       = "name3"
      attribute1 = "name3-value1"
      attribute2 = "name3-value2"
      attribute3 = "name3-value3"
    }
  ]
}

locals {
  map_of_attribute1_value_to_name   = zipmap(local.list_of_objects[*].attribute1, local.list_of_objects[*].name)
  map_of_attribute1_value_to_object = { for o in local.list_of_objects : o.attribute1 => o }
  set_of_attribute1_values          = toset(local.list_of_objects[*].attribute1)
  set_of_names                      = toset(local.list_of_objects[*].name)
}

To take a closer look at the differences go here.

For me, these transforms are very clear and consistent.

We can quickly deduce the resulting collection type without much effort.

Getting to a map of maps is just as easy.

locals {
  map_of_maps = {
    for o in local.list_of_objects : o.name => {
      attribute1 = o.attribute1
      attribute2 = o.attribute2
      attribute3 = o.attribute3
    }
  }
}

Using a list of objects allows for expressive flexibilty when building various collection types from a single source.

I get lots of mileage out of this pattern for general creation of resources and modules.

Now, I can think deeply about simple abstractions and think less about complex transforms.

~jq1

links:

Feedback

What did you think about this post? jude@jq1.io