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.