Skip to main content

Managing AWS Tags

·1999 words·10 mins
Aws Terraform Terratags

Tagging AWS resources is more or less a requirement when you go from managing a few resources to managing multiple AWS accounts.

With no clear tagging strategy you will end up with a pile of resources with no clear ownership and you will have no idea how to split the AWS bill internally.

Even if you do not utilize internal billing, you will still benefit from a structured approach to tagging to understand resource ownership, criticality, data protection needs, and also to base automation decisions around your resources.

In this blog post I will cover how you can manage resource tagging for AWS with Terraform, as well as how you can enforce and monitor your resource tagging compliance.

Define your tagging strategy
#

The first step to successful AWS resource tagging is to decide which tags you want to use.

There is no right or wrong answer here, it all depends on your context and your needs. You should decide on what you intend to use tags for, and then specify required tags from this.

A few example of tags that you commonly see across many organizations are (you might use other names for the same purposes than what is shown here):

  • Name (the Name tag is special on AWS, it is usually used to display a friendly name in the UI)
  • Owner (e.g. platform-team)
  • CostCenter
  • Environment (e.g. dev, test, uat, prod)
  • DataClassification (e.g. sensitive, public)
  • Application (e.g. www-frontend, consul-server, etc)

In general, try to keep the number of required tags at a reasonable level. If you have 20 required tags then this puts more burden on your teams to comply with.

In the following discussion in this blog post I will limit the number of required tags to only two: Owner and CostCenter.

Managing AWS tags in a Terraform configuration
#

In a Terraform configuration you can add tags to resource types that support it. One example is an EC2 instance:

resource "aws_instance" "web" {
  # other arguments omitted ...

  tags = {
    Name       = "ec2-web-instance"
    Owner      = "web-team"
    CostCenter = "1234"
  }
}

This approach does not scale well when you have hundreds of resources that you must tag. For this, you can configure the provider instance with a set of default tags that will be added to each resource that supports tags:

provider "aws" {
  default_tags = {
    tags = {
      Owner      = "web-team"
      CostCenter = "1234"
    }
  }
}

This doesn’t work for all tags, for instance the Name tag. (Unless you want each resource to have the same name of course.) With default tags in place the previous EC2 instance can be configured as follows:

resource "aws_instance" "web" {
  # other arguments omitted ...

  tags = {
    Name = "ec2-web-instance"
  }
}

Enforcing tags on your AWS resources
#

How can you make sure certain tags are used? There are in fact many options to consider here. I will just mention a few of these in this section and in the following sections.

If you use AWS organizations you could enforce part of your tagging strategy using service-control policies (SCPs) for your organizational units. An example of an SCP:

data "aws_iam_policy_document" "required_tags_for_ec2" {
  statement {
    sid       = "DenyCreateInstanceWithNoOwnerTag"
    effect    = "Deny"
    actions   = ["ec2:RunInstances"]
    resources = ["arn:aws:ec2:*:*:instance/*"]
    
    condition {
      test     = "Null"
      variable = "aws:RequestTag/Owner"
      values   = [
        "true"
      ]
    }
  }
}

resource "aws_organizations_policy" "required_tags_for_ec2" {
  name    = "require-tags-for-ec2-instance"
  content = data.aws_iam_policy_document.required_tags_for_ec2.json
}

resource "aws_organizations_policy_attachment" "workloads" {
  policy_id = aws_organizations_policy.required_tags_for_ec2.id
  target_id = aws_organizations_organizational_unit.my_org_unit.id
}

Another option is to use the managed config rule REQUIRED_TAGS with AWS Config. Make sure to set the evaluation mode to Proactive to block resources from being created that do not include the mandatory tags. You can configure the Config rule like this:

resource "aws_config_config_rule" "tagging" {
  name        = "mandatory-tagging"
  description = "Enforce mandatory tags on resources"

  evaluation_mode {
    mode = "Proactive"
  }

  input_parameters = jsonencode({
    tag1Key = "CostCenter"
    tag2Key = "Owner"
  })

  scope {
    compliance_resource_types = [
      "AWS::EC2::Instance",
      # ... other resource types
    ]
  }

  source {
    owner             = "AWS"
    source_identifier = "REQUIRED_TAGS"
  }
}

AWS has other services for managing tags, specifically AWS resource groups and tag manager. I will not cover these options in this blog post.

Sentinel policy-as-code
#

If your CI/CD system (e.g. HCP Terraform) allows you can introduce a policy-as-code framework and enforce tags this way. This requires that you have a workflow for Terraform in place where you can enforce that certain steps are followed (in this case that policies are run before Terraform runs an apply).

I have previously written a deep-dive on HashiCorp Sentinel for policy as code, so I will not repeat everything here.

Deep-Dive: Hashicorp Sentinel with Terraform
·6585 words·31 mins
Hashicorp Sentinel Policy Terraform Github

You could put in place policies that require tags to exist. An example of a Sentinel policy for enforcing tags on AWS resources is this:

import "tfplan"
import "strings"
import "types"

# Find all resources of a specific type from all modules using the tfplan import
find_resources_from_plan = func(type) {
  resources = {}

  for tfplan.module_paths as path {
    for tfplan.module(path).resources[type] else {} as name, instances {
      for instances as index, r {

        if length(path) == 0 {
          address = type + "." + name + "[" + string(index) + "]"
        } else {
          address = "module." + strings.join(path, ".module.") + "." +
                    type + "." + name + "[" + string(index) + "]"
        }

        resources[address] = r
      }
    }
  }

  return resources
}

validate_attribute_contains_list = func(type, attribute, required_values) {
  validated = true
  resource_instances = find_resources_from_plan(type)

  for resource_instances as address, r {
    if r.destroy and not r.requires_new {
      print("Skipping resource", address, "that is being destroyed.")
      continue
    }

    if (r.diff[attribute + ".%"].computed else false) or
       (r.diff[attribute + ".#"].computed else false) {
      print("Resource", address, "has attribute", attribute, "that is computed.")
    } else {
      if r.applied[attribute] else null is not null and
         (types.type_of(r.applied[attribute]) is "list" or
          types.type_of(r.applied[attribute]) is "map") {

        for required_values as rv {
          if r.applied[attribute] not contains rv {
            print("Resource", address, "has attribute", attribute,
                  "that is missing required value", rv, "from the list:",
                  required_values)
            validated = false
          }
        }

      } else {
        print("Resource", address, "is missing attribute", attribute, "or it is not a list or a map")
        validated = false
      }
    }
  }
  return validated
}

mandatory_tags = [
  "Owner",
  "CostCenter",
]

tags_validated = validate_attribute_contains_list("aws_instance",
                 "tags", mandatory_tags)

main = rule {
  tags_validated
}

This policy is adapted from the terraform-guides repository on GitHub. I have updated the mandatory tags to match the running example in this blog post.

My suggestion is to do what I just did here, you find a sample policy to start from and you adapt it to fit your context. Sentinel is not a trivial language, it takes some getting used to.

In the deep-dive post on Sentinel I mentioned before you can find instructions for how you can run Sentinel in GitHub Actions to enforce policies for your Terraform runs.

The beauty of using Sentinel (or another third-party policy-as-code framework, e.g. OPA) is that you can use the same tool for managing tags across multiple providers and not just AWS.

Terratags
#

There is a new tool for validating required tags compliance called Terratags. You can find installation instructions and documentation over at the Terratags GitHub repository:

terratags/terratags

Required tags validation on terraform resources

Go
26
2

Terratags currently supports the AWS provider, so this is perfect for what we are discussing in this blog post.

Start by creating a configuration file for Terratags. You can use either JSON or YAML. Since YAML is less verbose, this is usually what I prefer. Add the required tags to the configuration file:

# config.yaml
required_tags:
  - Owner
  - CostCenter

You can add exceptions for specific resources or even for all resources of a specific type to avoid requiring the tags on them. For simplicity, we assume we want to enforce our tags on all resources so we skip adding any exceptions.

With the configuration file in place you can run Terratags:

$ terratags -config config.yaml -dir /path/to/terraform/configuration

For a concrete example, I have the following Terraform configuration:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "6.0.0-beta2"
    }
  }
}

provider "aws" {
  # left blank ...
}

resource "aws_s3_bucket" "backup" {
  bucket_prefix = "backup-bucket"

  tags = {
    Owner = "platform-team"
  }
}

resource "aws_instance" "web" {
  ami           = "ami-0fc32db49bc3bfbb1"
  instance_type = "t3.small"

  tags = {
    Owner = "platform-team"
  }
}

When I run Terratags on this configuration (with the config file from above) I get the following result:

$ terratags -config config.yaml
INFO	2025-05-28T16:07:30.250+0200
Tag validation issues found:
INFO	2025-05-28T16:07:30.250+0200	Resource aws_s3_bucket 'backup' is missing required tags: CostCenter
INFO	2025-05-28T16:07:30.250+0200	Resource aws_instance 'web' is missing required tags: CostCenter
INFO	2025-05-28T16:07:30.250+0200
Summary: 0/2 resources compliant (0.0%)
INFO	2025-05-28T16:07:30.250+0200
Tag validation failed. Please fix the issues above.
Note that if you don’t specify a path with the -dir flag it will use the current working directory by default.

In this case it is clear I have forgotten to add the CostCenter tag. I can fix that by introducing default tags in my provider configuration (also moving the Owner tags to this section):

provider "aws" {
  default_tags {
    tags = {
      Owner      = "platform-team"
      CostCenter = "1234"
    }
  }
}

resource "aws_s3_bucket" "backup" {
  bucket_prefix = "backup-bucket"
}

resource "aws_instance" "web" {
  ami           = "ami-0fc32db49bc3bfbb1"
  instance_type = "t3.small"
}

Rerunning Terratags now gives me a different result:

$ terratags -config config.yaml -dir .
INFO	2025-05-28T16:10:20.043+0200	All resources have the required tags!

This time every required tag was in place!

Note that Terratags supports provider default tags.

How should you enforce tags using Terratags? You will need to introduce Terratags in your CI/CD system (e.g. GitHub Actions) and enforce that it is a part of all Terraform runs. You need to have a workflow in place where you can enforce which steps should be included. For instance, you could provide a certain Terraform template for CI/CD that must be used to be able to deploy to your AWS environment. Note that this is basically the same “issue” as for Sentinel policies discussed earlier.

You could also run Terratags as an after-the-fact tool to get an overview of your current tag compliance. For this Terratags supports outputting an HTML dashboard with your current tagging state:

$ terratags -config config.yaml -report index.html
INFO	2025-05-28T16:14:00.086+0200	Report written to index.html
INFO	2025-05-28T16:14:00.086+0200	All resources have the required tags!

The output looks like this:

Terratags HTML-report output

As mentioned, Terratags currently supports the AWS provider but Azure and GCP are on the roadmap. This is good news for you multi-cloud enthusiasts out there!

Key Takeaways
#

There are many options for managing tags on AWS resources. It is likely that you will use more than one tool on your journey to be tag compliant.

The tagging journey starts with defining your tagging strategy and enumerate the required tags. Once this is in place you can start figuring out how you want to enforce tags on your resources. Why do you want to enforce tag? Well, you might want to be sure of who owns a given resource, who should pay the bill for the resource, what type of data protection should be put in place for a given resource, and more.

This blog post did not dive deep into AWS-specific services for this, except for organization service-control policies and Config rules. Services in this category include resource groups and tag manager.

Policy-as-code is a weapon you can use to enforce tagging. In this blog post we briefly covered how you can do this with HashiCorp Sentinel. The benefit here is that you can use it to enforce tagging for all infrastructure you set up using Terraform. This is also true for other third-party policy-as-code frameworks (e.g. OPA).

Finally, we saw how you can use Terratags either before-the-fact or after-the-fact to produce a report of your Tagging compliance. Terratag currently supports AWS, and it can output a nice report showing the current tag compliance for a Terraform configuration.

Mattias Fjellström
Author
Mattias Fjellström
Cloud architect · Author · HashiCorp Ambassador