The most surprising thing about publishing Terraform modules to the registry is that the registry doesn’t actually store your module code; it just indexes and serves metadata about versions you’ve already pushed to a compatible source.
Let’s see this in action. Imagine we have a simple S3 bucket module:
# modules/s3-bucket/main.tf
resource "aws_s3_bucket" "this" {
bucket = var.bucket_name
tags = var.tags
}
variable "bucket_name" {
description = "The name of the S3 bucket."
type = string
}
variable "tags" {
description = "A map of tags to assign to the bucket."
type = map(string)
default = {}
}
output "bucket_id" {
description = "The ID of the S3 bucket."
value = aws_s3_bucket.this.id
}
To publish this, we first need to structure it correctly and push it to a version control system (VCS) that the registry supports, like GitHub. The registry expects a specific directory layout and tag-based versioning.
Here’s the typical structure for a module intended for the registry:
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
└── versions.tf # Crucial for specifying Terraform version requirements
The versions.tf file is important for compatibility. It tells Terraform which versions of the provider and Terraform itself the module is designed for.
# modules/s3-bucket/versions.tf
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
Once your module is in a VCS repository, you’ll create Git tags that follow a specific semantic versioning scheme (e.g., v1.0.0, v1.0.1, v1.1.0). The Terraform Registry uses these tags to identify distinct module versions.
The process on the Terraform Registry website involves:
- Registering your VCS provider: You’ll link your GitHub, GitLab, or Bitbucket account.
- Publishing a new module: You select the repository containing your module. The registry will then scan for valid Git tags.
- Selecting a tag: You choose the tag you want to publish as a module version.
Once published, your module will appear in the registry, accessible via its namespace, name, and version. For our S3 bucket example, it might look something like your-github-username/s3-bucket/aws.
When another user wants to use your module, they declare it in their Terraform configuration:
# main.tf of a consumer
module "my_public_bucket" {
source = "your-github-username/s3-bucket/aws"
version = "1.0.0" # Pinning to a specific version
bucket_name = "my-unique-app-bucket-12345"
tags = {
Environment = "Production"
ManagedBy = "Terraform"
}
}
Terraform then queries the registry for the metadata associated with your-github-username/s3-bucket/aws version 1.0.0. The registry provides the source location (your Git repository and the specific tag) and the module’s inputs and outputs. Terraform then clones the specified Git tag’s content and executes it. The registry itself is essentially a sophisticated index and discovery service, not a module storage service.
The key levers you control are the module’s code, its versions.tf constraints, and the Git tags you apply. The registry automates the discovery and version lookup based on these.
A common point of confusion is how versioning works with the registry. When you push a new tag like v1.0.1 to your repository, you don’t re-publish the module from scratch on the registry website. Instead, you go to the module’s page on the registry and click "Publish new versions." The registry will scan your repository again, detect the new tag, and make it available. This confirms that the registry is aware of your VCS, not storing the code itself.
The next concept you’ll likely encounter is module composition and dependency management across multiple published modules.