Terraform’s private module registry is a powerful tool for sharing internal Terraform modules, but it’s not just a place to dump code; it’s a versioned API for your infrastructure-as-code.

Let’s see it in action. Imagine you have a common pattern for deploying a PostgreSQL database in your organization. You’ve built a Terraform module for it, and you want to share it across multiple teams without them copying and pasting the code.

Here’s what a module definition might look like in modules/rds/main.tf:

resource "aws_db_instance" "this" {
  allocated_storage    = var.allocated_storage
  engine               = "postgres"
  engine_version       = var.engine_version
  instance_class       = var.instance_class
  identifier           = var.identifier
  username             = var.username
  password             = var.password
  parameter_group_name = "default.postgres13"
  skip_final_snapshot  = true

  tags = {
    Name        = var.identifier
    Environment = var.environment
  }
}

variable "allocated_storage" {
  description = "The allocated storage in gibibytes."
  type        = number
  default     = 20
}

variable "engine_version" {
  description = "The database engine version."
  type        = string
  default     = "13.7"
}

variable "instance_class" {
  description = "The instance class."
  type        = string
  default     = "db.t3.micro"
}

variable "identifier" {
  description = "The RDS instance identifier."
  type        = string
}

variable "username" {
  description = "The master username for the database."
  type        = string
}

variable "password" {
  description = "The master password for the database."
  type        = string
  sensitive   = true
}

variable "environment" {
  description = "The deployment environment (e.g., dev, staging, prod)."
  type        = string
}

output "rds_instance_address" {
  description = "The address of the RDS instance."
  value       = aws_db_instance.this.address
}

To make this available internally, you’d push it to a private registry. This could be Terraform Cloud, a self-hosted registry, or even a Git repository (though a dedicated registry is recommended for true versioning). For Terraform Cloud, you’d typically create a "Module" in your organization.

Then, in another Terraform project where you want to use this module, you’d declare it in your main.tf like this:

terraform {
  cloud {
    organization = "my-org"
    workspaces {
      name = "app-prod"
    }
  }
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}

module "rds_postgres" {
  source = "app.terraform.io/my-org/rds/aws"
  version = "1.2.0" # Pinning to a specific version is crucial

  identifier     = "my-prod-db"
  username       = "admin"
  password       = "supersecretpassword" # In a real scenario, use a secret manager
  environment    = "prod"
  allocated_storage = 50
  instance_class  = "db.t3.medium"
}

output "db_host" {
  value = module.rds_postgres.rds_instance_address
}

The source attribute points to your private registry. app.terraform.io/my-org/rds/aws is the canonical address for a module named rds in the aws provider namespace, belonging to the my-org organization on Terraform Cloud. The version attribute is critical for reproducibility.

This system solves the problem of scattered, unmanaged Terraform code. Instead of each team writing and maintaining their own versions of common infrastructure components, you centralize this logic into versioned modules. This promotes consistency, reduces duplication, and allows for faster iteration on shared infrastructure patterns. When you need to update a database configuration (e.g., add a new security parameter), you update the module, version it, and then teams can opt-in to the new version by updating their version constraint.

Internally, when Terraform encounters a module source like app.terraform.io/my-org/rds/aws, it queries the Terraform Registry API (or the equivalent for your private registry) to fetch the specified version of the module. It then downloads the module’s code and uses it as a "sub-configuration" within the calling configuration. Variables are passed down, and outputs are surfaced. The registry acts as a discoverable, versioned API for your internal Terraform modules.

The most subtle yet powerful aspect of the private module registry is its role in enforcing standards and best practices. By creating and publishing modules, you can bake in security controls, compliance requirements, and preferred configurations directly into the reusable code. Teams consuming these modules automatically inherit these standards, significantly reducing the risk of misconfigurations and compliance drift without requiring manual oversight on every deployment.

The next logical step is to explore how to automatically test these modules before publishing them to the registry, ensuring quality and stability.

Want structured learning?

Take the full Terraform course →