Tags in Terraform are key-value pairs associated with cloud resources. They allow categorization and organization of resources for better management, cost allocation, environment identification such as production, staging, development, and automation. Tags are one of the most underused tools in a Terraform practitioner’s toolkit. Done well, they give a consistent way to track costs, enforce compliance, manage access control, and automate operations across cloud infrastructure. Skip them entirely and you are left with ungoverned resources, surprise bills, and no easy way to answer who owns this.
The merge function is the backbone of any tagging strategy in Terraform. 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 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.
What Tags Are in Terraform and Why Merge Matters
Tags in Terraform are key-value pairs associated with cloud resources. They allow you to categorize and organize these resources for better management, cost allocation, environment identification, and automation.
The real world impact of consistent tagging is financial visibility and operational control. Without a consistent tag map, cost allocation reports fragment, compliance audits fail, and automation policies cannot reliably select resources. Merge enables a single source of truth for tags to propagate while still allowing targeted overrides.
Contextually, tagging sits at the intersection of configuration management, governance, and cost management. When teams adopt Terraform modules, the need to combine organization defaults with caller supplied values becomes a recurring pattern. Merge provides the mechanism to layer those values without duplication.
How merge() Combines Maps for Tags
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.
A canonical example shows a default variable and a resource-level override:
```hcl
variable "common_tags" {
type = map(string)
default = {
Environment = "Development"
}
}
resource "awsinstance" "myinstance" {
# ... other configuration options
tags = merge(var.common_tags, {
Name = "My Web Server"
})
}
```
In this pattern, var.common_tags supplies the baseline and the inline map supplies the resource specific Name tag. The resulting tag map contains both entries. The impact for operators is that a single variable change propagates to all resources that reference it, while individual resources retain the ability to add or refine entries.
The same pattern scales to more layers. When writing modules, always accept a tags variable and merge it with your module's required tags so callers can add their own. This keeps module authors in control of required metadata while preserving caller flexibility.
Precedence and Last-Wins Behavior
Understand key precedence: When keys overlap, values from later maps in the argument list take precedence. Terraform processes the arguments from left to right and applies values accordingly.
The last-wins rule makes it natural to express precedence. Put your defaults first and your overrides last. When the same key exists in two maps, the value from the later map wins. This behavior is deterministic and allows explicit layering of organization defaults, environment tags, and resource tags.
The practical consequence is that an accidental duplicate key silently overwrites the earlier value. Teams rely on this to ensure environment-specific tags override organization defaults without needing conditional logic. The risk is that an unintended late map can erase an earlier intentional value, so ordering must be reviewed in plan output.
Contextually, precedence ties directly to module composition. A module that merges var.tags after its own required tags allows callers to override module defaults. Reversing the order would lock callers out of overrides and create hard to debug drift.
Building Tag Hierarchies with Merge
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.
A hierarchy typically flows:
- Organization defaults set at the provider or root module
- Environment defaults such as Development, Staging, Production
- Team or compliance tags
- Resource specific tags such as Name
Each layer is a map. Merge combines them left to right, so later layers win. The impact is reduced duplication and a single place to update defaults. Changes to organization defaults flow automatically unless explicitly overridden downstream.
In practice, teams define var.commontags or var.defaulttags at the root, then merge with local maps per environment, then merge again at the resource. This creates auditability. A plan diff shows exactly which layer introduced a tag change.
Module Patterns for Tag Merging
When writing modules, always accept a tags variable and merge it with your module's required tags so callers can add their own.
A module pattern looks like:
```hcl
variable "tags" {
type = map(string)
default = {}
}
resource "aws_instance" "example" {
tags = merge(
{
ManagedBy = "Terraform"
Module = "example"
},
var.tags
)
}
```
The module supplies required tags first, then merges caller supplied tags last so callers can override. The impact is consistent metadata across all module instances and the ability for consumers to inject project specific tags without forking the module.
Contextually, this pattern enables reuse across teams with different tagging standards. The module remains stable while the caller controls the final tag set.
Provider-Specific Tag Handling and Ignore Patterns
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. The reference material notes AWS, Azure, and Google Cloud as the three major providers with varying implementation.
In that case, 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.
hcl
resource "aws_instance" "my_instance" {
# ... other configuration options
lifecycle {
ignore_changes = [tags]
}
}
Using ignore_changes can be helpful, but it’s 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.
hcl
provider "aws" {
# ... other configuration options
ignore_tags = [
"CostCenter", # Ignore changes to tags with this key
]
}
The impact of ignore_tags is reduced plan noise for tags managed outside Terraform, such as by billing systems. The contextual risk is that ignored tags can drift silently, making audits difficult if external changes are not governed.
Lifecycle Ignore Changes Versus Provider Ignore Tags
Lifecycle ignorechanges operates per resource and can target the entire tags map or specific attributes. Provider ignoretags operates globally for the provider and targets specific tag keys.
Choosing between them depends on scope. A team that wants to ignore a single tag key across all AWS resources uses provider ignoretags. A team that wants to ignore all tag changes for a specific resource that is managed by an external process uses lifecycle ignorechanges.
Both mechanisms prevent Terraform from showing unexpected changes in your Terraform plan. The governance impact is that ignored tags must still be monitored outside Terraform to avoid compliance gaps.
Nested Maps and the Deep Merge Limitation
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:
hcl
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.
The impact of the shallow merge behavior is that users who nest tags inside a config object can lose data silently. The contextual mitigation is to keep tag maps flat and avoid nesting tag maps inside other maps when merging.
Argument Expansion for List of Tag Maps
If your maps are already in a list or tuple, use Terraform's function argument expansion syntax with merge.
Argument expansion allows merging a dynamic collection of tag maps without manually enumerating them. The impact is concise code when tags are generated from a list of modules or environments.
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.
OpenTofu Parity and Merge Semantics
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.defaulttags, var.extratags).
The parity between Terraform and OpenTofu means tagging strategies port cleanly between the two tools. The impact for teams evaluating OpenTofu is that existing merge-based tag hierarchies continue to work with the same precedence rules.
Merge Versus Concat Versus Zipmap
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.
The contextual distinction is data shape. Merge operates on maps. Concat operates on lists. Zipmap creates a map from two lists. Tagging strategies rely on merge because tags are maps.
State File Merging Limitations
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.
State file merging is distinct from map merging. The former is about resource identity in state, the latter is about configuration composition. Confusing the two leads to operational risk.
Best Practices for Effective Merge Tagging
Best practices for using the Terraform merge function:
- Understand key precedence: When keys overlap, values from later maps in the argument list take precedence
- Put defaults first and overrides last to express precedence naturally
- Keep tag maps flat to avoid shallow merge data loss with nested maps
- Accept a tags variable in modules and merge it with module required tags
- Use provider-level ignore_tags for keys managed externally
- Use lifecycle ignore_changes for resources where tags are managed outside Terraform
- Review plan output for silent overwrites caused by last-wins behavior
The impact of these practices is consistent, auditable tagging with minimal drift and predictable plan output.
Conclusion
Terraform merge tags provide a deterministic mechanism to layer organization defaults, environment values, and resource-specific metadata into a single tag map per resource. The last-wins precedence model aligns with the natural mental model of defaults overridden by specifics. The shallow merge limitation requires flat tag maps or external deep merge providers when nested structures are unavoidable. Provider-specific ignore mechanisms allow teams to accommodate tags managed outside Terraform without constant plan churn. When combined with module-level tag variables and argument expansion, merge enables scalable tagging strategies that remain maintainable as infrastructure grows. The continued reliance on explicit ordering and flat maps means that governance and code review remain essential to prevent silent tag loss and drift.