Terraform Merge Function for Tag Maps and Resource Tagging Strategies

Tagging in Terraform begins as a simple key-value annotation and rapidly becomes a composition problem. Default tags, environment-specific tags, team tags, compliance tags, and resource-specific tags all need to be combined into a single tag map for each resource. The merge function is the backbone of any tagging strategy in Terraform. It takes maps or objects and combines them into a single map or object. The merge function is the foundation of any tagging strategy in Terraform. Use it to build tag hierarchies from organization defaults through environment-specific overrides to resource-level tags. The last-wins rule makes it natural to express precedence. Put defaults first and overrides last. When writing modules, always accept a tags variable and merge it with your module's required tags so callers can add their own. For related map operations, see posts on the lookup function and the values function.

The 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 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.

Terraform processes the arguments from left to right and applies values accordingly. 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 is useful for modular configuration and dynamic variable injection. Keep in mind that the merge function is available in Terraform versions 0.12 and later.

What Merge Does in Terraform

The merge function combines multiple maps, key-value pairs, and tags are typically defined as a map. The function is useful for modular configuration and dynamic variable injection. The real-world consequence for operators is that a single source of truth for tagging can be established at the organization level and then refined without duplicating declarations across hundreds of resources. This reduces drift and makes audits tractable.

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.

Tag Merging Patterns with Variables and Resources

One useful application of the merge function in Terraform is adding additional tags to resources to combine with your default set of tags. The merge function combines multiple maps, key-value pairs, and tags are typically defined as a map.

In this example, a tag is set as the default in the variable, and the merge function is used to add a name tag using the tags attribute in the resource.

variable "common_tags" { type = map(string) default = { Environment = "Development" } } resource "aws_instance" "my_instance" { # ... other configuration options tags = merge(var.common_tags, { Name = "My Web Server" }) }

The impact for a user is that the Environment tag is applied consistently across all resources that reference var.common_tags, while each resource can add its own Name tag without redeclaring the common set. The pattern scales to multiple layers: organization defaults, then environment defaults, then resource-specific additions, each layer placed later in the argument list to override earlier values.

If your maps are already in a list or tuple, use Terraform's function argument expansion syntax (...) with merge. Argument expansion allows a collection of maps to be spread into individual arguments to merge.

Provider Behavior and Tag Management Differences

Tag implementation varies significantly across cloud providers. Understanding how each provider handles defaults, inheritance, and naming constraints prevents subtle misconfigurations that are easy to miss in a plan.

AWS has the most complete tagging support of the three major providers.

Provider-level and lifecycle controls affect how tag changes are interpreted by Terraform. To ignore changes to specific tags, you can configure Terraform to ignore changes to specific tags. This prevents Terraform from showing unexpected changes in your Terraform plan.

To ignore changes to Terraform tags, you can use the lifecycle block in your resource definition, which is the most common approach and works for all Terraform resources. Simply set the value of ignore_changes to "tags" to ignore all tag changes.

resource "aws_instance" "my_instance" { # ... other configuration options lifecycle { ignore_changes = [tags] } }

Using ignore_changes can be helpful, but it is essential to understand why tags are being modified externally and ensure these external modifications are consistent with your infrastructure management practices.

Some Terraform providers, like the AWS provider, offer a provider-level configuration option called ignore_tags. This option applies to all resources that provider manages in your Terraform configuration.

provider "aws" { # ... other configuration options ignore_tags = [ "CostCenter", # Ignore changes to tags with this key ] }

The impact is that tags managed outside Terraform, such as by a cost allocation tool or a manual console edit, do not cause perpetual diff noise. The contextual layer is that ignoretags is provider-scoped, while lifecycle ignorechanges is resource-scoped, giving operators two granularities for handling external tag mutations.

Last-Wins Precedence and Argument Expansion

The merge function is the foundation of any tagging strategy in Terraform. Use it to build tag hierarchies from organization defaults through environment-specific overrides to resource-level tags. The last-wins rule makes it natural to express precedence. Put your defaults first and your overrides last.

The merge function takes maps or objects and combines them into a single map or object. When writing modules, always accept a tags variable and merge it with your module's required tags so callers can add their own.

When keys overlap, values from later maps in the argument list take precedence. Understand key precedence: when keys overlap, values from later maps in the argument list take precedence.

Argument expansion syntax (...) with merge allows a list or tuple of maps to be unpacked into merge arguments. This is useful when the set of tag layers is itself generated or stored in a variable.

Module Tag Hierarchies and Composition

Tagging is one of those things that starts simple and gets complicated fast. You have default tags, environment-specific tags, team tags, compliance tags, and resource-specific tags, all of which need to be combined into a single tag map for each resource. The merge function is the backbone of any tagging strategy in Terraform.

When writing modules, the recommended pattern is to accept a tags variable from the caller and merge it with the module's required tags. This preserves caller control while enforcing module requirements. The last-wins rule ensures that caller-supplied values override module defaults if placed later in the merge call.

The impact for module authors is a clean separation between enforced tags and optional caller tags. Callers can add their own tags without forking the module.

Nested Map Limitations and Deep Merge Behavior

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 behavior creates silent data loss for users who assume recursive merging. The example with baseconfig and overrideconfig demonstrates that environment and owner disappear from the resulting tags map. The consequence is incomplete tagging and potential compliance gaps.

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.

The contextual implication is that tag maps should remain flat. Nesting should be avoided for tags, or the nesting should be flattened before merging.

Merge Versus Related Map 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.

| Function | Input Types | Output Type | Primary Use Case | Override Behavior |
| merge | maps or objects | map or object | layer default and custom tags | later argument wins |
| concat | lists | list | join lists end-to-end | preserves order |
| zipmap | list of keys, list of values | map | create map from parallel lists | pairs by index |

The table clarifies when merge is appropriate versus concat or zipmap. Choosing the wrong function leads to type errors or incorrect data structures.

OpenTofu Compatibility and State File Merging

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 is commonly used to layer default configurations with environment-specific overrides, such as merge(var.defaulttags, var.extratags).

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 is important to back up both state files and verify resource mappings before making changes.

The impact is that state merging is an operational task, not a function call. It requires careful planning and validation.

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
  • Place defaults first and overrides last to express precedence clearly
  • Keep tag maps flat to avoid nested map replacement issues
  • Accept a tags variable in modules and merge it with required tags to allow caller extension
  • Use argument expansion (...) when merging a list or tuple of maps
  • Consider lifecycle ignorechanges or provider ignoretags when tags are managed externally
  • Document which layer provides each tag to aid troubleshooting

These practices reduce drift, improve auditability, and make tag inheritance predictable across environments.

Conclusion

The merge function provides a deterministic, left-to-right composition model for Terraform tag maps. Its last-wins semantics make it natural to express organization defaults, environment overrides, and resource-specific additions in a single expression. The real power emerges in module design, where accepting a tags variable and merging it with required tags creates an extensible contract between caller and module.

The limitations are equally important. Merge does not perform deep merging of nested maps, which can silently discard data. State file merging remains a manual operational process using terraform state mv and terraform import, with no built-in automatic merge. Provider differences, particularly AWS's comprehensive tagging support and the availability of ignore_tags at the provider level, shape how tag drift is handled in practice.

Effective tagging strategies therefore combine merge-based composition with explicit precedence design, flat map structures, and lifecycle controls for external modifications. When these elements are aligned, Terraform tag management scales from a handful of resources to large, multi-environment fleets without repetitive declarations or unexpected plan diffs.

Sources

  1. Source Name
  2. Source Name
  3. Source Name

Related Posts