Terraform Lists and Maps: Practical Guide with Examples

Terraform lists and Terraform maps are two of the most important collection types in Terraform. Lists are best when order matters. Maps are best when you need stable keys, readable configuration, and safer resource creation with for_each. This guide explains both with practical examples.

Terraform lists Terraform maps for_each Preserving order Checked: 26 June 2026 HashiCorp docs based

Terraform Lists and Maps: Practical Guide with Examples

Terraform lists and Terraform maps are core collection types used to organise variables, create repeatable resources, and build reusable modules. Lists are best when order matters. Maps are best when stable keys and readable configuration matter.

Focus areas: Terraform lists, Terraform maps, list vs map, for_each, for expressions, zipmap, list of objects, map of objects, nested maps, preserving order, and real-world infrastructure patterns.

Quick answer

Terraform lists vs maps in one sentence

Use a Terraform list when you need an ordered sequence of values. Use a Terraform map when you need named values, stable resource identity, or environment-specific configuration.

Terraform list Ordered collection, accessed by index
Terraform map Key-value collection, accessed by key
Best production pattern map(object({...})) with for_each
Simple rule: lists are for sequence. Maps are for identity. If you are creating long-lived infrastructure resources, identity usually matters more than order.

Table of contents

  1. Why Terraform lists and maps matter
  2. Terraform collection types
  3. What are Terraform lists?
  4. When to use Terraform lists
  5. What are Terraform maps?
  6. When to use Terraform maps
  7. Terraform list vs map comparison
  8. Using Terraform maps with for_each
  9. Terraform list of objects
  10. Terraform map of objects
  11. Terraform nested maps
  12. Convert a Terraform list to a map
  13. Preserving order without relying on list indexes
  14. Useful Terraform functions
  15. Best practices
  16. FAQs
  17. Useful links

Why Terraform lists and maps matter

Terraform is declarative, but real infrastructure still needs structure. You may need multiple subnets, multiple environments, alert rules, diagnostic settings, private endpoints, firewall rules, or module inputs. Choosing the right Terraform data type keeps that code clean and predictable.

This refreshed version focuses on how Terraform lists and Terraform maps are used in practical infrastructure code, especially with for_each, for expressions, maps of objects, reusable modules, and preserving order safely.

Common mistake: using a list with count for resources that should have stable names. If the list order changes, Terraform can interpret that as a change to resource indexes. For stable resources, a map with for_each is usually safer.
Terraform language basics

Terraform collection types

Terraform supports primitive types such as string, number, and bool. It also supports complex types such as list, set, map, object, and tuple.

TypeMeaningExample type constraintTypical use
list Ordered collection of values of the same type. list(string) Regions, CIDR blocks, names, ordered inputs.
map Key-value collection where values have the same type. map(string) Environment settings, tags, lookup values.
object Named attributes where each attribute can have its own type. object({ name = string, size = string }) Structured configuration for a resource or module.
set Unordered collection of unique values. set(string) Unique strings for for_each where order does not matter.
Practical module design: for real-world Terraform modules, the most useful structures are often list(string), map(string), list(object({...})), and map(object({...})).
Terraform lists

What are Terraform lists?

A Terraform list is an ordered collection of values. Each value in a list has a position, and you can access values by index.

Basic Terraform list example

variable "regions" {


description = "Azure regions used by this deployment."
type        = list(string)
default     = ["australiaeast", "australiasoutheast"]
}
locals {
primary_region   = var.regions[0]
secondary_region = var.regions[1]
}

Terraform list indexes start at 0. In the example above, var.regions[0] returns australiaeast.

Be careful: lists preserve order. That is useful when order matters, but risky if you are using list indexes to identify long-lived infrastructure resources.

When to use Terraform lists

Use Terraform lists when you need a simple ordered sequence of values.

  • Ordered region lists.
  • Ordered availability zones.
  • Subnet address ranges.
  • Allowed IP address ranges.
  • Simple names or labels.
  • Ordered output values.
  • Input values where position genuinely matters.
  • Short simple collections that are unlikely to be reordered.

Example: allowed IP ranges

variable "allowed_ip_ranges" {


description = "CIDR ranges allowed to access the application."
type        = list(string)
default = [
"203.0.113.10/32",
"198.51.100.0/24"
]
}

Example: list with count

resource "azurerm_resource_group" "example" {


count    = length(var.regions)
name     = "rg-demo-${count.index + 1}"
location = var.regions[count.index]
}
Good use case: lists work well for simple repeated values. For named resources, consider converting the list to a map or accepting a map as the input from the beginning.
Terraform maps

What are Terraform maps?

A Terraform map is a key-value collection. Instead of accessing values by numeric position, you access them by key. This makes maps more readable and safer for many infrastructure scenarios.

Basic Terraform map example

variable "vm_sizes" {


description = "Virtual machine size by environment."
type        = map(string)
default = {
dev  = "Standard_B2s"
test = "Standard_D2s_v5"
prod = "Standard_D4s_v5"
}
}
locals {
production_vm_size = var.vm_sizes["prod"]
}

Using lookup with a fallback value

locals {


selected_vm_size = lookup(var.vm_sizes, var.environment, "Standard_B2s")
}

The lookup function is useful when a key might not exist and you want to provide a default value.

When to use Terraform maps

Use Terraform maps when you want stable names, descriptive keys, or environment-specific configuration.

  • Environment settings such as dev, test, and prod.
  • Resource definitions keyed by resource name.
  • Tags and metadata.
  • Region-specific values.
  • Diagnostic settings.
  • Metric alert definitions.
  • Policy assignments.
  • Module input objects.

Example: map of Azure regions

variable "region_map" {


description = "Region name by deployment role."
type        = map(string)
default = {
primary   = "australiaeast"
secondary = "australiasoutheast"
}
}
locals {
primary_location = var.region_map["primary"]
}
Why maps are usually cleaner: keys like primary, secondary, dev, and prod explain the purpose of each value directly in the code.
Comparison

Terraform list vs map comparison

QuestionTerraform listTerraform mapPractical recommendation
How is data accessed? By index, for example var.items[0]. By key, for example var.items["prod"]. Use maps when readability matters.
Does order matter? Yes. Lists preserve order. Maps are key-based. Do not rely on visual order in the file. Use lists only when sequence is meaningful.
Best use case Simple ordered values. Named settings and resources. Use maps for environment and resource definitions.
Works with for_each? A plain list is not the normal input for for_each. Yes. Maps work naturally with for_each. Use map(object({...})) for repeatable resources.
Risk Reordering can affect index-based resources. Keys provide stable identity. Prefer maps for long-lived infrastructure.
for_each pattern

Using Terraform maps with for_each

The for_each meta-argument is one of the biggest reasons Terraform maps are so useful. It creates one resource instance for each map item, and the map key becomes the resource instance identity.

Example: create resource groups from a map

variable "resource_groups" {


description = "Resource groups to create."
type = map(object({
location = string
tags     = map(string)
}))
default = {
dev = {
location = "australiaeast"
tags = {
environment = "dev"
owner       = "platform"
}
}
prod = {
location = "australiaeast"
tags = {
environment = "prod"
owner       = "platform"
}
}
}
}
resource "azurerm_resource_group" "this" {
for_each = var.resource_groups
name     = "rg-${each.key}"
location = each.value.location
tags     = each.value.tags
}

This creates stable resource addresses:

azurerm_resource_group.this["dev"]


azurerm_resource_group.this["prod"]
Why this is better than count: if you add a new key such as test, Terraform adds that specific instance without shifting existing numeric indexes.
Structured lists

Terraform list of objects

A Terraform list of objects is useful when you need an ordered list where each item has several attributes.

Example: list of subnet objects

variable "subnets" {


description = "Subnets to create."
type = list(object({
name           = string
address_prefix = string
}))
default = [
{ name = "snet-web", address_prefix = "10.0.1.0/24" },
{ name = "snet-app", address_prefix = "10.0.2.0/24" }
]
}

This is readable, but if the subnet name is the real identity, it is often better to convert the list to a map before using for_each.

Convert list of objects to a map

locals {


subnets_by_name = {
for subnet in var.subnets : subnet.name => subnet
}
}

The local value above creates a map where each key is the subnet name.

Production module pattern

Terraform map of objects

A Terraform map of objects is one of the best patterns for reusable Terraform modules. Each map key gives the object a stable identity, and each object can contain multiple attributes.

Example: map of metric alerts

variable "metric_alerts" {


description = "Metric alerts keyed by alert name."
type = map(object({
metric_name = string
operator    = string
threshold   = number
severity    = number
enabled     = bool
}))
default = {
cpu_high = {
metric_name = "Percentage CPU"
operator    = "GreaterThan"
threshold   = 80
severity    = 2
enabled     = true
}
memory_low = {
metric_name = "Available Memory Bytes"
operator    = "LessThan"
threshold   = 1073741824
severity    = 2
enabled     = true
}
}
}
Production recommendation: when each item represents a real infrastructure object, prefer map(object({...})) over list(object({...})) unless order is genuinely required.
Nested configuration

Terraform nested maps

Terraform nested maps are useful for environment-specific, region-specific, or workload-specific configuration. They help you keep related settings together.

Example: environment configuration

variable "environment_config" {


description = "Environment-specific configuration."
type = map(object({
location = string
vm_size  = string
tags     = map(string)
}))
default = {
dev = {
location = "australiaeast"
vm_size  = "Standard_B2s"
tags = {
environment = "dev"
cost_center = "platform"
}
}
prod = {
location = "australiaeast"
vm_size  = "Standard_D4s_v5"
tags = {
environment = "prod"
cost_center = "platform"
}
}
}
}

Access nested map values

locals {


selected_config  = var.environment_config[var.environment]
selected_region  = local.selected_config.location
selected_vm_size = local.selected_config.vm_size
}
Keep nesting under control: nested maps are powerful, but too much nesting can make Terraform modules hard to read. Use clear names and document expected inputs.
Data transformation

How to convert a Terraform list to a map

There are two common ways to convert a Terraform list to a map: use a for expression or use the zipmap function.

Option 1: convert a list of objects to a map

variable "apps" {


type = list(object({
name = string
port = number
}))
default = [
{ name = "api", port = 8080 },
{ name = "web", port = 80 }
]
}
locals {
apps_by_name = {
for app in var.apps : app.name => app
}
}

This creates a map where each key is the app name:

{


api = { name = "api", port = 8080 }
web = { name = "web", port = 80 }
}

Option 2: use zipmap with two lists

variable "environment_names" {


type    = list(string)
default = ["dev", "test", "prod"]
}
variable "vm_sizes" {
type    = list(string)
default = ["Standard_B2s", "Standard_D2s_v5", "Standard_D4s_v5"]
}
locals {
vm_size_by_environment = zipmap(var.environment_names, var.vm_sizes)
}
Important: zipmap depends on both lists being in the correct order. The first key matches the first value, the second key matches the second value, and so on.
Advanced pattern

Preserving order without relying on list indexes

Sometimes you need resources to stay in a predictable order, but you do not want Terraform to identify resources by raw list index. This is where a custom sortable key is useful.

Using list indexes directly can be risky. If you insert, remove, or reorder items, Terraform may see the indexes differently and plan unnecessary changes. Maps are more stable because each item has a key, but maps are sorted by key, not by the order you typed them in the file.

Important: Terraform maps are key-based. Do not assume a map will preserve the visual order from your .tf file. If order matters, design the order explicitly.

Example: list of objects with an explicit order field

variable "environments" {


description = "Ordered list of environments."
type = list(object({
id   = number
name = string
}))
default = [
{ id = 1, name = "dev" },
{ id = 2, name = "test" },
{ id = 3, name = "prod" }
]
}

Now convert the list into a map with a stable, sortable key:

locals {


environments_by_order = {
for env in var.environments : format("%04d", env.id) => env
}
}

This produces keys like:

0001


0002
0003

You can then use the generated map with for_each:

resource "null_resource" "environment" {


for_each = local.environments_by_order
triggers = {
order = each.key
name  = each.value.name
}
}

Why use format("%04d", env.id)?

The format() function pads numbers so lexical sorting works as expected. Without padding, string sorting can produce unexpected ordering such as 1, 10, 2. With padding, the order becomes 0001, 0002, 0010.

Best use case: use this pattern when you need both stable Terraform resource keys and a human-controlled order.
Even simpler option: if order does not matter for the actual infrastructure, do not force order at all. Use a normal map with clear keys and let Terraform manage each resource by identity.
Functions

Useful Terraform functions for lists and maps

FunctionUseful forExampleNotes
length() Count items in a list, map, or set. length(var.subnets) Useful with validation and conditional logic.
lookup() Read a map value with a fallback. lookup(var.vm_sizes, "prod", "Standard_B2s") Good when a key may be optional.
merge() Combine maps. merge(var.default_tags, var.extra_tags) Commonly used for tags.
keys() Return map keys as a list. keys(var.vm_sizes) Map keys are returned in lexicographical order.
values() Return map values as a list. values(var.vm_sizes) Useful for outputs, but remember values follow map key ordering.
toset() Convert a list to a set of unique values. toset(var.regions) Useful with for_each for simple strings.
tomap() Convert a compatible object value to a map. tomap({ dev = "small", prod = "large" }) Do not confuse this with converting any list into a map.
zipmap() Create a map from a keys list and values list. zipmap(var.names, var.values) Both lists must align correctly.
format() Create formatted strings, including padded order keys. format("%04d", var.id) Useful when you need sortable keys like 0001.
flatten() Flatten nested lists into one list. flatten([["web"], ["app", "db"]]) Useful before transforming nested data.
Best practices

Best practices for Terraform lists and maps

  • Use lists when order matters.
  • Use maps when identity matters.
  • Prefer maps for long-lived resources.
  • Prefer for_each with maps over count with lists for named resources.
  • Use map(object({...})) for scalable module inputs.
  • Use list(object({...})) only when order is meaningful.
  • Use clear map keys such as dev, test, prod, primary, and secondary.
  • Use explicit order fields if you need predictable ordering.
  • Use padded keys such as 0001, 0002, and 0010 when lexical sorting matters.
  • Use validation blocks to catch incorrect input early.
  • Use locals to transform input data into the shape your resources need.
  • Avoid over-nesting maps if it makes the module hard to understand.

Example: input validation

variable "environment" {


description = "Deployment environment."
type        = string
validation {
condition     = contains(["dev", "test", "prod"], var.environment)
error_message = "Environment must be one of: dev, test, prod."
}
}
Real-world example

Real-world pattern: tags with maps

Tags are naturally key-value data, so Terraform maps are a good fit.

variable "default_tags" {


description = "Default tags applied to all resources."
type        = map(string)
default = {
managed_by = "terraform"
owner      = "platform"
}
}
variable "environment_tags" {
description = "Environment-specific tags."
type        = map(string)
default = {
environment = "dev"
cost_center = "cloud"
}
}
locals {
tags = merge(var.default_tags, var.environment_tags)
}

This keeps your tagging strategy centralised and avoids repeating the same tags across every resource block.

FAQs

Terraform lists and maps FAQs

What is the difference between Terraform lists and Terraform maps?

A Terraform list is an ordered collection accessed by index. A Terraform map is a key-value collection accessed by key. Use lists for ordered values and maps for named values.

Should I use a list or map with for_each?

Use a map when each resource needs stable identity. for_each works naturally with maps and sets of strings. For named resources, maps are usually easier to maintain than lists.

Can I use a plain list directly with for_each?

For resources and modules, for_each expects a map or a set of strings. If you have a list, either convert it to a set with toset() for simple strings, or convert it to a map with a for expression when you need stable keys.

How do I convert a Terraform list to a map?

Use a for expression when converting a list of objects to a map. Use zipmap() when you have one list of keys and one list of values.

Can Terraform maps contain objects?

Yes. map(object({...})) is a common pattern for reusable modules. It lets each key represent one resource, environment, alert, subnet, or policy assignment.

Can Terraform lists contain objects?

Yes. list(object({...})) is useful when you need an ordered collection of structured items. If each object has a unique name, consider converting the list to a map for for_each.

Are Terraform maps ordered?

Maps are key-based, not position-based. When Terraform converts map keys to a list, the keys are returned in lexicographical order. Do not design resource identity around the order you typed map items in the file.

How do I preserve order in Terraform?

If order truly matters, use an explicit order field such as id or order, then generate padded sortable keys with format(). For example, format("%04d", env.id) creates keys like 0001, 0002, and 0010.

Should I use map or object in Terraform?

Use map when all values have the same type. Use object when you need named attributes with different types. For production modules, map(object({...})) is often the most practical structure.

Is tomap the right way to convert a list to a map?

Usually no. tomap() converts a compatible object value into a map. To convert a list into a useful map, use a for expression or zipmap().

Conclusion: Terraform lists or Terraform maps?

Terraform lists and Terraform maps are both useful, but they solve different problems. Use lists when order matters. Use maps when identity, readability, and maintainability matter.

For production infrastructure, maps are usually the safer default for repeatable resources because they work cleanly with for_each and give every resource a stable key. If you also need predictable ordering, add an explicit order field and generate padded keys such as 0001, 0002, and 0010.

A good final rule is simple: lists are for sequence, maps are for identity, and explicit order keys are for cases where both matter.