
When you run terraform init on a new project, something happens in the background: Terraform contacts registry.terraform.io, locates the providers you declared, and downloads them. Most engineers accept this as background infrastructure and move on. But the registry is doing real work, and understanding how it functions changes how you structure modules, manage providers, and govern IaC as your team scales.
The public registry gives you access to thousands of provider plugins and reusable configuration templates. The private registry (whether hosted by HCP Terraform, env zero, GitLab, or a self-hosted solution) gives your organization control over what gets used and by whom. Knowing the difference, and when each applies, is foundational to running Terraform at anything beyond a solo project.
The decisions you make about the registry early (which modules to trust, how to version what you publish, whether to run your own private one) tend to compound. This covers the practical side of all three.
Last updated: April 2026. Refreshed to cover Terraform 1.15 (release candidate), OpenTofu registry differences, private registry options, and CI/CD automation patterns.
At a glance The Terraform Registry is the official distribution platform for Terraform providers, modules, and policy libraries, hosted at registry.terraform.io. Terraform 1.15 (release candidate as of April 2026) introduces variables in
sourceandversionattributes, the first major change to module sourcing in years. OpenTofu maintains its own separate registry at search.opentofu.org, with broad module compatibility with the Terraform Registry.
What the Terraform Registry contains
The registry isn't a single thing. It's three distinct artifact types that serve different purposes.
Providers are plugins that let Terraform communicate with external APIs: AWS, Azure, Google Cloud, Datadog, GitHub, and hundreds of others. When you declare a required_providers block, Terraform fetches the appropriate binary from the registry automatically on init. Understanding how providers work covers version constraints, provider configurations, and multi-provider setups, all worth mastering before you scale past a single account.
Modules are reusable infrastructure templates. Think of them as the IaC equivalent of libraries: instead of writing raw resource blocks from scratch every time you need an S3 bucket or a VPC, you import a module, pass in variables, and get a fully configured resource stack. The public ecosystem covers most common patterns across AWS, Azure, and GCP. The registry also hosts policy libraries (collections of Sentinel and OPA rules) for teams that want to share governance policies across workspaces alongside their infrastructure code.
The badge system: what it actually signals
Every module and provider in the public registry carries one of three labels: Official, Partner, or Community.
Official modules and providers come from HashiCorp directly: the AWS, Azure, and Google providers all live here. Partner means a technology vendor (Datadog, Snowflake, MongoDB) maintains the artifact against their own product. Community is everything else, and it's where most of the widely-used modules actually live.
The badge tells you who's responsible for maintenance, not quality. A community module maintained by a responsive team with an active commit history can be a safer choice than a partner-badged module that hasn't been updated since 2022. Download counts are a reasonable first signal. The GitHub repository (recent commits, open issues, and whether the maintainer responds) is the real signal.
Using modules from the public registry
The workflow has four steps, and the one people rush most is the second.
Search and evaluate
Start at registry.terraform.io/browse/modules. Filter by provider to narrow results. For any given infrastructure pattern, you'll find multiple competing modules. That's normal and expected.
Before committing to one, check: who published it, when it was last updated, and whether it's tracking the current provider version. A module last updated for the AWS provider 4.x when you're running 5.x isn't going to break immediately, but you'll spend time chasing compatibility issues later. Two minutes of evaluation saves hours of debugging.
Configure and import
Once you've picked a module, the basic source block looks like this:
module "s3_bucket" {
source = "terraform-aws-modules/s3-bucket/aws"
version = "~> 4.0"
bucket = "my-org-data"
# versioning block is the module's abstraction over aws_s3_bucket_versioning
versioning = {
enabled = true
}
server_side_encryption_configuration = {
rule = {
apply_server_side_encryption_by_default = {
sse_algorithm = "AES256"
}
}
}
}
The ~> 4.0 constraint pins to any 4.x release while allowing patch updates. That balance (stability without missing security patches) is usually the right default. Pinning to a specific patch like "4.1.0" gives you more control, but you'll need to manually update it when fixes land. Omitting version entirely means you're one module release away from an unexpected plan diff.
Before running terraform plan, scroll the module's Inputs section on the registry page and confirm every required variable is covered in your block. Missing a required input at plan time is a common first-run error.
Run the workflow
terraform init downloads and installs the module. terraform plan shows the proposed changes. terraform apply provisions the resources.
If you're deploying through env zero, this all happens automatically inside the platform's init phase, so you don't need separate pipeline steps to handle module downloads. The runner takes care of it.
Related reading: How to use Terraform providers. Practical walkthrough of provider configuration, version constraints, and multi-provider patterns.
Publishing your own module
At some point, a pattern appears in your codebase three or four times. The right response is to package it as a module rather than copy-paste it across repositories. If the pattern is general-purpose and doesn't contain proprietary configuration, publishing it to the public registry makes it reusable beyond your organization. For anything internal, a private registry is the better home (more on that below).
To publish to the public Terraform Registry, you need a public GitHub repository named with the convention terraform-<provider>-<module-name>. The registry requires this exact format; it won't accept repositories that don't follow it. So a module for AWS S3 with encryption and versioning would live in a repo named terraform-aws-s3-sse-versioning.
The required files are main.tf, variables.tf, outputs.tf, and a README.md. That README matters more than people assume. It's the first thing a potential consumer reads, and a module without clear documentation on what it does, what inputs it requires, and what outputs it exposes rarely gets adopted. Even internally.
Once the repository exists and the code is ready, go to registry.terraform.io/publish/module, authenticate with GitHub, and select the repo. From that point forward, the registry monitors the repository for releases automatically.
Versioning is driven entirely by git tags in vX.Y.Z format:
git tag v1.0.0
git push origin v1.0.0
Push the tag, publish the GitHub release, and the Terraform Registry picks it up within minutes. No manual upload, no CLI commands. Just a tag. Consumers can then pin to version = "~> 1.0" and receive updates as you release them.
Terraform 1.15: what changed for the registry
Terraform 1.15 (in release candidate as of April 2026) introduces a feature that's been on the community wishlist for a long time: variables are now allowed in source and version attributes of module blocks (release notes).
Until now, both had to be static string literals. You couldn't reference a variable for the module version, which made it awkward for platform teams trying to manage module versions centrally. With 1.15:
variable "s3_module_version" {
type = string
default = "~> 4.0"
}
module "s3_bucket" {
source = "terraform-aws-modules/s3-bucket/aws"
version = var.s3_module_version
bucket = "my-org-data"
# ...
}
The version constraint now lives in a tfvars file or environment variable, not hardcoded in every module block. When you want to update pinned versions across a large codebase, you change one value instead of hunting through dozens of files.
The same release adds deprecation markers for variables and outputs, useful for signaling to module consumers that an input is going away before you remove it. Worth testing in non-production environments before adopting in critical workflows, since 1.15 is still in RC.
Your private registry options
The public registry works well for open-source modules. The moment you have proprietary configuration, internal compliance requirements, or organization-specific conventions baked into your modules, you need a private registry: somewhere to host modules that shouldn't be public, with access control over who can consume them.
The comparison that matters most isn't "which option exists" but which one fits the infrastructure you're willing to operate and the governance controls you actually need:
| Option | Setup complexity | RBAC | OpenTofu support | Module CI testing | Cost |
|---|---|---|---|---|---|
| HCP Terraform | Low | Role-based | Limited | Basic | Free tier + paid |
| env zero | Low (SaaS) | Fine-grained | Native | Native (tofu test) |
Paid |
| GitLab registry | Medium | Group-based | Partial | Via CI pipelines | Paid tiers |
| Self-hosted (Terrareg/citizen) | High | Custom | Yes | DIY | Infrastructure only |
A few practical notes from this table. HCP Terraform's private registry is solid if you're already using it for remote state and runs; the marginal setup cost is low. GitLab's built-in registry is a reasonable choice if GitLab is already your VCS and you'd rather not introduce another SaaS. Self-hosted options like Terrareg or citizen give you complete control, but you're also taking on the operational responsibility of running them.
Two things stand out in env zero's row: native OpenTofu support across the same module registry, and built-in CI testing that gates releases before modules reach consuming teams. Both matter more as the number of teams sharing the registry grows.
Related reading: Terraform Cloud alternatives: 2026 in-depth guide. Covers the full landscape of IaC platforms, private registry capabilities, pricing, and migration paths.
OpenTofu and the registry
OpenTofu, the Linux Foundation-backed open-source fork of Terraform, maintains its own registry at search.opentofu.org. It contains all the providers and modules from the Terraform Registry plus OpenTofu-specific contributions, and it operates under a different license model than HashiCorp's registry.
For most teams writing modules for internal consumption through a private registry, the difference between the two registries rarely surfaces. The HCL syntax is effectively identical, and most modules work with both runtimes without modification. Where you hit edge cases is at provider version boundaries, not in the module code itself.
The public registry divergence has a legal dimension worth noting. In 2023, HashiCorp updated the Terraform Registry's terms of service to restrict community redistribution of its contents. OpenTofu's registry exists partly in response to that change, with a more permissive model for community reuse. For enterprise teams, this matters more as a compliance and vendor dependency question than a technical one.
If your team is running Terraform and OpenTofu workspaces side by side (which is increasingly common during evaluation periods), env zero handles both natively through the same module registry. You don't maintain parallel registries or reconfigure pipelines per runtime.
Related reading: OpenTofu registry guide: tips, examples, and ways to contribute. Detailed walkthrough of the OpenTofu registry, how it differs from the Terraform Registry, and how to contribute modules.
CI/CD integration for module publishing
Publishing a module manually once is fine. By the third or fourth version, the manual process is a friction point. By the tenth, it's a liability.
The standard pattern is to automate git tagging on merge to your main branch. Here's a GitHub Actions workflow that reads conventional commits and handles version bumping automatically:
# .github/workflows/release.yml
name: Release module
on:
push:
branches:
- main
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: hashicorp/setup-terraform@v3
- name: Validate configuration
run: |
terraform init -backend=false
terraform fmt -check -recursive
terraform validate
- name: Run tests
# terraform test creates real infrastructure; add provider credentials as repo secrets
# Terraform 1.15 introduces mock providers if you want credential-free CI
run: terraform test
- name: Bump version and push tag
uses: mathieudutour/github-tag-action@v6.2 # github.com/mathieudutour/github-tag-action
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
default_bump: patch
A commit prefixed with feat: bumps the minor version; fix: bumps the patch. The tag gets pushed to GitHub, and the Terraform Registry or your private registry picks it up automatically.
For module quality assurance, add terraform validate and terraform test (or tofu test) to the workflow before the release step. This is where env zero's CI testing feature plugs in directly: when a new module version is published to env zero's private registry, the platform can run your *.tftest.hcl test files against real infrastructure before the version becomes available to consuming teams. A broken module that fails tests never reaches production.
Related reading: Celebrating OpenTofu GA with our new CI testing feature. How env zero integrates OpenTofu's native testing framework into module lifecycle management.
Troubleshooting common registry errors
Most registry failures fall into a small number of categories. The error message usually points to the right category; the fix depends on which one you're in.
Module not found
The most common cause isn't actually a missing module: it's a version constraint with no matching release. If you write version = "5.0.0" for a module that's only published 4.x releases, Terraform returns a lookup failure that can look like the module doesn't exist.
Before debugging further, check the version constraints against the module's releases page on registry.terraform.io. If the constraint is the problem, you'll see it immediately. Running terraform providers lock -platform=linux_amd64 can also surface constraint conflicts before they hit your CI pipeline.
Authentication failures on private registries
When Terraform can't authenticate to a private registry, the error usually reads "Failed to install provider" or "no credentials found for registry." The fix depends on where your registry lives:
# For HCP Terraform and Terraform Cloud
terraform login
# For other private registries: add credentials to ~/.terraformrc
credentials "registry.example.com" {
token = "your-api-token"
}
For env zero's registry specifically, authentication is managed at the platform level. The runner already has what it needs, so you don't configure .terraformrc per workspace.
Provider version conflicts
terraform init failing with "incompatible provider versions" usually means two modules in the same configuration are requesting provider version ranges that don't overlap. The clearest fix is to declare the provider constraint explicitly in your root module:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
This overrides implied constraints from child modules. If the conflict persists after that, run terraform providers to see exactly which module is requesting which range. It usually points to one older module that hasn't been updated for the current provider.
Stale lock file after a module update
If you update a module version but the plan shows no changes, the .terraform.lock.hcl may be caching the previous selection. Delete it and re-initialize:
rm .terraform.lock.hcl
terraform init -upgrade
The -upgrade flag tells Terraform to re-resolve constraints and download updated versions rather than using the existing lock file.
Registry governance at scale
Individual contributors can manage module versioning manually. A platform team supporting 50 engineers cannot. The problems that emerge at scale are predictable before they happen: teams write slightly different versions of the same module in parallel; naming conventions drift across namespaces; modules get pinned to old versions and never updated; nobody knows which modules are officially supported and which are abandoned experiments from two years ago.
Naming conventions and namespace ownership
In both the public and private registry, namespaces map to organizations or teams. Define ownership before you scale: ambiguity about who owns a namespace is one of the most reliable sources of module sprawl. The convention terraform-<provider>-<name> works for the public registry; for private registries, most platforms use <hostname>/<namespace>/<name>/<provider>. Pick a pattern, document it, and enforce it in your publishing workflow.
Documentation standards matter as much as naming. A module without a README describing its inputs, outputs, and a working example won't get adopted correctly. Enforce this in CI: a terraform-docs step that fails the pipeline when documentation is missing costs almost nothing to add and saves significant time explaining the module to every new consumer.
Access control and publishing governance
Separating who can publish from who can consume is the single most impactful governance control in a private registry. Unrestricted publish access leads to unofficial module versions proliferating alongside the supported ones, and consumers end up picking the wrong one.
env zero's module registry uses fine-grained RBAC to enforce this separation: module maintainers get publish access, everyone else gets read access. Combined with Terraform workspaces organized by team, this gives platform teams visibility into which modules are actually being used before they consider deprecating something.
When you're ready to deprecate a module version, use the deprecation markers introduced in Terraform 1.15 to signal it in plan output before removing the version entirely. Removing a version without warning breaks consumers silently; a deprecation marker gives them time to update.
For organizations that need policy-level enforcement (preventing consumption of modules that don't meet compliance requirements), pairing registry controls with policy-as-code using OPA gives you both the access controls and the enforcement layer.
env zero module and provider registry
env zero offers two registry types: a module registry and a provider registry. Both are private, version-controlled, and connected to your VCS.
The module registry integrates with GitHub, GitLab, and Bitbucket. Connect a repository, define the module name and provider, and env zero tracks versioning from that point forward based on your git tags. Consumers reference the module the same way they'd reference any Terraform source:
module "s3_bucket" {
source = "api.env0.com/<org>/s3-bucket/aws"
version = "~> 1.0"
bucket = "my-org-data"
# variables...
}
What makes the env zero registry different from a plain hosted registry is the CI testing integration. When a new module version is published, env zero can automatically run your *.tftest.hcl test files against real infrastructure before the version becomes available to consuming teams. Tests that fail block the release. That gate is the difference between a broken module reaching twenty teams simultaneously and catching the regression before it ships.
The provider registry solves a different problem. When your organization builds internal Terraform providers (for proprietary APIs, internal tooling, or compliance wrappers around cloud resources), the env zero provider registry gives you the same version management and RBAC controls you apply to modules. Authentication is handled at the platform level, so individual workspaces don't carry provider credentials separately.
Related reading: Module registry and Terraform provider updates. env zero's registry feature overview with setup walkthrough.
Both registries work with Terraform and OpenTofu workspaces in the same organization. If you're running a mixed Terraform and OpenTofu environment during an evaluation or gradual migration, you don't maintain separate registries for each runtime.
Try it with env zero
Managing modules and providers across a growing team (versioning, CI testing, access control, multi-runtime support) accumulates overhead quietly until it becomes a real engineering cost.
env zero's private registry handles the distribution and governance layer, so your team focuses on writing good modules rather than maintaining the infrastructure that hosts them.
Start a free trial or book a demo.
References
- Terraform Registry: official public registry for providers, modules, and policies
- HashiCorp Terraform Registry documentation: publishing guides, API reference, and registry concepts
- Terraform 1.15 release notes: variables in source/version attributes, deprecation markers, output type constraints
- OpenTofu registry: OpenTofu's provider and module distribution platform
- Spacelift: Terraform Registry guide: comprehensive coverage of public and private registry patterns, including provider publishing
- Spacelift: Terraform private registry: private registry options comparison with API protocol details
- Terrareg: open-source self-hosted Terraform module registry
- citizen: lightweight open-source private module registry
FAQ
What is the Terraform Registry?
The Terraform Registry is the official distribution platform for Terraform providers, modules, and policy libraries, hosted at registry.terraform.io and maintained by HashiCorp. Providers let Terraform communicate with external APIs; modules are reusable infrastructure templates; policy libraries contain shared governance rules for Sentinel and OPA. All three artifact types are available on the public registry at no cost.
What's the difference between a Terraform provider and a module?
A provider is a plugin that gives Terraform access to an external API: AWS, Azure, Google Cloud, Datadog, and so on. When you declare a required_providers block and run terraform init, Terraform downloads the provider binary from the registry. A module, by contrast, is a collection of Terraform resource configurations packaged together to provision a specific pattern, like a VPC or an encrypted S3 bucket. Providers are required to manage any resources; modules are optional but reduce configuration duplication significantly.
Does the Terraform Registry work with OpenTofu?
OpenTofu maintains its own registry at search.opentofu.org, which includes providers and modules from the Terraform Registry along with OpenTofu-specific contributions. Most modules written for Terraform run on OpenTofu without modification, since both runtimes use the same HCL syntax. Edge cases tend to appear at provider version boundaries rather than in module code.
Can I use variables in module source or version attributes?
As of Terraform 1.15 (release candidate, April 2026), yes. Previously, both attributes required static string literals. With 1.15, you can pass a variable as the version constraint, useful for platform teams managing module versions centrally via tfvars or environment variables rather than editing individual module blocks across many configurations.
How do I choose between a managed and a self-hosted private registry?
Managed registries (HCP Terraform, env zero) have lower setup overhead (you're up in hours rather than days) and include built-in RBAC, versioning, and in some cases module CI testing. Self-hosted options (Terrareg, citizen) give you complete control over data residency and operation, but you own the infrastructure and maintenance. For most teams, managed is the right starting point; self-hosted makes sense when compliance requirements prohibit third-party SaaS or when you need customization the managed options don't support.
How does access control work in env zero's module registry?
env zero uses fine-grained RBAC to separate module publishers from consumers. Maintainers get publish access to specific namespaces; everyone else gets read access. This prevents ad-hoc publishing from creating a parallel set of unofficial module versions alongside the officially maintained ones, a problem that compounds quickly as team size grows.


.webp)

![Using Open Policy Agent (OPA) with Terraform: Tutorial and Examples [2026]](https://cdn.prod.website-files.com/63eb9bf7fa9e2724829607c1/69d6a3bde2ffe415812d9782_post_th.png)