Tags are one of the most underused tools in a Terraform practitioner’s toolkit. Done well, they give you a consistent way to track costs, enforce compliance, manage access control, and automate operations across your cloud infrastructure. Skip them entirely and you are left with ungoverned resources, surprise bills, and no easy way to answer “who owns this?”
In this article, we cover everything you need to know about using tags in Terraform, from the basics of adding key-value pairs to resources, to managing default tags at the provider level, enforcing required tags with lifecycle rules, and handling provider-specific differences across AWS, Azure, and Google Cloud.
What we will cover:
- What are tags in Terraform?
- Use cases for tags in Terraform
- How to manage resource tags using Terraform?
- How to add multiple tags to Terraform resources?
- What are Terraform default tags?
- How to ignore changes to Terraform to tags?
- How to merge Terraform tags?
- Provider differences in Terraform tags
- Tagging shared resources on AWS
- How to enforce required tags in Terraform
- Best practices for Terraform tags
What are tags in Terraform?
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 (e.g., production, staging, development), and automation. The keys can be anything you choose as long as they don’t conflict with any reserved keys from your cloud provider.
For cloud providers like AWS, Azure, and Google Cloud, Terraform leverages these tags to associate metadata with resources such as instances, networks, or databases. The key represents the category (e.g., “Environment”), while the value specifies the detail (e.g., “Production”).
Tags are implemented differently depending on the cloud provider you’re using. However, Terraform itself offers ways to set and manage tags consistently across providers using the tags attribute of a resource.
What is the difference between tags and tags_all in Terraform?
Terraform tags sets or overrides tags directly on a resource, while tags_all reflects the combined set of tags, including provider-level defaults. If a provider defines default tags via the default_tags block, those are automatically merged into tags_all but not into tags unless explicitly added.
For example, if you had an Azure VNet called example that you wanted to reference the tags on, you could use azurerm_virtual_network.example.tags_all.
Use cases for tags in Terraform
Tags are often used to apply metadata to resources, making it easier to identify, manage, and track costs. They can also help categorize resources by ownership, environment, project, or other criteria.
- Organization: Terraform tags help organize and group specific resources, making locating them within a large set easier.
Example tags:
-
env = "production"owner = "team-abc"purpose = "web-server"
- Cost management: You can use Terraform tags to identify resources associated with a particular project or cost center.
Example tag:
-
cost_center = "sales"
Consider tagging resources that have assigned reserved instances.
- Automation: Tags can be used to automate deployments or configurations based on specific tags. For example, you might only deploy resources with the tag
deploy = "true"during a production rollout.
You could also use them to enable targeting with configuration management tools such as Chef and Ansible or to denote which DevOps deployment strategy has been used.
Example tags:
-
auto_shutdown = "true"deploy = "true"ansible_managed = "true"deployment = "bluegreen"
- Access control: Tags can be used to define access control policies, ensuring that only authorized users or groups can manage specific resources.
For example, you could tag resources in the development environment with the key “Environment” and the value “Development.” Then, in the AWS IAM console, create a policy that allows the “DevOps” group to access resources with the “Environment” tag set to “Development.”
Note that tag-based access control might not be the most granular approach. It’s often best used in conjunction with other IAM roles and permissions.
- Disaster recovery and backup: Tags help manage disaster recovery and backup processes. For example:
-
backup = "monthly"
- Compliance: You can mark resources that need to comply with specific regulations. For example:
-
gdpr = "true"iso27001 = "true"
How to add tags to Terraform resources?
You can use the tags attribute to specify the map of key-value pairs you want to use as tags.
tags– (Optional) A mapping of tags to assign to the resource.
Each key in the map represents the tag name, and the corresponding value is the tag content.
Here are the basic steps for adding tags to resources in Terraform:
- Check if the resource supports tags (not all Terraform resources support tags)
- Add a tags block
- Define the key-value pairs (you can specify multiple tags by defining key-value pairs inside the tags block)
Example: Using tags in Terraform
This code snippet adds a tag to a resource, where the key is Environment and the value is Development.
tags = {
Environment = "Development"
}How to add multiple tags to Terraform resources?
You can include multiple key-value pairs inside the tags block to add multiple tags to Terraform resources. Each tag consists of a unique key and its corresponding value.
For example:
tags = {
Environment = "Development"
Owner = "Luke Skywalker"
Department = "Jedi Order"
}What are Terraform default tags?
Default tags in Terraform refer to tags applied to all or most of the resources in your configuration. These can be defined at a higher level (e.g., through variables or modules) to avoid repetition and ensure consistency across resources. You can define default tags either by setting them globally through variables or directly within each resource.
For example, to define tags in the variable defaults:
variable "common_tags" {
type = map(string)
default = {
Environment = "Development"
}
}
tags = var.common_tagsHow to override the default tags?
If you want to override the default tags with custom tags for specific resources, you can do this by explicitly defining the tags argument in the resource definition.
- Override at the resource level: If default tags are applied via the provider or module, you can add or change specific tags directly within the resource block.
- Merge default and custom tags: Use Terraform functions like merge() to combine the default tags with custom tags while giving priority to the custom ones.
How to ignore changes to Terraform tags?
Suppose external systems interact with resource tags (e.g., Configuration Management Databases or your cloud adds auto-generated tags for certain resources, such as Azure Databricks). 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.
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.
resource "aws_instance" "my_instance" {
# ... other configuration options
lifecycle {
ignore_changes = [tags]
}
}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
]
}How to merge Terraform 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.
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"
})
}Provider differences in Terraform tags
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
AWS has the most complete tagging support of the three major providers. The default_tags block in the provider applies tags automatically to all supported resources, and any resource-level tags are merged on top, with resource-level values winning on key conflicts.
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
Environment = "production"
ManagedBy = "terraform"
Owner = "platform-team"
}
}
}
resource "aws_s3_bucket" "example" {
bucket = "my-app-assets"
# This tag is merged with the default_tags above.
# The bucket will carry four tags in total.
tags = {
Purpose = "static-assets"
}
}The merged result is available via tags_all on any resource. Note that default_tags does not apply to all resources. Most notably, aws_autoscaling_group does not pick them up automatically. This is because ASGs dynamically create EC2 instances outside Terraform’s direct management, so the provider cannot propagate defaults to the launched instances.
The standard workaround is to use the aws_default_tags data source with a dynamic “tag” block on the ASG, or to define a local and merge() it explicitly into each resource. AWS imposes a limit of 50 tags per resource.
Azure
The AzureRM provider (v4.x) does not support provider-level default tags. Every resource must define its tags explicitly using the tags block. To avoid repetition, define a local and reference it throughout your configuration.
Note: If you use the AzAPI provider (commonly used for preview features and resources not yet supported by AzureRM), it does support default_tags at the provider level. For pure AzureRM configurations, the local-variable pattern below is the canonical approach.
locals {
common_tags = {
Environment = "production"
ManagedBy = "terraform"
Owner = "platform-team"
}
}
resource "azurerm_resource_group" "main" {
name = "rg-myapp-prod"
location = "West Europe"
tags = local.common_tags
}
resource "azurerm_virtual_network" "main" {
name = "vnet-myapp-prod"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
address_space = ["10.0.0.0/16"]
tags = merge(local.common_tags, {
Component = "networking"
})
}Two important Azure-specific caveats:
- Tag names are case-insensitive — Azure treats
environment,Environment, andENVIRONMENTas the same key. This can cause unexpected diffs if different resources use inconsistent casing, and can result in duplicate entries in Cost Management reports. Standardize your key casing (PascalCase is the most common convention). - Tag values are case-sensitive —
productionandProductionare treated as different values. Enforce allowed values via variable validation to prevent inconsistencies. - Resource group tags do not inherit automatically — child resources within a resource group do not inherit their tags. You must explicitly apply tags to each resource.
Azure’s limit is 50 tags per resource.
Google Cloud (GCP)
GCP’s tagging model is different from both AWS and Azure. There are two distinct concepts that are often conflated:
- Labels — key-value metadata attached to resources for organization, cost allocation, and automation. This is the equivalent of tags in AWS and Azure.
- Network tags — string values attached to Compute Engine VMs that are referenced by firewall rules. These are not metadata and have no key-value structure.
Terraform manages these through separate attributes. The labels attribute handles resource metadata; the tags attribute on google_compute_instance refers specifically to network tags.
resource "google_compute_instance" "app" {
name = "app-server-prod"
machine_type = "e2-standard-2"
zone = "us-central1-a"
# Labels = resource metadata (equivalent to tags in AWS/Azure)
labels = {
environment = "production"
managed_by = "terraform"
owner = "platform-team"
component = "app-server"
}
# Tags = network tags for firewall rules (not metadata)
tags = ["allow-http", "allow-https"]
boot_disk {
initialize_params {
image = "debian-cloud/debian-12"
}
}
network_interface {
network = "default"
}
}GCP label constraints to be aware of:
- Keys and values must be lowercase – uppercase letters are not allowed
- Only letters, numbers, hyphens (
-), and underscores (_) are permitted - Keys must start with a lowercase letter (international characters are also accepted)
- Maximum of 64 labels per resource; keys and values are limited to 63 characters each
Like AzureRM, GCP has no provider-level default labels. Use a local or module variable to propagate a common label set, and apply it with merge() per resource.
Tagging shared resources on AWS
As of version 3.38.0 of the Terraform AWS provider, the Terraform Configuration Language also enables provider-level tagging. Instead of defining tags on each resource, you can set them at the provider level, and they will propagate down to all supported resources automatically (except for scale sets), keeping your code clean and reducing duplication.
If you want to apply individual tags in addition to the default set, you can still use the AWS tags attribute on the resource in question and these will get added to the set. In the example below, the aws_vpc resource will be assigned all four tags.
provider "aws" {
# ... other configuration ...
default_tags {
tags = {
Environment = "Development"
Owner = "Luke Skywalker"
Department = "Jedi Order"
}
}
}
resource "aws_vpc" "example" {
# ... other configuration ...
tags = {
VNET_Name = "Dagobah system VNET"
}
}How to enforce required tags in Terraform
Relying on convention alone doesn’t scale. As teams grow, resources will inevitably be deployed without the required tags. Terraform provides two built-in ways to catch this early, and you can add a policy layer on top for team-wide enforcement.
Variable validation
The simplest approach is validating tag inputs at the variable level. Terraform evaluates these checks when generating a plan, so invalid values are caught before anything is created. This works well for constraining allowed values on tags like Environment:
variable "environment" {
type = string
description = "Deployment environment. Must be development, staging, or production."
validation {
condition = contains(["development", "staging", "production"], var.environment)
error_message = "environment must be one of: development, staging, production."
}
}Preconditions
For more granular checks, for example, verifying that a specific tag is present on a resource, use a precondition block inside the resource’s lifecycle block. Terraform evaluates these before creating or updating the resource:
resource "aws_instance" "app" {
ami = var.ami_id
instance_type = var.instance_type
tags = var.tags
lifecycle {
precondition {
condition = contains(keys(var.tags), "CostCenter")
error_message = "All EC2 instances must include a CostCenter tag."
}
}
}Policy enforcement with Spacelift
Variable validation and preconditions work at the individual resource level, but they rely on each module author remembering to add them. For team-wide enforcement across all stacks, use Spacelift’s plan policies, which are written in Open Policy Agent (OPA) and run automatically against every plan before it is applied:
package spacelift
required_tags := {"Environment", "Owner", "CostCenter", "ManagedBy"}
deny[msg] {
resource := input.terraform.resource_changes[_]
resource.change.actions[_] == "create"
provided := {tag | resource.change.after.tags[tag]}
missing := required_tags - provided
count(missing) > 0
msg := sprintf("Resource '%s' is missing required tags: %v", [resource.address, missing])
}This policy blocks any run that would create a resource without the full required tag set, regardless of which module or team authored the configuration.
Best practices for Terraform tags
Defining a tagging strategy at the start of your project deployment is definitely a good idea to avoid any rework.
- Consistency — Ensure that the same key names are consistently used across different resources for better organization. It’s important to find the right balance when determining the level of detail for your tags. While too many tags can be overwhelming, too few can make it difficult to effectively organize and retrieve information.
- Define a required tag set — Agree on a mandatory baseline that every resource must carry. A common minimum is
Environment,Owner,Project,CostCenter, andManagedBy. Resources missing these tags create billing blind spots and make access control harder to reason about. - Enforcement — Convention alone doesn’t scale. Use policy-as-code to enforce required tags automatically before resources are created. On Spacelift, you can write an OPA plan policy that rejects any run where a resource is missing required tags. Tools like checkov can enforce the same rules locally or in CI.
- Documentation — Document your tagging convention and valid values in a
TAGS.mdfile in your repository, or encode them directly in variable descriptions and validation blocks as shown above. Documentation that lives in the code is harder to miss than a wiki page. - Automation — Use Terraform modules or the provider-level
default_tagsblock (AWS) to apply baseline tags in one place rather than repeating them on every resource. This follows the DRY principle and reduces the chance of a resource being deployed untagged. - Tag drift — Tags modified outside of Terraform (e.g., via the AWS console) won’t be visible until the next
terraform plan. Use drift detection to catch these changes proactively before they cause billing or access control inconsistencies.
Deploying Terraform resources with Spacelift
Terraform is really powerful, but to achieve an end-to-end secure GitOps approach, you need a platform that can orchestrate your Terraform workflows. Spacelift is the infrastructure orchestration platform built for the AI-accelerated software era.
It manages the full lifecycle for both traditional infrastructure as code (IaC) and AI-provisioned infrastructure, giving you access to a powerful CI/CD workflow and unlocking features such as:
- Policies (based on Open Policy Agent) – You can control how many approvals you need for runs, what kind of resources you can create, and what kind of parameters these resources can have, and you can also control the behavior when a pull request is open or merged.
- Multi-IaC workflows – Combine Terraform with Kubernetes, Ansible, and other IaC tools such as OpenTofu, Pulumi, and CloudFormation, create dependencies among them, and share outputs.
- Build self-service infrastructure – You can use Templates and Blueprints to build self-service infrastructure; simply complete a form to provision infrastructure based on Terraform and other supported tools.
- AI-powered provisioning and diagnostics – Spacelift Intelligence adds an AI-powered layer for natural language provisioning, diagnostics, and operational insight across your infrastructure workflows.
- Integrations with any third-party tools – You can integrate with your favorite third-party tools and even build policies for them. For example, see how to integrate security tools in your workflows using Custom Inputs.
Spacelift enables you to create private workers inside your infrastructure, which helps you execute Spacelift-related workflows on your end. Read the documentation for more information on configuring private workers.
You can check it for free by creating a trial account or requesting a demo with one of our engineers.
Key points
Terraform tags represent key-value pairs assigned to resources to improve resource categorization, cost management, and automation. They are widely used in cloud environments for optimizing infrastructure operations. A well-thought-out tagging strategy forms the backbone of successful cloud governance and resource optimization.
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.
Automate Terraform deployments with Spacelift
Automate your infrastructure provisioning, build more complex workflows based on Terraform using policy as code, programmatic configuration, context sharing, drift detection, resource visualization, and many more.
Frequently asked questions
What does ~> mean in Terraform?
In Terraform,
~>is the pessimistic constraint operator used for versioning. It allows updates within the same major or minor version but prevents breaking changes. For example,~> 2.1means any version>= 2.1.0and< 3.0.0. Similarly,~> 2.1.3allows versions< 2.2.0. This helps ensure compatibility without locking to a single patch.How do you ignore certain tags in Terraform?
Add a
lifecycleblock to the resource and list the attributes (or specific tag keys) Terraform should ignore. Theignore_changesmeta-argument tells Terraform to disregard those attributes on updates while still respecting them at creation time.Why did my Terraform tags disappear?
Tags in Terraform often disappear due to one of the following causes:
- A module, resource, or provider update overwrote or ignored tags not explicitly defined in the code.
- Default tags defined at the provider level were removed or not merged correctly.
- Tags were managed outside Terraform, and a
terraform applyreverted them. - Tag keys had dynamic values that evaluated to
nullor empty strings, causing them to be dropped. - A
for_eachormerge()function in your tag logic excluded expected keys during evaluation.
How can I tag all existing Terraform resources?
To tag all existing Terraform-managed resources, update your Terraform configuration to include the desired tags in each resource block or use provider-level default tags if supported (e.g.,
default_tagsin AWS). Then runterraform planandterraform applyto update the tags in place without recreating resources.Keep in mind:
- Any objects Terraform isn’t already managing must be imported (CLI
terraform importor theimport { … }block in Terraform ≥ 1.6) before the new tags can be applied. - Not all resources support tag updates without replacement.
- Use
lifecycle { ignore_changes = [tags] }cautiously if avoiding drift is important. - For bulk updates, modules or automation can help inject standard tags consistently.
- Any objects Terraform isn’t already managing must be imported (CLI
What is etag in Terraform?
etag is a server-supplied fingerprint used for optimistic concurrency control. Terraform includes the etag value on updates. If the remote object’s etag has changed (someone else modified it), the provider raises a conflict instead of overwriting the change. For example, Google IAM resources expose a computed etag, and S3 objects expose their MD5 (or multipart) hash as etag.
Not every resource supports etag. Where absent, the provider handles locking internally or relies on the API’s idempotency.
