· 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.

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-logsIt 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-clusterBut 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.
| Resource | Max Length | Lowercase Required | Globally Unique | Notes |
|---|---|---|---|---|
| S3 Bucket | 63 | Yes | Yes | Must be DNS-compliant, no underscores |
| ALB / NLB | 32 | No | No (per account/region) | Alphanumeric and hyphens only |
| Target Group | 32 | No | No (per account/region) | Alphanumeric and hyphens only |
| ECS Cluster | 255 | No | No (per account/region) | |
| ECS Service | 255 | No | No (per cluster) | |
| IAM Role | 64 | No | Yes (per account) | Path + name combined |
| IAM Policy | 128 | No | Yes (per account) | Path + name combined |
| Lambda Function | 64 | No | No (per account/region) | |
| DynamoDB Table | 255 | No | No (per account/region) | |
| RDS Cluster | 63 | Yes | No (per account/region) | Alphanumeric and hyphens |
| RDS Instance | 63 | Yes | No (per account/region) | |
| CloudFront Distribution | N/A | N/A | N/A | AWS assigns the ID |
| Route 53 Hosted Zone | 255 | No | Yes (DNS) | Must be valid domain name |
| VPC | 255 | No | No | Tag-based naming (Name tag) |
| Subnet | 255 | No | No | Tag-based naming |
| Security Group | 255 | No | No (per VPC) | |
| ECR Repository | 256 | Yes | No (per account/region) | Can include / for namespacing |
| SNS Topic | 256 | No | No (per account/region) | |
| SQS Queue | 80 | No | No (per account/region) | |
| CloudWatch Log Group | 512 | No | No (per account/region) | Can include / for hierarchy |
| CloudWatch Alarm | 255 | No | No (per account/region) | |
| SSM Parameter | 1011 | No | No (per account/region) | Full path including / prefix |
| EFS File System | 255 | No | No | Creation token for idempotency |
| Secrets Manager | 512 | No | No (per account/region) | Can include / for hierarchy |
| Step Functions | 80 | No | No (per account/region) | |
| EventBridge Rule | 64 | No | No (per account/region) | |
| API Gateway (REST) | 1024 | No | No | Stage name max 128 |
| Kinesis Stream | 128 | No | No (per account/region) | |
| ElastiCache Cluster | 40 | Yes | No (per account/region) | Alphanumeric and hyphens |
| OpenSearch Domain | 28 | Yes | No (per account/region) | Must start with lowercase letter |
| Cognito User Pool | 128 | No | No (per account/region) | |
| WAF Web ACL | 128 | No | No (per account/region) | |
| CodeBuild Project | 255 | No | No (per account/region) | |
| CodePipeline | 100 | No | No (per account/region) |