2026-06-12
    9 min read

    Enforcing AWS Tagging Standards with Terraform Tag Policies

    Hardik Shah

    Hardik Shah

    Cloud Architect & AWS Expert

    Terraform
    AWS
    Tagging
    Tag Policy
    AWS Organizations
    DevOps
    FinOps
    Governance
    Infrastructure as Code
    Cost Management
    Compliance
    SCP
    Enforcing AWS Tagging Standards with Terraform Tag Policies

    If you have ever sat through a cloud cost review where half the resources have no owner, no environment label, and zero context — you already know the pain of untagged infrastructure. AWS Tag Policies give you a way to define tagging standards at the organization level, and the Terraform AWS provider now has a built-in property that can fail your plan before a single non-compliant resource makes it to AWS.

    The Problem with Voluntary Tagging

    Most teams start with good intentions — a tagging guide in the internal wiki, a reminder in the PR template. But nothing technically stops someone from shipping an EC2 instance with no tags or with env=prod when your standard is Environment=production. After a few months you have a mix of inconsistent tags, unattributable costs, and compliance dashboards that are nearly impossible to trust.

    What you actually need is enforcement at the code level — something that makesterraform plan refuse to proceed if the tags don't match the standard. That's exactly what tag_policy_compliance does, and it's available in the Terraform AWS provider starting from v6.22.0.

    What Are AWS Tag Policies?

    AWS Tag Policies live inside AWS Organizations. They let you define the rules for how resources across every account in your org should be tagged — which keys are required, what values are acceptable, and which resource types the rules apply to.

    A few things worth clarifying upfront because the docs can be a bit dense on this:

    • Tag Policies enforce case sensitivity. If your standard says the key is Environment, then environment or ENVIRONMENT is non-compliant. This catches the most common drift.
    • Policies inherit down the hierarchy. Attach a policy at the Org root and every account gets it. Attach it to an OU and only that OU's accounts are affected.
    • The policy itself is just a definition. Without enforcement wired up, it reports non-compliance but doesn't block anything. The Terraform provider'stag_policy_compliance property is what closes that loop.

    Step 1 — Enable Tag Policies in Your Org

    Before you can create any tag policies, the feature needs to be enabled in your AWS Organization. This is a one-time setup:

    hcl
    resource "aws_organizations_organization" "main" {
      feature_set = "ALL"
    
      enabled_policy_types = [
        "TAG_POLICY"
      ]
    }

    The feature_set = "ALL" is required. Without it, none of the policy features work. If your organization already exists, you can import it and just addTAG_POLICY to the enabled_policy_types list.

    Step 2 — Define the Tag Policy

    The policy content is a JSON document that uses a specific syntax with@@assign operators. The @@assign tells AWS to set a value directly (as opposed to inheriting or merging from a parent policy).

    Here's an example that requires three tags — Environment, Owner, and Project — on EC2 instances, S3 buckets, and RDS databases:

    hcl
    resource "aws_organizations_policy" "mandatory_tags" {
      name        = "mandatory-resource-tags"
      description = "Requires Environment, Owner, and Project on core resources"
      type        = "TAG_POLICY"
    
      content = jsonencode({
        tags = {
          Environment = {
            tag_key = {
              "@@assign" = "Environment"
            }
            tag_value = {
              "@@assign" = ["production", "staging", "development", "sandbox"]
            }
            enforced_for = {
              "@@assign" = [
                "ec2:instance",
                "s3:bucket",
                "rds:db"
              ]
            }
          }
    
          Owner = {
            tag_key = {
              "@@assign" = "Owner"
            }
            enforced_for = {
              "@@assign" = [
                "ec2:instance",
                "s3:bucket",
                "rds:db",
                "lambda:function"
              ]
            }
          }
    
          Project = {
            tag_key = {
              "@@assign" = "Project"
            }
            enforced_for = {
              "@@assign" = [
                "ec2:instance",
                "s3:bucket",
                "rds:db",
                "lambda:function"
              ]
            }
          }
        }
      })
    }

    What each block does:

    • tag_key — Sets the exact key name including casing. This is what enforces Environment vs environment.
    • tag_value — Optional. When present, only the listed values are accepted. Leave this out if you want to allow any value for that key.
    • enforced_for — Scopes the rule to specific resource types. This is important — without it, compliance is reported but the Terraform provider's shift-left check won't flag violations.

    Step 3 — Attach the Policy

    A tag policy only takes effect once it's attached to a target in your org hierarchy — the root, an OU, or a specific account:

    hcl
    # Attach to the org root — every account in the org gets this policy
    data "aws_organizations_organization" "current" {}
    
    resource "aws_organizations_policy_attachment" "root" {
      policy_id = aws_organizations_policy.mandatory_tags.id
      target_id = data.aws_organizations_organization.current.roots[0].id
    }
    
    # Or attach to a specific OU if you want narrower scope
    resource "aws_organizations_policy_attachment" "production_ou" {
      policy_id = aws_organizations_policy.mandatory_tags.id
      target_id = "ou-xxxx-yyyyyyy"
    }
    Attach to a non-production OU first. The policy propagates instantly across all child accounts, so it's worth validating the impact in a sandbox before going org-wide.

    Step 4 — Wire Up tag_policy_compliance in the Provider

    This is the core of what makes everything click. As of Terraform AWS provider v6.22.0, there's a new tag_policy_compliance argument in the provider block. When set, Terraform evaluates your resource tags against the effective tag policy during terraform plan — before anything touches AWS.

    It accepts two values:

    ValueBehaviorWhen to Use
    warningPlan succeeds but prints a diagnostic for every non-compliant resourceRoll-out phase — surface violations without breaking existing pipelines
    errorPlan fails immediately if any resource violates the tag policyEnforcement phase — hard block for all IaC workflows
    hcl
    terraform {
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = ">= 6.22.0"
        }
      }
    }
    
    provider "aws" {
      region = "us-east-1"
    
      # "warning" surfaces violations without breaking the plan
      # "error" fails the plan if any resource is non-compliant
      tag_policy_compliance = "error"
    }

    When tag_policy_compliance = "error" is set and a resource is missing a required tag — say an EC2 instance without an Owner tag — the plan output will look something like this:

    bash
    │ Error: Tag Policy Compliance
    │   with aws_instance.app_server,
    │   on main.tf line 12, in resource "aws_instance" "app_server":
    12: resource "aws_instance" "app_server" {
    │ Resource is missing required tag "Owner" as defined in the
    │ effective tag policy for this account.
    

    No AWS API call was made. The resource never existed. The developer sees the exact tag that's missing and fixes it in the PR before it ever gets reviewed or merged.

    How the IAM Permission Works

    For the provider to check tag compliance during plan, the IAM principal running Terraform needs the organizations:ListRequiredTags permission. This is how the provider fetches the effective tag policy for the account. Make sure your pipeline role or your local AWS profile includes this:

    json
    {
      "Effect": "Allow",
      "Action": [
        "organizations:ListRequiredTags"
      ],
      "Resource": "*"
    }

    Recommended Rollout Strategy

    Jumping straight to error mode on an existing codebase is going to break a lot of pipelines at once. This is the sequence that works well:

    1. Create the tag policy and attach it to a sandbox OU. Let the compliance report run for a few days in the AWS console. Understand what's actually non-compliant before you plug in any enforcement.
    2. Set tag_policy_compliance = "warning" across all repos. This surfaces violations in CI pipeline logs without failing any runs. Share the report with your teams and give them a sprint to fix their missing tags.
    3. Flip to error once the backlog is clean. At this point, every new resource that goes through Terraform will be validated against the policy. Tag drift is blocked at the source.
    4. Attach the policy to the org root. Once you've validated behavior at the OU level, expand the coverage to all accounts.

    Things to Watch Out For

    • Provider version matters. The tag_policy_compliance argument doesn't exist below v6.22.0. If you don't pin your provider version, you might not get this behavior in all environments. Always use version = ">= 6.22.0".
    • The enforced_for field is required for the shift-left check. If your tag policy doesn't have an enforced_for block for a resource type, the Terraform provider won't flag that resource type during plan, even in error mode.
    • Case sensitivity is the most common gotcha. If the policy saysEnvironment and your Terraform has environment, it will fail. Document the exact casing in your team's standards and be consistent.
    • Default tags in the provider don't automatically satisfy policy. If you use default_tags in the provider block, those tags are evaluated too — but make sure the values match the allowed list in the policy.

    Wrapping Up

    The combination of AWS Tag Policies and the tag_policy_compliance provider property is a genuinely clean solution to a problem that most cloud teams deal with for years before getting serious about fixing. You define the standard once in your org, you wire up one property in your provider block, and from that point forward Terraform is your first line of enforcement — not an audit report two weeks after the fact.

    Start in warning mode, clean up the existing violations, then flip toerror. The migration is low risk and the payoff — clean cost attribution, trustworthy compliance reports, and consistent resource metadata across every account — is well worth the effort.

    Hardik Shah

    About Hardik Shah

    Hardik is a dedicated Cloud Architect specializing in AWS solutions and DevOps automation. With years of industry experience, he focuses on building scalable, resilient architectures and sharing technical insights to help teams optimize their cloud-native journeys.