Naming & Tagging Conventions
Consistent naming and tagging are the cheapest governance you can buy. When every resource follows a predictable name and carries a known set of tags, engineers can find things instantly, finance can attribute spend, and security can scope policies without guesswork. Terraform makes this enforceable: a handful of locals and a single default_tags block can drive thousands of resources. This page shows how to design a naming scheme, generate names programmatically, and apply mandatory tags so that consistency is the path of least resistance rather than a code-review afterthought.
Why conventions matter
Cloud accounts decay fast without rules. A resource named db-final-2 tells you nothing about its environment, owner, or purpose, and an untagged EC2 instance is invisible to cost reports. Conventions turn raw infrastructure into a searchable, auditable inventory. The two pillars are names (the human- and API-facing identifier) and tags (key/value metadata used for filtering, billing, and automation). Names should be deterministic and unique; tags should be uniform and complete.
A naming pattern
A widely used scheme is <env>-<app>-<component>[-<suffix>]. It reads left-to-right from broadest to most specific scope, sorts predictably in consoles, and stays within the character limits of most AWS resources.
| Segment | Example | Purpose |
|---|---|---|
env | prod, stg, dev | Deployment environment |
app | payments, auth | Owning application or service |
component | api, db, queue | Resource role |
suffix | 01, use1 | Disambiguator (region, index) |
Define the building blocks once in locals and compose a name_prefix so individual resources only append their component.
locals {
env = "prod"
app = "payments"
region = "us-east-1"
region_short = { "us-east-1" = "use1", "eu-west-1" = "euw1" }
name_prefix = "${local.env}-${local.app}"
}
resource "aws_ecs_cluster" "this" {
name = "${local.name_prefix}-api"
}
resource "aws_db_instance" "primary" {
identifier = "${local.name_prefix}-db"
engine = "postgres"
engine_version = "16.3"
instance_class = "db.r6g.large"
allocated_storage = 100
username = "app"
manage_master_user_password = true
skip_final_snapshot = false
final_snapshot_identifier = "${local.name_prefix}-db-final"
}
Some resources reject hyphens or impose tight length limits. S3 bucket names are globally unique and DNS-constrained, while IAM roles cap at 64 characters. Validate names with
can(regex(...))in a variable validation block rather than discovering the limit at apply time.
Tagging strategy
The provider-level default_tags block applies a baseline set of tags to every taggable resource it manages, so you never tag individual resources by hand for the common keys. Resource-level tags are merged on top and win on conflict.
provider "aws" {
region = local.region
default_tags {
tags = {
Environment = local.env
Application = local.app
ManagedBy = "terraform"
Owner = "[email protected]"
CostCenter = "CC-4471"
Repository = "github.com/acme/payments-infra"
}
}
}
Now any resource you declare inherits those six tags automatically:
resource "aws_sqs_queue" "events" {
name = "${local.name_prefix}-events"
tags = {
Component = "queue"
}
}
Output:
# aws_sqs_queue.events will be created
+ resource "aws_sqs_queue" "events" {
+ name = "prod-payments-events"
+ tags = {
+ "Component" = "queue"
}
+ tags_all = {
+ "Application" = "payments"
+ "Component" = "queue"
+ "CostCenter" = "CC-4471"
+ "Environment" = "prod"
+ "ManagedBy" = "terraform"
+ "Owner" = "[email protected]"
+ "Repository" = "github.com/acme/payments-infra"
}
}
Plan: 1 to add, 0 to change, 0 to destroy.
The merged result surfaces in the read-only tags_all attribute, which is what AWS actually receives. OpenTofu implements default_tags identically, so this pattern is portable across both binaries.
Enforcing consistency
Conventions only hold if they are checked. Use variable validation to reject malformed inputs, and a policy engine to reject missing tags before apply.
variable "env" {
type = string
validation {
condition = contains(["dev", "stg", "prod"], var.env)
error_message = "env must be one of dev, stg, prod."
}
}
For organization-wide rules, gate terraform plan output with a policy. A minimal OPA/Conftest rule that fails when a mandatory tag is absent:
deny[msg] {
resource := input.resource_changes[_]
required := {"Environment", "Owner", "CostCenter"}
missing := required - {k | resource.change.after.tags_all[k]}
count(missing) > 0
msg := sprintf("%s missing tags: %v", [resource.address, missing])
}
Run it in CI against a saved plan:
terraform plan -out=tfplan
terraform show -json tfplan > plan.json
conftest test plan.json --policy policy/
This keeps drift out of the codebase: a pull request that forgets CostCenter fails the pipeline rather than silently shipping an unbillable resource.
Best practices
- Centralize naming logic in
locals(aname_prefix) so a single edit re-bases every resource consistently. - Use
default_tagsfor organization-wide tags and reserve resource-leveltagsfor component-specific keys only. - Treat a small set of tags —
Environment,Owner,CostCenter— as mandatory and enforce them with policy-as-code in CI. - Validate env/app inputs with
validationblocks andregexso illegal names fail at plan, not apply. - Account for per-resource constraints (length limits, no hyphens, global uniqueness) instead of assuming one format fits all.
- Keep tag keys in stable PascalCase and document the allowed values; inconsistent casing fragments cost reports.
- Avoid encoding mutable data (like an IP or version) into names — names are effectively immutable once dependents reference them.