Subscribe to the Spacelift blog newsletter, Mission Infrastructure |

Sign Up ➡️

Terraform

How to Use Terraform Merge Function – Examples

How to Use Terraform Merge Function - Examples

In Terraform, a “function” refers to a built-in or user-defined operation that can be performed on input values to produce an output value. The Terraform merge() function takes two or more maps or objects and returns a single combined map. When the same key appears in multiple maps, the value from the last map in the argument list wins.

In this article, we will take a look at what the Terraform merge function is and how to use it, with a few useful practical examples showing how to merge lists of maps, lists of objects, and apply merged lists of tags to cloud resources.

What we’ll cover:

  1. What is merge in Terraform?
  2. How to use Terraform merge
  3. Shallow vs. deep merging in Terraform
  4. Best practices for using the Terraform merge function

What is merge in Terraform?

Terraform merge is a built-in function that takes an arbitrary number of maps or objects and returns a single map or object containing a merged set of elements from all arguments. If the same key appears in multiple maps, the value from the later map overrides earlier ones. 

It’s useful for modular configuration and dynamic variable injection.

Keep in mind that the merge function is available in Terraform versions 0.12 and later. If you are using an earlier version of Terraform, you might need to use alternative methods like the concat function or the for expression to achieve similar merging behavior.

How to use Terraform merge

Let’s explore how to practically use Terraform merge with a few examples.

Example 1: Terraform merge maps

In Terraform, we define a map of objects within {}.

We can use the merge function by defining the maps we want to merge:

merge(map1, map2, ...)

Consider we have two maps of objects we want to merge together, defined in our Terraform locals declaration:

locals {
  map1 = {
    luke  = "jedi"
    yoda  = "jedi"
    darth = "sith"
  },
  map2 = {
    quigon     = "jedi"
    palpantine = "sith"
    hansolo    = "chancer"
  }
}

We can use the merge function like so:

merged_map = merge(local.map1, local.map2)

Which would produce the following value for merged_map:

merged_map = {
  luke       = "jedi"
  yoda       = "jedi"
  darth      = "sith"
  quigon     = "jedi"
  palpantine = "sith"
  hansolo    = "chancer"
}

Note that when merging maps, if there are duplicate keys between the input maps, the value from the last map provided as an argument will take precedence.

In other words, if the input maps contain conflicting keys, the value from the rightmost map will be used in the merged map.

For example, in our example above, if we defined the value as jedi for the key darth again in map2 (controversial!), then the resulting output would be darth = "jedi".

Example 2: Terraform merge multiple maps

The merge function accepts any number of map arguments and returns a single combined map. If keys overlap, the later map’s value overrides the earlier one. 

For example:

variable "map1" {
  default = {
    a = 1
    b = 2
  }
}

variable "map2" {
  default = {
    b = 3
    c = 4
  }
}

output "merged" {
  value = merge(var.map1, var.map2)
}

This returns { a = 1, b = 3, c = 4 }

For dynamic or nested merges, use merge() within loops or combine with for expressions as needed.

Example 3: Terraform merge lists of objects

Terraform does not have a built-in function to directly merge lists of objects into a single flat list. You can achieve this using concat() or flatten(), depending on how your data is structured.

One way is to use the concat function to take two or more lists of objects and combine them into a single list.

locals {
  list1 = [
    { key = "luke",      value = "jedi" },
    { key = "yoda",      value = "jedi" }
  ]

  list2 = [
    { key = "darth",     value = "sith" },
    { key = "palpatine", value = "sith" }
  ]

  merged_list = concat(local.list1, local.list2)
}

merged_list produces the following flat list:

[
  { key = "luke",      value = "jedi" },
  { key = "yoda",      value = "jedi" },
  { key = "darth",     value = "sith" },
  { key = "palpatine", value = "sith" }
]

If you need a map instead of a list, for example, to look up objects by key, use a for expression to convert the combined list into a map keyed by key:

locals {
  merged_map = { for elem in concat(local.list1, local.list2) : elem.key => elem.value }
}

This produces a map, not a list:

{
  "luke"      = "jedi"
  "yoda"      = "jedi"
  "darth"     = "sith"
  "palpatine" = "sith"
}

If duplicate keys exist across the two lists, the last one wins.

Use flatten() when your input is already a list of lists rather than two separate variables:

locals {
  list_of_lists = [
    [
      { key = "luke",  value = "jedi" },
      { key = "yoda",  value = "jedi" }
    ],
    [
      { key = "darth",     value = "sith" },
      { key = "palpatine", value = "sith" }
    ]
  ]

  merged_list = flatten(local.list_of_lists)
}

flatten() collapses all nested lists into one flat list, the output is identical to the concat() example above.

Example 4: Terraform merge lists of maps

When you have a list of maps stored in a variable or local and want to merge them all into a single map, use the ... expansion operator. This expands a list into individual arguments, which is exactly what merge() expects.

Going back to the previous examples, the following will again result in the same output:

locals {
  list_of_maps = [
    { environment = "production", project = "myapp"    },
    { cost_center = "12345",      team    = "engineering" },
    { project     = "override"  } # overrides the first map's "project" value
  ]

  merged_map = merge(local.list_of_maps...)
}

merged_map produces:

{
  cost_center = "12345"
  environment = "production"
  project     = "override"
  team        = "engineering"
}

The ... operator passes each map in the list as a separate argument to merge(). Because merge() follows last-write-wins order, the project key from the third map ("override") takes precedence over the value in the first map ("myapp").

This pattern is especially useful when the number of maps is dynamic — for example, when building a merged config from a for_each result or a variable-length list of environment-specific overrides.

Example 5: Terraform merge tags

One common use case for using the merge function is to merge together lists of tags applied to cloud resources.

locals {
  default_tags = {
    Environment = "Production"
    Project     = "MyProject"
  }

  additional_tags = {
    CostCenter = "12345"
    Department = "Engineering"
  }

  merged_tags = merge(local.default_tags, local.additional_tags)
}

This will result in merged_tags containing the combined list, which can then be applied to the resource as needed.

{
  Environment = "Production"
  Project     = "MyProject"
  CostCenter  = "12345"
  Department  = "Engineering"
}

The examples below show the list of merged_tags applied to the example AWS EC2 instance example1, where instance example2 will only have the default_tags applied.

resource "aws_instance" "example1" {
  ami           = "ami-0c55b159cbfafe1f0" # Replace with the AMI ID of your desired instance
  instance_type = "t2.micro"               # Replace with your desired instance type
  subnet_id     = "subnet-12345678"        # Replace with the subnet ID where you want to launch the instance

  tags = local.merged_tags
}

resource "aws_instance" "example2" {
  ami           = "ami-0c55b159cbfafe1f0" # Replace with the AMI ID of your desired instance
  instance_type = "t2.micro"               # Replace with your desired instance type
  subnet_id     = "subnet-12345678"        # Replace with the subnet ID where you want to launch the instance

  tags = local.default_tags
}

Example 6: Terraform merge with for_each

A common reason to combine merge with for_each is when every resource in a loop needs the same baseline configuration plus its own overrides. Defining defaults once and merging them into each iteration keeps your code short and makes precedence explicit.

locals {
  default_tags = {
    Environment = "Production"
    ManagedBy   = "Terraform"
  }

  buckets = {
    logs    = { name = "myproject-logs",    tags = { Purpose = "Logging" } }
    assets  = { name = "myproject-assets",  tags = { Purpose = "StaticAssets" } }
    backups = { name = "myproject-backups", tags = { Purpose = "Backups", Environment = "Compliance" } }
  }
}

resource "aws_s3_bucket" "this" {
  for_each = local.buckets

  bucket = each.value.name
  tags = merge(
    local.default_tags,
    each.value.tags
  )
}

Notice how the backups bucket ends up with Environment = "Compliance" even though the default is Production. That’s merge resolving duplicate keys using last-write-wins. 

On each iteration, merge receives two maps in a clear order of precedence. Shared defaults come first, per-bucket tags come second, and the later map always wins.

This pattern works for anything map-shaped, including tags, environment variables, or policy settings. Just remember that merge is shallow, so nested maps are replaced rather than combined. 

If default_tags contained a nested map, the per-bucket tags would replace it wholesale instead of deep-merging into it.

What is the difference between shallow and deep merging in Terraform?

Terraform’s built-in merge() function performs only shallow merges, meaning it operates only at the top level of the maps you provide. When the same key exists in two maps and its value is itself a nested map, merge() does not combine those nested maps. It discards the earlier nested map entirely and replaces it with the one from the later argument. 

This means that in the example below, result.tags would only contain cost_center = "12345". The environment and owner keys are silently lost:

locals {
  base_config     = { tags = { environment = "production", owner = "platform-team" } }
  override_config = { tags = { cost_center = "12345" } }
  result          = merge(local.base_config, local.override_config)
}

A deep merge, by contrast, would descend into both tags maps recursively and combine their keys, producing a result containing all three: environment, owner, and cost_center.

Terraform does not provide a native deep merge function. For most use cases involving flat tag maps or scalar config values, this is not a problem. 

If you do need recursive merging, the cleanest options are to restructure your data so all keys live at the top level, or to use the isometry/deepmerge provider, which adds a provider::deepmerge::mergo() function at the cost of an extra provider dependency.

Best practices for using the Terraform merge function

Here are practical tips to use Terraform’s merge function effectively when combining multiple configuration maps:

  • Understand key precedence: When keys overlap, values from later maps in the argument list take precedence. This is useful for layering environment-specific settings over shared defaults.
  • Avoid type confusion: All arguments must be maps. If combining dynamic values, use tomap() to ensure type consistency.
  • Use for module inputs: merge is often used to simplify module input by combining global and local variable maps.
  • Handle nulls gracefully: Use the null coalescing operator (??) when dealing with optional inputs to avoid passing null maps.

Key points

In this post, we covered another Terraform functionmerge, and learned what it is and how to use it. We went through examples showing how to merge lists of maps, lists of objects and apply merged lists of tags to cloud resources.

Terraform is powerful, but to achieve an end-to-end secure GitOps approach, you need a platform that can orchestrate your Terraform workflows. Spacelift takes managing Terraform to the next level by giving you access to a purpose-built infrastructure orchestration platform with features such as:

  • Policies (based on Open Policy Agent)
  • Multi-IaC workflows
  • Self-service infrastructure via Blueprints and templates
  • Drift detection and remediation
  • Integrations with any third-party tools
  • Spacelift Intelligence for natural language provisioning, diagnostics, and operational insight

If you want to learn more about Spacelift, create a free account today or book a demo with one of our engineers.

Note: New versions of Terraform are placed under the BUSL license, but everything created before version 1.5.x stays open-source. OpenTofu is an open-source version of Terraform that expands on Terraform’s existing concepts and offerings. It is a viable alternative to HashiCorp’s Terraform, being forked from Terraform version 1.5.6.

Manage Terraform better with Spacelift

Orchestrate Terraform workflows with policy as code, programmatic configuration, context sharing, drift detection, resource visualization, and more.

Start free trial

Frequently asked questions

  • How does merge handle duplicate keys in order of precedence?

    When using the Terraform merge() function, duplicate keys are resolved by last-write-wins order, meaning later maps in the argument list take precedence over earlier ones.

    This behavior ensures that if multiple maps contain the same key, the value from the last map provided will overwrite the previous ones. Terraform processes the arguments from left to right and applies values accordingly.

  • How do I merge two Terraform state files?

    To merge two Terraform state files, you typically use the terraform state subcommands, such as terraform state mv or terraform import, to move or import resources from one state into another. Terraform does not provide an automatic or built-in merge function, so this process must be done manually and carefully to avoid conflicts or inconsistencies. It’s important to back up both state files and verify resource mappings before making changes.

  • How does merge() work in OpenTofu?

    The merge() function in OpenTofu combines two or more maps into a single map. When keys overlap, values from later arguments override those from earlier ones. It’s commonly used to layer default configurations with environment-specific overrides, such as merge(var.default_tags, var.extra_tags).

  • When should I use Terraform merge, concat, or zipmap functions?

    Use merge to combine multiple maps into one, with later keys overriding earlier ones, ideal for layering default and custom tags. Use concat to join lists end-to-end, useful for building subnet or security rule collections. Use zipmap to pair a list of keys with a list of values, creating a map from two parallel lists.

Terraform Functions Cheat Sheet

Grab our ultimate cheat sheet PDF for all
the Terraform functions you need on hand.

Share your data and download the cheat sheet