· Evan O'Connor · Terraform  · 7 min read

Why I Built a Terraform Naming Conventions Module (And Open-Sourced It)

Every AWS service has its own naming rules. I got tired of discovering them mid-deploy, so I built a module that handles names, tags, truncation, and global uniqueness in one place.

Every AWS service has its own naming rules. I got tired of discovering them mid-deploy, so I built a module that handles names, tags, truncation, and global uniqueness in one place.

Variable naming is hard enough. Naming AWS resources can be even scarier.

With all the limits and constraints AWS puts on resource names, it’s hard to keep straight what makes each resource name valid or not. S3 buckets need to be globally unique and lowercase. ALB names cap out at 32 characters. IAM roles give you 64. And you don’t find out you got it wrong until terraform apply fails halfway through.

When I started building Infralista, I noticed early on that anywhere I was hardcoding names as strings in Terraform, I would regret it later. A quick "my-app-dev-logs" here, a "myapp-prod-static" there, and suddenly I had dozens of unique strings scattered across modules that all needed to stay in sync. One rename meant hunting through the entire codebase hoping I didn’t miss one.

I wanted to make sure that no unique name appeared more than once in the code. That way, big refactors down the line would be less scary. I wouldn’t have to search through every module looking for which strings to update when I hit a collision or needed to restructure.

That’s what led me to build a naming conventions module that lets my entire system speak a common language.

The Replication Problem

Once the common language was in place, I hit another headache when I started replicating environments. Mid-way through a terraform apply, I found out that a resource I was replicating needed to be globally unique, like an S3 bucket. Two environments in the same account with the same naming pattern? Collision. Two accounts using the same base name? Collision.

So I added a hash. It’s an 8-character SHA1 derived from the base name plus the AWS account ID. The same inputs always produce the same hash, so names are deterministic (not random), but unique across accounts. No more surprises when spinning up a new environment.

The Truncation Trap

But then I hit another edge case. Some resource names started getting truncated because they exceeded the max character limit for that service. And the part getting cut off? The unique hash suffix. Suddenly my “unique” names weren’t unique anymore.

I had to adjust the naming logic to prioritize keeping the hash and suffix when truncating. The base name gets shortened, but uniqueness is never sacrificed for length. You shouldn’t have to think about whether your project name is too long for an ALB. The module handles it.

Tags: The Other Half of the Problem

Since this module already had all the metadata and context about each resource (business unit, project, app, environment, owner), it only made sense to standardize resource tagging from the same source.

This becomes really powerful when you need to dig into AWS Cost Explorer. With consistent tags across all modules and environments, you can drill down to the most specific level of your app to see exactly where costs are coming from. Without standard tags, there’s no way to be certain you won’t miss a key piece when trying to optimize your AWS bill.

And if you ever get a call late at night that a resource is erroring out or eating up spend, tags help you match that AWS resource with exactly where in the code it was created and who the owner or point of contact is. You know who to call instead of trying to piece it together yourself at 3am.

How It Works

The module builds names from hierarchical components:

{business_unit}-{project}-{app}-{env}-{suffix}

For globally unique resources, it appends the hash:

acme-platform-api-dev-a1b2c3d4-logs

It comes with a built-in catalog of common AWS resource types, so you don’t need to pass any strings for the basics. Just reference module.naming.names.s3_logs or module.naming.names.ecs_cluster. To add custom resources or multiple copies of a resource, you pass in custom definitions at module creation time:

module "naming" {
  source        = "github.com/infralista/terraform-aws-naming-conventions"
  business_unit = "acme"
  project       = "platform"
  app           = "api"
  env           = "dev"
  region        = "us-east-1"

  custom = {
    grafana_alb = { suffix = "grafana-alb", max = 32 }
    api_bucket  = { suffix = "api-data", max = 63, lower = true, global = true }
  }
}

With this module, I never have to worry about remembering whether I called something "myapp-prod" or "my_prod_app". There’s one source of truth, and every module reads from it.

Single Account vs. Multi-Account

The module also handles something that trips people up as they grow: whether to include the environment and region in resource names.

If you’re running all your environments in a single AWS account (which is common for startups keeping things simple and cost-effective), you need dev, stg, and prod in the name to avoid collisions. The module includes the environment by default:

acme-platform-api-dev-cluster
acme-platform-api-prod-cluster

But if you move to a multi-account strategy, where each environment lives in its own AWS account for better isolation and security, the environment is already implicit from the account. Including it in the name is redundant. You can set include_env = false:

module "naming" {
  # ...
  include_env = false  # env is implicit from the account
}

Same goes for include_region if you expand to multi-region. The module lets you balance cost and complexity with security and isolation as your infrastructure grows, without having to refactor all your names.

Cross-Module References

Knowing that all of my 30+ shared modules use this central naming conventions module, I could technically access resources from other modules just by using the naming category key. But the approach I actually take is to use a versioned SSM runtime configuration that stores metadata about each environment: names, versions, whether they’re active or sleeping. This keeps things cleaner for backwards compatibility when modules need to reference each other. (That’s a topic for a future post.)

Start Early

We all know that ticket to standardize naming patterns ends up sitting in the backlog year after year, getting pushed behind higher-visibility items. Meanwhile, the risk grows with the codebase. The longer you wait, the more names you have to untangle when you finally get around to it.

If you’re a startup, take this advice: set it up early. Standardize all the naming metadata you need before the codebase grows past the point where renaming is painful. That way, as your code grows, whether you’re mixing languages, frameworks, or AWS accounts, you know that everything speaks a common language. And when a call goes off at 3am, you know exactly where and from whom to get the info to solve the issue and go back to bed.

The Module

I open-sourced this module under Apache 2.0 because naming conventions shouldn’t be proprietary. It’s one of the foundational tools behind Infralista, my DevOps platform for startups.

GitHub: infralista/terraform-aws-naming-conventions


AWS Resource Name Limits Reference

A quick reference for the naming constraints across common AWS services. This is what the module handles for you.

ResourceMax LengthLowercase RequiredGlobally UniqueNotes
S3 Bucket63YesYesMust be DNS-compliant, no underscores
ALB / NLB32NoNo (per account/region)Alphanumeric and hyphens only
Target Group32NoNo (per account/region)Alphanumeric and hyphens only
ECS Cluster255NoNo (per account/region)
ECS Service255NoNo (per cluster)
IAM Role64NoYes (per account)Path + name combined
IAM Policy128NoYes (per account)Path + name combined
Lambda Function64NoNo (per account/region)
DynamoDB Table255NoNo (per account/region)
RDS Cluster63YesNo (per account/region)Alphanumeric and hyphens
RDS Instance63YesNo (per account/region)
CloudFront DistributionN/AN/AN/AAWS assigns the ID
Route 53 Hosted Zone255NoYes (DNS)Must be valid domain name
VPC255NoNoTag-based naming (Name tag)
Subnet255NoNoTag-based naming
Security Group255NoNo (per VPC)
ECR Repository256YesNo (per account/region)Can include / for namespacing
SNS Topic256NoNo (per account/region)
SQS Queue80NoNo (per account/region)
CloudWatch Log Group512NoNo (per account/region)Can include / for hierarchy
CloudWatch Alarm255NoNo (per account/region)
SSM Parameter1011NoNo (per account/region)Full path including / prefix
EFS File System255NoNoCreation token for idempotency
Secrets Manager512NoNo (per account/region)Can include / for hierarchy
Step Functions80NoNo (per account/region)
EventBridge Rule64NoNo (per account/region)
API Gateway (REST)1024NoNoStage name max 128
Kinesis Stream128NoNo (per account/region)
ElastiCache Cluster40YesNo (per account/region)Alphanumeric and hyphens
OpenSearch Domain28YesNo (per account/region)Must start with lowercase letter
Cognito User Pool128NoNo (per account/region)
WAF Web ACL128NoNo (per account/region)
CodeBuild Project255NoNo (per account/region)
CodePipeline100NoNo (per account/region)
Back to Blog

Related Posts

View All Posts »