The Terraform Registry & Module Versioning

Don’t Build What Already Exists

I’ve watched engineers spend two weeks writing a custom VPC module. Handcrafting subnet logic, NAT gateway failover, route table associations. Beautiful code. Then I showed them terraform-aws-modules/vpc/aws — a module that’s been downloaded millions of times and handles edge cases they hadn’t even thought of yet.

The registry exists so you don’t reinvent the wheel. Use it.

The Terraform Registry

The public registry lives at registry.terraform.io. Two things live there:

  • Providers — the plugins that talk to cloud APIs
  • Modules — reusable infrastructure packages

Modules follow a naming convention: <namespace>/<module>/<provider>

hashicorp/consul/aws        — HashiCorp's Consul cluster module for AWS
terraform-aws-modules/vpc/aws   — Community VPC module for AWS
terraform-aws-modules/eks/aws   — Community EKS module for AWS
Using a Registry Module
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b", "us-east-1c"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24"]

  enable_nat_gateway = true
  single_nat_gateway = true

  tags = {
    Environment = "prod"
  }
}

Run terraform init and Terraform pulls the module into .terraform/modules/. That’s it.

The source format changes depending on where the module lives:

  • Registry: "<namespace>/<module>/<provider>"
  • Local path: "./path/to/module"
  • Git: "git::https://github.com/org/repo.git//modules/vpc?ref=v1.2.0"
Module Versioning

Pin your registry module versions. I cannot stress this enough. Registry modules release breaking changes, and if you don’t pin, you’re at the mercy of whoever published the latest version.

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.1.2"    # Exact pin
}

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.0"  # 20.x.x — allows patches and minor versions
}

Same constraint operators as providers: ~>, >=, =, etc.

Here’s the thing that bites people: module versions are NOT recorded in .terraform.lock.hcl. The lock file only tracks providers. Module versions are determined entirely by the version argument in your config. There’s no lock file safety net for modules — if you forget to pin, running terraform init a month from now might pull a completely different module version. I’ve seen this cause production outages.

Module Sources
Source TypeExample source value
Local path"./modules/vpc"
Terraform Registry"hashicorp/consul/aws"
GitHub"git::https://github.com/org/repo.git//modules/vpc"
GitHub with ref"git::https://github.com/org/repo.git//modules/vpc?ref=v1.2.0"
Bitbucket"bitbucket.org/org/repo"
S3 bucket"s3::https://s3.amazonaws.com/bucket/module.zip"
GCS bucket"gcs::https://storage.googleapis.com/bucket/module.zip"

That // in Git URLs isn’t a typo. It’s how Terraform separates the repo root from the subdirectory within it.

HCP Private Registry

For modules your company writes internally and doesn’t want to share with the world, HCP Terraform includes a private module registry. Same experience as the public registry — upload modules, version them, control access via team permissions.

module "vpc" {
  source  = "app.terraform.io/my-company/vpc/aws"
  version = "~> 2.0"
}

Teams browse an internal catalog of approved modules. Nobody writes a VPC from scratch. This is how you get consistency across an org — not by writing documentation nobody reads, but by giving people modules that enforce your standards by design.

Module Composition

Modules calling other modules. This is where things get powerful.

# modules/three-tier-app/main.tf

module "networking" {
  source = "../networking"
  ...
}

module "compute" {
  source    = "../compute"
  vpc_id    = module.networking.vpc_id
  subnet_ids = module.networking.private_subnet_ids
}

module "database" {
  source     = "../database"
  vpc_id     = module.networking.vpc_id
  subnet_ids = module.networking.database_subnet_ids
}

The caller just invokes three-tier-app and gets the full stack. Clean, composable, and each piece is independently testable.

Keep individual modules focused. A VPC module does VPC things. A compute module does compute things. Composition makes them powerful together. The moment you build a module that does networking AND compute AND databases, you’ve got a monolith — hard to maintain, impossible to reuse. I made this mistake early in my career. Don’t repeat it.

Inspecting Registry Modules Before Using Them

Before you drop a registry module into production, spend five minutes doing your homework:

  1. Check the inputs and outputs on registry.terraform.io — are they well-documented?
  2. Check the version history — is it actively maintained, or has it been abandoned?
  3. Read the README — understand what infrastructure it actually creates
  4. Check the source on GitHub — look at the actual resources being provisioned

Here’s why this matters: the terraform-aws-modules/vpc/aws module creates the VPC, subnets, route tables, internet gateway, NAT gateways, NACLs, and more. That’s a LOT of resources. If you didn’t read the docs and assumed it was “just a VPC,” you’d be surprised by your AWS bill when it spins up three NAT gateways at $32/month each.

Key Takeaways

Module versions are not in the lock file. If you skip the version argument, terraform init grabs whatever’s latest. Run it today, get v5.1. Run it next month, get v6.0 with breaking changes. Always pin.

Changing a module’s source after it’s already in your state is dangerous. Terraform sees a new source as a completely new module — it’ll destroy all the old resources and create new ones. If you need to change the source (like moving from a local path to a registry reference), use a moved block to tell Terraform it’s the same thing at a different address. Skip this step and you’re looking at a full destroy-recreate cycle on production resources.

The terraform-aws-modules GitHub organization is gold for AWS. For GCP, look at terraform-google-modules. For Azure, Azure/terraform-azurerm-*. Start there. Only write custom modules when the community ones don’t fit or when they’re more opinionated than you can live with.