Introduction
Infrastructure naming might feel like a minor detail when you're spinning up resources in Terraform, but poor naming conventions become the source of genuine pain once you have dozens of environments, multiple teams, and a monthly cloud bill that needs reconciliation. Naming conventions for infrastructure in Terraform & IaC matter because they directly affect how quickly you can locate a resource in the AWS console, understand who owns it, debug a billing issue, and prevent accidental modifications or deletions across environments.
We've watched teams waste hours tracking down which database was causing a production outage because resource names didn't indicate environment or component type. We've also seen infrastructure drift occur silently because rename operations unintentionally triggered resource replacement, destroying data. This article distills what we've learned about naming conventions that work across small teams and large organizations.
The foundational pattern: {project}-{env}-{component}
The most reliable naming pattern we've seen in production uses three segments separated by hyphens: project, environment, and component. This pattern scales from a single environment to multi-region, multi-team deployments without needing rewrites.
Why this structure works: It's human-readable when you're scanning the AWS console or Kubernetes manifests. It's also unambiguous—anyone joining the team can infer what a resource does and where it runs. When you receive a bill with resource identifiers, the pattern immediately tells you which project to charge and which environment burned the budget.
Here's a realistic example for an e-commerce platform:
resource "aws_rds_instance" "primary" {
identifier = "ecommerce-prod-maindb"
engine = "postgres"
instance_class = "db.r6i.2xlarge"
allocated_storage = 500
publicly_accessible = false
skip_final_snapshot = false
final_snapshot_identifier = "ecommerce-prod-maindb-final-${formatdate("YYYY-MM-DD-hhmm", timestamp())}"
tags = {
Name = "ecommerce-prod-maindb"
Project = "ecommerce"
Environment = "prod"
Component = "maindb"
}
}
resource "aws_rds_instance" "replica" {
identifier = "ecommerce-prod-maindb-replica"
replicate_source_db = aws_rds_instance.primary.identifier
tags = {
Name = "ecommerce-prod-maindb-replica"
Project = "ecommerce"
Environment = "prod"
Component = "maindb"
}
}
resource "aws_elasticache_cluster" "sessions" {
cluster_id = "ecommerce-prod-sessions"
engine = "redis"
node_type = "cache.r6g.xlarge"
num_cache_nodes = 3
parameter_group_name = "default.redis7"
tags = {
Name = "ecommerce-prod-sessions"
Project = "ecommerce"
Environment = "prod"
Component = "sessions"
}
}
resource "aws_s3_bucket" "assets" {
bucket = "ecommerce-prod-assets-${data.aws_caller_identity.current.account_id}"
tags = {
Name = "ecommerce-prod-assets"
Project = "ecommerce"
Environment = "prod"
Component = "assets"
}
}This approach immediately tells you: which platform owns it (ecommerce), which environment it serves (prod), and its role (maindb, sessions, assets). When AWS sends a bill, you see the project code. When debugging a performance issue, the component label tells you to check database metrics, not cache metrics.
For staging and development, the pattern stays identical but environment changes:
resource "aws_rds_instance" "dev" {
identifier = "ecommerce-dev-maindb"
instance_class = "db.t4g.micro"
allocated_storage = 20
# ... other config
}
resource "aws_rds_instance" "staging" {
identifier = "ecommerce-staging-maindb"
instance_class = "db.t4g.small"
allocated_storage = 100
# ... other config
}Consistent snake_case in Terraform
While your AWS resource names use hyphens (the {project}-{env}-{component} pattern), your Terraform resource and variable names should consistently use snake_case. This distinction matters because hyphens aren't valid in HCL identifiers, and mixing conventions across your codebase creates confusion.
The rule: AWS resource identifiers use hyphens. Terraform locals, variables, resource declarations, and outputs use snake_case. Keep them separate.
Here's the right way:
variable "project_name" {
type = string
default = "ecommerce"
}
variable "environment_name" {
type = string
default = "prod"
}
locals {
base_name = "${var.project_name}-${var.environment_name}"
common_tags = {
Project = var.project_name
Environment = var.environment_name
ManagedBy = "Terraform"
CreatedAt = timestamp()
}
}
resource "aws_security_group" "api_server" {
name = "${local.base_name}-api-sg"
description = "Security group for API servers in ${var.environment_name}"
vpc_id = aws_vpc.main.id
tags = merge(
local.common_tags,
{
Name = "${local.base_name}-api-sg"
Component = "api"
}
)
}
output "api_security_group_id" {
value = aws_security_group.api_server.id
description = "Security group ID for API servers"
}Notice how the Terraform declarations use snake_case (api_server, common_tags, base_name), while the actual AWS resource names in the name attribute use hyphens. This consistency prevents naming errors and makes the codebase easier to navigate for new team members.
When you have multiple modules, extend this pattern:
module "database" {
source = "./modules/rds"
db_identifier = "${local.base_name}-maindb"
db_instance_class = var.db_instance_class
db_allocated_storage = var.db_allocated_storage
common_tags = local.common_tags
}
module "cache" {
source = "./modules/elasticache"
cache_cluster_id = "${local.base_name}-sessions"
cache_node_type = var.cache_node_type
common_tags = local.common_tags
}Avoid names that force replacement when changed
One of the most dangerous pitfalls in infrastructure naming occurs when you change a resource name that AWS treats as immutable. Some resources—like RDS database identifiers, S3 bucket names, and IAM roles—cannot be renamed in place. Terraform will destroy the old resource and create a new one, potentially causing data loss or downtime.
Resources that force replacement on name changes: RDS identifier, S3 bucket name, IAM role name, Kinesis stream name, DynamoDB table name, ElastiCache cluster ID, Lambda function name, API Gateway REST API name.
The safest approach is to make the name immutable by construction. Use a naming scheme that doesn't need to change, and rely on tags for metadata that might evolve:
// BAD: Using instance class in the name
resource "aws_rds_instance" "main" {
identifier = "${local.base_name}-maindb-t4g" // DANGEROUS: Changing this forces replacement
instance_class = "db.t4g.large"
# ...
}
// GOOD: Keep the name stable, use tags for instance class
resource "aws_rds_instance" "main" {
identifier = "${local.base_name}-maindb"
instance_class = "db.t4g.large"
tags = merge(
local.common_tags,
{
Name = "${local.base_name}-maindb"
InstanceClass = "db.t4g.large"
Component = "maindb"
}
)
}The first approach bakes the instance class into the name. If you later scale to db.r6i.xlarge, Terraform sees the identifier change and destroys the database, then creates a new one—losing all data. The second approach keeps the identifier stable and tracks the instance class in tags instead.
For resources with truly immutable names, create them once with careful naming, then document the decision in your module's README or comments:
// This S3 bucket name is immutable. Do not change it without recreating the bucket.
// Changing the name forces bucket destruction and recreation, losing all objects.
resource "aws_s3_bucket" "application_logs" {
bucket = "${local.base_name}-logs-${data.aws_caller_identity.current.account_id}"
lifecycle {
prevent_destroy = true
}
tags = {
Name = "${local.base_name}-logs"
Purpose = "Application access logs"
BackupStrategy = "Versioning enabled, 90-day retention"
}
}The lifecycle { prevent_destroy = true } block stops Terraform from accidentally destroying this bucket, giving you time to reconsider before making breaking changes.
Tags carry extra metadata
Names should be stable and concise, but infrastructure requires rich metadata: cost center, owner, compliance classification, backup policy, deployment timestamp, and more. Tags are where this metadata lives, and they should complement—not replace—your naming convention.
We recommend a minimal set of tags on every resource:
- Name: Human-readable resource identifier (matches or derives from the resource ID)
- Project: Billing and ownership tracking
- Environment: prod, staging, dev
- Component: Functional role (database, cache, api, worker, etc.)
- ManagedBy: Terraform, CloudFormation, manual, etc.
- CostCenter: For billing allocation across departments
- Owner: Team or person responsible for escalations
- CreatedAt: Timestamp for lifecycle and deprecation policies
- Version: For tracking configuration versions
Here's a practical tagging strategy using Terraform locals:
variable "cost_center" {
type = string
description = "Cost center code for billing allocation"
}
variable "owner_email" {
type = string
description = "Email of the team responsible for this infrastructure"
}
variable "backup_policy" {
type = string
default = "daily"
description = "Backup frequency: daily, weekly, none"
}
locals {
base_tags = {
Project = var.project_name
Environment = var.environment_name
ManagedBy = "Terraform"
CreatedAt = timestamp()
CostCenter = var.cost_center
Owner = var.owner_email
BackupPolicy = var.backup_policy
TerraformDir = path.root
}
}
resource "aws_rds_instance" "main" {
identifier = "${local.base_name}-maindb"
instance_class = "db.r6i.2xlarge"
allocated_storage = 500
backup_retention_period = 30
tags = merge(
local.base_tags,
{
Name = "${local.base_name}-maindb"
Component = "maindb"
Tier = "critical"
}
)
}
resource "aws_s3_bucket" "app_backups" {
bucket = "${local.base_name}-backups-${data.aws_caller_identity.current.account_id}"
tags = merge(
local.base_tags,
{
Name = "${local.base_name}-backups"
Component = "storage"
Tier = "archive"
}
)
}
resource "aws_iam_role" "lambda_execution" {
name = "${local.base_name}-lambda-execution"
tags = merge(
local.base_tags,
{
Name = "${local.base_name}-lambda-execution"
Component = "iam"
}
)
}With this approach, you can query AWS with tags to find all production databases, all resources owned by a specific team, or everything created by Terraform. You can also write Lambda functions or CloudWatch rules that use tags to automate compliance checks, apply backup policies, or generate cost reports by cost center.
Common mistakes
1. Embedding version numbers in names: Avoid ecommerce-prod-api-v2 because future versions force you to choose between updating the name (and risking replacement) or keeping outdated version numbers. Use tags instead: ecommerce-prod-api with a Version = "v2" tag.
2. Using uppercase or mixed case: AWS allows uppercase in some resource names, but it's inconsistent across services. Stick to lowercase and hyphens for cloud resources. It also makes shell scripts and grep commands more reliable.
3. Overly long names: Some AWS services limit name length (IAM roles: 64 characters, S3 buckets: 63 characters). Keep base names concise: proj-env-component leaves room for suffixes like -replica, -backup, or account IDs.
4. Hardcoding environment-specific names in modules: Don't create a module that assumes prod. Pass environment as a variable so the same module works for dev, staging, and production.
5. Ignoring AWS service naming rules: S3 bucket names are globally unique and must be lowercase with no underscores. RDS identifiers are per-account but case-insensitive. Check the Terraform AWS provider docs before settling on a naming scheme.
6. Forgetting to tag: Untagged resources become impossible to filter, audit, or delete when they're no longer needed. Make tagging mandatory by using required_providers and validation blocks, or enforce it with AWS Config rules.
Key takeaways
- Use the pattern
{project}-{env}-{component}for all infrastructure resource names. It's human-readable, searchable in consoles and bills, and scales across teams. - Write Terraform resource declarations in snake_case while AWS resource identifiers use hyphens. Keep this distinction clear to avoid HCL syntax errors and confusion.
- Never embed mutable information (instance class, version numbers, feature flags) in immutable resource names. Use tags instead, and test naming changes in dev environments before applying to production.
- Define a minimal set of required tags—Project, Environment, Component, Owner, CostCenter, ManagedBy—and apply them consistently using Terraform locals. These tags unlock automation, billing, and audit capabilities that names alone cannot provide.
- Document your naming and tagging conventions in a README at the root of your Terraform code. Consistency across teams prevents the drift and confusion that slows down deployments and incident response.






Comments
Loading comments…