Terraform Map and Object Patterns

Terraform variables implement both a map and an object type. They mostly work the same. The docs even say, “The distinctions are only useful when restricting input values for a module or resource.” They can be defined and accessed in several ways. There’s some automatic conversion back and forth between them.

This article distills these details into patterns you can copy and paste, while highlighting some of the subtleties.

Here’s the main detail you need:

Maps contain many things of one type. Objects contain a specific set of things of many types.

This is a simplification. It doesn’t cover all the behavior of terraform’s maps and objects (like loss that can happen in conversions back and forth between them), but it’s enough for the patterns you’re likely to need day to day.

Table of Contents

Style

Key Names

You can quote the key names in map definitions.

variable "quoted_map" {
  default = {
    "key_1" = "value_1"
    "key_2" = "value_2"
  }
}

But you don’t have to.

variable "unquoted_map" {
  default = {
    key_1 = "value_1"
    key_2 = "value_2"
  }
}

We prefer the unquoted format. Partly because the syntax is lighter and partly because it only works if key names are valid identifiers, so it forces us to use ones that are. If the key names are identifiers, the interior of maps look similar to the rest of our terraform variables, and we can also use a dotted notation for referencing them.

Commas

You can separate key/value pairs with commas.

variable "comma_map" {
  default = {
    key_1 = "value_1",
    key_2 = "value_2",
  }
}

But you don’t have to.

variable "no_comma_map" {
  default = {
    key_1 = "value_1"
    key_2 = "value_2"
  }
}

We prefer no commas because the syntax is lighter.

References

You can reference values by attribute name with quotes and square brackets.

output "brackets" {
  value = var.unquoted_map["key_2"]
}

But you can also use the dotted notation.

output "dots" {
  value = var.unquoted_map.key_2
}

We prefer the dotted notation because the syntax is lighter. This also requires the key names to be identifiers, but they will be if you use the unquoted pattern for defining them.

Patterns

  • Each pattern implements a map containing a value_2 string that we’ll read into an output.
  • Examples set values with variable default values, but they work the same with tfvars, etc.
  • The types of values in these examples are known, so they’re set explicitly. There’s also an any keyword for cases where you’re not sure. We recommend explicit types whenever possible.

Untyped Flat Map

This is the simplest pattern. We don’t recommend it. Use a typed map instead.

variable "untyped_flat_map" {
  default = {
    key_1 = "value_1"
    key_2 = "value_2"
  }
}
output "untyped_flat_map" {
  value = var.untyped_flat_map.key_2
}

Typed Flat Map

This is sufficient for simple cases.

variable "typed_flat_map" {
  default = {
    key_1 = "value_1"
    key_2 = "value_2"
  }
  type = map(string)
}
output "typed_flat_map" {
  value = var.typed_flat_map.key_2
}

With the type set, if a module mistakenly passes a value of the wrong type that our code wasn’t expecting, terraform throws an error.

variable "typed_flat_map_bad_value" {
  default = {
    key_1 = []
    key_2 = "value_2"
  }
  type = map(string)
}
│ Error: Invalid default value for variable
│ 
│   on main.tf line 49, in variable "typed_flat_map_bad_value":
│   49:   default = {
│   50:     key_1 = []
│   51:     key_2 = "value_2"
│   52:   }
│ 
│ This default value is not compatible with the variable's type constraint: element "key_1": string required.

except when it doesn’t. If we set key_1 to a number or boolean, it’ll be automatically converted to a string. This is generic terraform behavior. It’s not specific to maps.

Untyped Nested Map

We don’t recommend this, either. Use a typed nested map instead.

variable "untyped_nested_map" {
  default = {
    key_1 = "value_1"
    key_2 = {
      nested_key_1 = "value_2"
    }
  }
}
output "untyped_nested_map" {
  value = var.untyped_nested_map.key_2.nested_key_1
}

Typed Nested Map, Values are Same Type

Like the flat map, this pattern protects us against types of inputs the code isn’t written to handle. This only works when the values of the keys within each map all share the same type.

variable "typed_nested_map_values_same_type" {
  default = {
    key_1 = {
      nested_key_1 = "value_1"
    }
    key_2 = {
      nested_key_2 = "value_2"
    }
  }
  type = map(map(string))
}
output "typed_nested_map_values_same_type" {
  value = var.typed_nested_map_values_same_type.key_2.nested_key_2
}

Typed Nested Map, Values are Different Types

This is where the differences between maps and objects start to show up in implementations. Remembering our distillation of the docs from the start:

Maps contain many things of one type. Objects contain a specific set of things of many types.

variable "typed_nested_map_values_different_types" {
  default = {
    key_1 = "value_1"
    key_2 = {
      nested_key_1 = "value_2"
    }
  }
  type = object({
    key_1 = string,
    key_2 = map(string)
  })
}
output "typed_nested_map_values_different_types" {
  value = var.typed_nested_map_values_different_types.key_2.nested_key_1
}

In this nested map, one value is a string and the other is a map. That means we need an object to define the constraint. We can’t do it with just a map, because maps contain one type of value and we need two.

Flexible Number of Typed Nested Maps, Values are Different Types

This is the most complex case. It lets us read in a map that has an arbitrary number of nested maps like the ones above.

variable "flexible_number_of_typed_nested_maps" {
  default = {
    map_1 = {
      key_1 = "value_1"
      key_2 = {
        nested_key_1 = "value_2"
      }
    }
    map_2 = {
      key_1 = "value_3"
      key_2 = {
        nested_key_1 = "value_4"
      }
    }
  }
  type = map(
    object({
      key_1 = string,
      key_2 = map(string)
    })
  )
}
output "flexible_number_of_typed_nested_maps" {
  value = var.flexible_number_of_typed_nested_maps.map_1.key_2.nested_key_1
}

We could add a map_3 (or as many more as we wanted) without getting type errors. Again remembering our simplification:

Maps contain many things of one type. Objects contain a specific set of things of many types.

Inside, we use objects because their keys have values that are different types. Outside, we use a map because we want an arbitrary number of those objects.

The inside objects all have the same structure. They can be defined with the same type expression. That passes the requirement that maps contain all the same type of thing.

Happy automating!

Operating Ops

Need more than just this article? We’re available to consult.

You might also want to check out these related articles: