Terraform module naming conventions that actually matter
Most naming advice for Terraform modules is bikeshedding. Whether a variable is enable_logging or logging_enabled will not sink your platform. Two naming decisions genuinely will, because the registry protocol and every consumer's source address depend on them, and once modules have consumers you cannot rename anything without breaking them.
Those two are the repo name and the source address. Everything else is preference.
The terraform-PROVIDER-NAME rule
If you want a module in a registry, the repository has to be named terraform-<PROVIDER>-<NAME>. A VPC module for AWS is terraform-aws-vpc. A Postgres module for the Google provider is terraform-google-postgres. This is not a style guide, it is a hard requirement of the public registry's ingestion, and most private registries follow it so the same repo works in both places.
The reason is that the registry derives the module's address from the repo name. terraform-aws-vpc becomes the vpc module under the aws provider. Break the format and the registry either rejects the repo or, worse, indexes it wrong. The PROVIDER slug is the primary provider the module wraps, not necessarily the only one it uses, and NAME can contain hyphens (terraform-aws-vpc-endpoints is fine, it is the vpc-endpoints module).
A thing people miss: this maps repo to module, not repo to source address. The address consumers actually type is a different shape.
Repo name versus source address
The repo is terraform-aws-vpc. Nobody writes that in a module block. What they write is a source address:
module "vpc" {
source = "registry.terramantle.dev/acme/vpc/aws"
version = "~> 3.2"
}
Read that address right to left and it makes sense: the vpc module, aws provider, acme namespace, on the registry.terramantle.dev host. The terraform- prefix and the provider-then-name ordering from the repo are both gone, rearranged into host/namespace/name/provider. The registry does that translation. You never type the repo name and consumers never see it.
The four-part form is the explicit one, host included. There is a three-part shorthand where the host is implied by your CLI config:
source = "acme/vpc/aws" # namespace/name/provider, host from config
Here is the part that trips people up, and it is worth knowing because it produces a genuinely confusing failure. The three-part form is namespace/name/provider. If the first segment looks like a hostname, with a dot or a colon in it, a well-built parser treats the whole thing as a malformed four-part address that forgot its namespace, not as an org called registry.example.com. So source = "registry.example.com/vpc/aws" is not a three-part address with a fancy namespace. It is a four-part address missing a segment, and it should fail rather than silently register a module under an org named after your registry host. If you have ever seen a module land under a namespace that is actually your hostname, this is why: a parser that did not make that distinction. Ours rejects it; not all do.
Namespaces before you have scale
The namespace is the segment most teams pick without thinking and regret at about module thirty. It is the ownership boundary. Two common strategies:
One namespace per team: platform/vpc/aws, data/warehouse/aws, security/baseline/aws. Ownership is obvious from the address, and access control maps to it cleanly. The cost is that a module moving between teams changes its address, which is a breaking change for every consumer.
One namespace for the whole org: acme/vpc/aws, acme/warehouse/aws. Addresses are stable forever because ownership is not encoded in them. The cost is that ownership lives somewhere else, in a CODEOWNERS file or the registry's metadata, and the address alone does not tell you who to ask.
There is no right answer, but there is a wrong time to decide, which is after you have three hundred modules and consumers pinned to addresses you now want to change. Pick the boundary while renaming is still cheap. If you are not sure, a single org namespace is the safer default because a stable address is worth more than self-documenting ownership, and you can always add team metadata later.
Anti-patterns worth naming
terraform-modulesas a single repo holding twenty modules. The registry indexes per repo, so this is one module or none, not twenty. Split it, painful as that is.- Provider in the name segment:
terraform-aws-aws-vpc. The provider is already its own slug, you are repeating it. - Environment in the module name:
terraform-aws-vpc-prod. Environments are variable values, not different modules. Onevpcmodule, aprodand adevinvocation. - Versioning in the repo name:
terraform-aws-vpc-v2. That is what SemVer tags are for. A-v2repo is a confession that you broke the interface and did not want to bump the major, which consumers cannot see and cannot pin against.
Quick reference
| Thing | Format | Example |
|---|---|---|
| Repo name | terraform-PROVIDER-NAME | terraform-aws-vpc |
| Source address (explicit) | host/namespace/name/provider | registry.terramantle.dev/acme/vpc/aws |
| Source address (implied host) | namespace/name/provider | acme/vpc/aws |
| Version constraint | SemVer range | ~> 3.2 |
| Namespace | team or org, decide early | platform or acme |
Get the repo name and the source address right and nothing downstream fights you. The naming that matters is the naming consumers depend on.
Once you have modules named and published, the next question is how to version them so a bump does not break everyone at once. That is module versioning, and the addresses above are what a private registry resolves.
Last reviewed
Terramantle is a private Terraform and OpenTofu registry, in beta. See pricing or read the FAQ.