Skip to content
ExponaBLOG
TERRAFORM, IAC & CLOUD AUTOMATION · DAY 051 / 100

Tagging strategy with Terraform: lessons from production

Tags for cost, ownership and automation.

Expona Admin7 min read

Why tagging strategy matters in production

We've all been there: a $50,000 AWS bill arrives, and nobody knows which team or project burned through that budget. Or worse, you need to shut down resources from a decommissioned service, but without clear ownership tags, you're manually hunting through the console. A solid tagging strategy with Terraform isn't bureaucracy—it's the backbone of cost visibility, operational ownership, and automated compliance in any production environment.

Tagging strategy with Terraform: lessons from production starts with understanding that tags aren't optional metadata. They're the connective tissue between your infrastructure code and your business operations. Whether you're allocating costs to different departments, automating resource cleanup, or enforcing security policies, a well-designed tagging approach scales with your organization while keeping your Terraform code maintainable.

The case for default_tags in the AWS provider

Manually adding the same set of tags to every single resource creates two problems: maintenance hell and inconsistency. When you need to update a tag value across hundreds of resources, you don't want to edit each one individually. The AWS provider's default_tags block solves this elegantly.

By defining common tags at the provider level, you ensure every resource—EC2 instance, RDS database, S3 bucket, Lambda function—inherits those tags automatically. This is not just convenience; it's consistency enforcement.

hcl
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Environment = var.environment
      Project     = var.project_name
      ManagedBy   = "Terraform"
      CostCenter  = var.cost_center
      CreatedAt   = timestamp()
    }
  }
}

When you set default tags at the provider level, every resource created in that provider configuration automatically receives those tags. Individual resources can still add their own specific tags, which will be merged with the defaults. This means you get both consistency and flexibility. In a team of 20 engineers across multiple teams, this single block prevents tag drift and ensures your cost reports don't have gaps.

Essential tags: environment, owner, service, and cost center

Not all tags are created equal. We've seen tagging schemes that include dozens of tags, most of which nobody actually uses. Focus on the core four that drive real business outcomes.

Environment

Development, staging, production—this tag is non-negotiable. It controls access policies, backup frequencies, and cost allocation. A misplaced production database in a dev environment (or vice versa) has career-ending consequences.

hcl
variable "environment" {
  description = "Deployment environment"
  type        = string
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

Owner

Who do we page when this service breaks at 3 AM? The owner tag should reference either a team name or an email distribution list. Individual names in tags create friction when people move teams. We've learned this the hard way.

hcl
resource "aws_iam_role" "lambda_execution" {
  name = "${var.service_name}-lambda-role"

  tags = {
    Owner = "platform-eng@company.com"
  }
}

Service

What is this infrastructure for? API gateway, database, worker queue, cache layer? This tag lets you group related resources and understand your blast radius when something breaks. It's essential for incident response and cost reporting by service.

Cost center

Which department pays for this? Engineering, Marketing, Finance? This tag drives billing and chargeback models. Without it, you either eat cloud costs or play guessing games with department heads.

Tag Purpose Example value Lifecycle
Environment Operational boundaries and security zones prod, staging, dev Fixed for resource lifetime
Owner Accountability and incident response platform-team@company.com May change when ownership shifts
Service Logical grouping and cost allocation user-api, billing-worker Fixed unless service is renamed
CostCenter Budget tracking and departmental billing engineering, sales Fixed or updated annually

Enforcing tags with policy as code

Tags are only valuable if they're actually applied and kept current. We've seen teams define beautiful tagging standards, then watch them erode as new projects cut corners under deadline pressure. Enforcement requires automation.

Terraform Cloud and AWS Config can validate tags during plan and apply phases, but the most practical approach we've used combines resource validation with OPA (Open Policy Agent) or AWS Config rules.

hcl
resource "aws_config_config_rule" "require_tags" {
  name = "required-tags-enforcement"

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

  input_parameters = jsonencode({
    tag1Key = "Environment"
    tag2Key = "Owner"
    tag3Key = "Service"
    tag4Key = "CostCenter"
  })

  depends_on = [aws_config_configuration_aggregator.organization]
}

This config rule continuously monitors resources across your account. If someone creates an EC2 instance without the required tags (perhaps through the console or SDK), AWS Config flags it as non-compliant. You can then integrate this with Lambda to auto-remediate by tagging or terminating the resource, depending on your policy.

In Terraform itself, you can enforce tags at the module level:

hcl
variable "required_tags" {
  description = "Tags that must be present on all resources"
  type        = set(string)
  default     = ["Environment", "Owner", "Service", "CostCenter"]
}

variable "tags" {
  description = "Resource tags"
  type        = map(string)
}

locals {
  missing_tags = setsubtract(
    var.required_tags,
    keys(var.tags)
  )
}

resource "null_resource" "tag_validation" {
  count = length(local.missing_tags) > 0 ? 1 : 0
  
  provisioner "local-exec" {
    command = "echo 'Missing required tags: ${join(", ", local.missing_tags)}' && exit 1"
  }
}

When developers forget a required tag, Terraform plan fails immediately. They see the message, update their configuration, and learn the habit. After a few weeks, tag discipline becomes routine.

Using tags for cost allocation and reporting

This is where tagging moves from administrative overhead to genuine business value. Cost Explorer and AWS Billing can group spending by any tag, giving you cost reports sliced by environment, team, or service.

We typically set up a Lambda function that runs nightly, pulls cost data from the Cost Explorer API, groups by tag, and dumps results into a Slack channel and a Google Sheet that Finance watches.

hcl
resource "aws_lambda_function" "cost_reporter" {
  filename      = "cost_reporter.zip"
  function_name = "daily-cost-report"
  role          = aws_iam_role.lambda_cost_reporter.arn
  handler       = "index.handler"
  timeout       = 60

  tags = {
    Service     = "cost-reporting"
    Environment = "prod"
    Owner       = "finance-eng@company.com"
  }
}

resource "aws_cloudwatch_event_rule" "daily_cost_report" {
  name                = "daily-cost-report-trigger"
  schedule_expression = "cron(0 8 * * ? *)"
  
  tags = {
    Service = "cost-reporting"
  }
}

resource "aws_cloudwatch_event_target" "cost_reporter_target" {
  rule      = aws_cloudwatch_event_rule.daily_cost_report.name
  target_id = "CostReporterLambda"
  arn       = aws_lambda_function.cost_reporter.arn
}

With clean tags, that Lambda function can break down daily spend like this:

  • By environment: Prod costs $15k/day, staging $3k/day, dev $1.2k/day
  • By team: Platform team infrastructure: $8k/day, Analytics: $6k/day, Mobile backend: $5.2k/day
  • By service: RDS databases: $10k/day (often the biggest surprise), Compute: $7k/day, Storage: $2k/day
  • By cost center: Engineering pays $18k/day, Sales infrastructure $1.2k/day

These numbers mean nothing if your tags are incomplete. But with consistent tagging, Finance can charge back cloud costs to departments accurately. Engineering can identify and optimize expensive services. And you can make data-driven decisions about whether that staging environment that nobody uses should exist at all.

Common mistakes we've seen

Too many tags

We've inherited Terraform codebases with 30+ tags per resource. Most were never used for anything. Pick your core four (Environment, Owner, Service, CostCenter) and add 2–3 more specific to your business. Stop there. Maintenance burden grows exponentially with tag count.

Inconsistent tag values

One engineer tags owner as "john.smith@company.com", another as "john-smith", another as "Platform". Aggregation becomes impossible. Use a strict enum for critical tags. Terraform variables with validation blocks prevent this at plan time.

Tags that change frequently

Avoid tags for temporary state. "Status: in-use" or "LastModified: 2024-01-15" drift constantly and make cost reports unreliable. Use tags for stable attributes: environment, ownership, service identity, cost center.

Forgetting to tag non-compute resources

Developers tag EC2 instances religiously but forget S3 buckets, RDS databases, and Lambda functions. Then the big bills come from somewhere you didn't expect. Use provider-level default_tags and enforcement policies to catch these gaps.

Not removing tags when they're no longer relevant

Once a tag scheme is established, it doesn't evolve automatically. Review tags quarterly. If a tag hasn't been used in cost reports or automation for six months, either start using it or delete it. Tag sprawl kills maintainability.

Key takeaways

  • Use provider-level default_tags to apply consistent tags to every resource without repetition
  • Focus on four essential tags: Environment, Owner, Service, and CostCenter—these drive 90% of real-world value
  • Enforce tags with AWS Config rules and Terraform validation to catch gaps during planning, not after resources are live
  • Wire tags into cost reporting with Lambda and Cost Explorer to turn infrastructure telemetry into business insights
  • Keep your tag schema simple, use strict enums for critical tags, and review quarterly to prevent drift
  • Tag immutable attributes (environment, service name) and avoid tags for mutable state (status, timestamps)
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles