Overview and What You Will Learn
The Terraform Registry, at registry.terraform.io, is like npm for infrastructure. Someone has almost certainly already written a well-tested, widely-used module for the thing you are about to build — an EKS cluster, a VPC, an RDS instance — and it has been used and fixed by thousands of other engineers before you ever typed a single line. Writing your own from scratch when a mature community module already exists is usually wasted effort.
By the end of this lab you will be able to:
- Search the Terraform Registry and judge whether a community module is trustworthy
- Use a registry module with the correct source address and version constraint
- Read a module's documentation to find required inputs, optional inputs, and outputs
- Understand private registries in Terraform Cloud for company-internal modules
- Publish your own module to the public registry, following its naming rules
- Choose a sane versioning strategy so you don't break every consumer with one release
Why This Matters in Production
At Meesho, a new platform engineer was assigned to set up an EKS cluster from scratch. Writing the IAM roles, node groups, security groups, and OIDC provider configuration by hand would have taken roughly two weeks and almost certainly missed a few of AWS's more obscure EKS requirements. Instead, the engineer used terraform-aws-modules/eks/aws — a community module already used in production by thousands of companies — and had a working cluster in an afternoon. Six months later, when AWS deprecated a node group API the module was using, the module's maintainers shipped a fix within days. Meesho bumped the version constraint and got the fix for free, without ever touching their own code.
This is the registry's real value: you are not just borrowing code, you are borrowing thousands of other engineers' bug reports.
Core Principles
How Module Source Resolves from Registry to Local Cache
+------------------------------------------------+| Your module block: || module "vpc" { || source = "terraform-aws-modules/vpc/aws" || version = "~> 5.0" || } |+------------------------------------------------+ | terraform init reads source + version | v+------------------------------------------------+| registry.terraform.io resolves the address to || the latest version matching "~> 5.0" || (e.g. it picks 5.8.1) |+------------------------------------------------+ | downloads module source code | v+------------------------------------------------+| Cached locally in: || .terraform/modules/vpc/ || (committed to .gitignore — never check this in) |+------------------------------------------------+Detailed Step-by-Step Practical Lab
Step 1 — Search the Registry and Evaluate Trustworthiness
Before using any community module, check these signals on its registry page:
Things to check on a module's registry.terraform.io page: 1. Verified badge -> published by the provider's official partner team2. Download count -> tens of millions = battle-tested at massive scale3. Last published date -> updated within the last few months = actively maintained4. GitHub stars/issues -> click through to the source repo, check open issue count5. Number of releases -> mature modules have 30+ releases, not version 1.0.0 from 2019INFORMATIONTip from a senior engineer: a module with 50 million downloads and a verified badge, like `terraform-aws-modules/vpc/aws`, has effectively been tested in more production environments than your entire company could test in years. That track record is worth more than reading every line of its source code yourself.
Step 2 — Use a Registry Module with Source and Version
module "vpc" { source = "terraform-aws-modules/vpc/aws" # namespace/name/provider — three parts, no slashes added version = "~> 5.0" # see version constraint syntax below name = "phonepay-prod-vpc" cidr = "10.0.0.0/16" azs = ["ap-south-1a", "ap-south-1b", "ap-south-1c"] private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"] enable_nat_gateway = true single_nat_gateway = true # cost optimization — one NAT gateway shared across AZs}Step 3 — Understand Version Constraint Syntax
version = "5.8.1" # EXACT version only — no flexibility, rarely used in modulesversion = "~> 5.0" # "pessimistic constraint" — allows 5.0.x and 5.1.x, NOT 6.0.0version = "~> 5.8" # allows 5.8.x only (last segment can change, not the second)version = ">= 5.0" # any version 5.0 or above, including future major versions — riskyversion = ">= 5.0, < 6.0" # explicit range — same effect as "~> 5.0" but more readable# After changing a version constraint, you must re-run init for it to take effectterraform init -upgrade# Terraform will print which version it actually selected within your constraintCOMMON MISTAKE / WARNINGCommon mistake: using `>= 5.0` with no upper bound. This silently allows Terraform to pull in version 6.0.0 — which might rename half its variables — the next time someone runs `terraform init -upgrade`. Always cap the major version with `~>` or an explicit `< 6.0`.
Step 4 — Popular AWS Registry Modules Worth Knowing
# VPC — the most-used module on the entire registrymodule "vpc" { source = "terraform-aws-modules/vpc/aws" version = "~> 5.0"} # EKS — handles the node groups, OIDC provider, and IAM roles correctlymodule "eks" { source = "terraform-aws-modules/eks/aws" version = "~> 20.0"} # RDS — wraps aws_db_instance with sane defaults for parameter groups, subnet groupsmodule "rds" { source = "terraform-aws-modules/rds/aws" version = "~> 6.0"} # S3 bucket — adds versioning, encryption, lifecycle rules as simple togglesmodule "s3_bucket" { source = "terraform-aws-modules/s3-bucket/aws" version = "~> 4.0"}Step 5 — Read Module Documentation Properly
Every registry module page has three tabs that matter: Inputs, Outputs, and Resources. Before calling any module, check:
1. Inputs tab -> which variables have NO default (these are REQUIRED)2. Inputs tab -> which variables have a default you should override for production3. Outputs tab -> what values you can chain into your own resources afterward4. Resources tab -> what this module actually creates under the hood — useful when debugging an unexpected AWS resource in your account# Example: reading the EKS module's outputs to wire up kubectl access afterwardoutput "cluster_endpoint" { value = module.eks.cluster_endpoint # straight from the module's documented outputs} output "cluster_certificate_authority_data" { value = module.eks.cluster_certificate_authority_data}Step 6 — Private Module Registries for Internal Modules
Public registry modules are great for generic infrastructure. But your company's specific patterns — like "how we configure logging for every Lambda function" — belong in a private registry, available only inside Terraform Cloud or Terraform Enterprise:
module "lambda_with_logging" { source = "app.terraform.io/zerodha-org/lambda-standard/aws" # ^^^^^^^^^^^^^^^^ ^^^^^^^^^^^ # your TFC hostname your organisation name version = "2.1.0"}Private registries give you the same version constraint syntax, documentation tabs, and dependency resolution as the public registry — just scoped to modules your company publishes internally.
Step 7 — Publish Your Own Module to the Public Registry
The public registry has strict naming and structure rules. Your GitHub repository must be named exactly terraform-<PROVIDER>-<NAME>:
Correct repo name: terraform-aws-ecs-serviceIncorrect repo name: ecs-service-moduleIncorrect repo name: aws-terraform-ecs-service (wrong word order)# Repository requirements before the registry will accept it:# 1. Repo name matches terraform-<PROVIDER>-<NAME> exactly# 2. Repo is public on GitHub# 3. A README.md exists at the root# 4. main.tf, variables.tf, outputs.tf exist at the root (not nested in a subfolder)# 5. At least one Git release tag exists, formatted as a semantic version (vX.Y.Z) git tag v1.0.0git push origin v1.0.0 # Then go to registry.terraform.io, sign in with GitHub,# and click "Publish" — the registry pulls releases automatically from then onINFORMATIONTip: once published, the registry watches your GitHub releases automatically. Every time you push a new `vX.Y.Z` tag, it appears on the registry within minutes — you never manually upload anything.
Step 8 — Choose a Versioning Strategy
Use semantic versioning — MAJOR.MINOR.PATCH — and mean it:
v1.0.0 -> v1.0.1 PATCH : bug fix, no behavior change for any existing callerv1.0.0 -> v1.1.0 MINOR : new optional variable added, fully backward compatiblev1.0.0 -> v2.0.0 MAJOR : a required variable was renamed, removed, or its type changed — anyone on v1.x must change their code to upgrade# Consumers protect themselves from breaking changes with the right constraintversion = "~> 1.5" # gets all 1.x patch and minor updates, but NEVER auto-jumps to v2.0.0Production Best Practices and Common Pitfalls
Read the CHANGELOG before any major version bump. A jump from
~> 4.0to~> 5.0on a popular module liketerraform-aws-modules/vpc/awscan rename several variables. Always read the module's changelog or release notes before widening a version constraint across a major boundary.Pin exact versions in CI, looser ranges during local development. Many teams use
~> 5.0while developing, but lock the resolved version with a committed.terraform.lock.hclfile so CI always uses exactly what was tested — never a newer patch release that happened to publish overnight.Don't fork a community module just to add one variable. Forking means you now own every future security fix and AWS API change yourself. Check if the module supports
nulloverrides or afor_each-based extension pattern first — most well-maintained modules already have a way to extend behavior without forking.Verify before trusting, every time — even for "verified" modules. A verified badge means the publisher's identity is confirmed, not that the code is bug-free. Still read the Resources tab to understand exactly what it creates in your AWS account before running
applyagainst production.
Quick Reference and Troubleshooting Commands
| Constraint Syntax | Meaning |
|---|---|
"5.8.1" |
Exact version only |
"~> 5.8" |
Allows 5.8.x patch updates only |
"~> 5.0" |
Allows any 5.x version, blocks 6.0.0 |
">= 5.0, < 6.0" |
Explicit range, same effect as ~> 5.0 |
| Error | Root Cause | Fix |
|---|---|---|
No available releases match |
Version constraint too narrow, or typo in constraint | Check the registry page for actual available versions |
Module not found in registry |
Wrong namespace/name/provider order in source | Re-check the exact three-part address on the registry page |
| Unexpected resources appear after upgrade | Jumped across a major version with breaking defaults | Read the module's CHANGELOG before widening version constraints |
Failed to install provider after module upgrade |
New module version requires a newer provider than your root config allows | Bump required_providers version range in your root versions.tf |