Learn OpenTofu - Reusable Modules & OpenTofu Registry
Episode 10 of 21

Learn OpenTofu - Reusable Modules & OpenTofu Registry

How to wrap infrastructure into reusable modules with the main.tf, variables.tf, and outputs.tf structure, then consume them from the OpenTofu Registry, Git repositories, or a local path to build a consistent platform across many teams and environments.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

In the previous episode 9 we covered state manipulation & import workflows — importing existing infrastructure via the import block and tofu import, then refactoring without destroy using the moved block and tofu state. Your infrastructure is now neatly recorded in state and safely manageable. The next question emerges: how do we stop every team from writing the same code from scratch?

Without modules, five teams in your company will each declare VPCs, security groups, and databases in slightly different ways. One team opens SSH to the public, another forgets to enable encryption, and worst of all — everything looks "working" so nobody notices the inconsistency until an incident happens.

In this episode we cover reusable modules & the OpenTofu Registry: how to wrap infrastructure into reusable modules, the standard main.tf, variables.tf, and outputs.tf file structure, and how to consume modules from the OpenTofu Registry, Git repositories, or a local path.

Main Discussion

The Module Principle: Lego Blocks

A module is a collection of OpenTofu resources packaged as one unit with inputs (variables) and outputs (exposed values). The best analogy is LEGO blocks: you don't assemble every gear yourself; you use ready-made blocks. All the "correct defaults" are encapsulated inside the module, and consumers only need to supply small inputs.

Every configuration we've written so far — including modules themselves — is actually a root module. The difference: reusable modules are called many times via the module block, while a root module is unique and is the execution entry point.

Reusable Module Structure

The standard convention for a module consists of three files: main.tf for resources, variables.tf for inputs, and outputs.tf for results:

resource "aws_vpc" "main" {
  cidr_block = var.cidr_block
  tags       = var.tags
}
 
resource "aws_subnet" "public" {
  count      = length(var.public_subnets)
  vpc_id     = aws_vpc.main.id
  cidr_block = var.public_subnets[count.index]
  tags = {
    Name = "public-${count.index}"
  }
}

Notice the pattern: variables.tf defines the input contract with type, description, and default; main.tf consumes it via var.name references; outputs.tf exposes the values callers need. A good module only exposes the outputs that are truly needed, not every internal attribute.

Tip

Always fill in description on every variable and output — this isn't secondary documentation, it's part of the module's interface. Module consumers will see these descriptions when running tofu docs or using module tooling. After calling a module, don't forget to run tofu init to download its dependencies.

Consuming Modules from the OpenTofu Registry

The OpenTofu Registry (registry.opentofu.org) is the public home for modules and providers. Popular community modules like terraform-aws-modules/vpc/aws can be called directly with the shorthand namespace/name/provider:

main.tf (root module)
module "vpc" {
  source = "registry.opentofu.org/terraform-aws-modules/vpc/aws"
  version = "5.0.0"
 
  name = "production"
  cidr = "10.0.0.0/16"
  azs  = ["ap-southeast-1a", "ap-southeast-1b"]
}
 
module "vpc_staging" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.0.0"
 
  name = "staging"
  cidr = "10.1.0.0/16"
  azs  = ["ap-southeast-1a"]
}

Two things to note. First, source can be written in full with the registry host, or shortened — the registry.opentofu.org host is used by default. Second, calling the same module twice is fine: each module block produces a separate instantiation, each with its own inputs. That's the power of "one definition, many instances."

Warning

Always pin version when using a module from the registry. Without a version, one upstream module update could unexpectedly change your entire infrastructure on the next tofu init. For production environments, combine with a lock file to pin provider and module versions.

Module Sources: Registry, Git, and Local Path

Not every module deserves publication to the registry. OpenTofu supports three main sources:

SourceExample sourceWhen to Use
OpenTofu Registryterraform-aws-modules/vpc/awsPublic modules, managed versions
Git repositorygit::https://github.com/org/modules.git//vpc?ref=v1.0.0Private team modules, review workflows
Local path./modules/vpcInternal modules in a single repository

The Git source uses the git::URL//subdir?ref=ref scheme — the double slash points to a directory inside the repo, and ref pins to a specific branch, tag, or commit:

source from Git
module "vpc" {
  source  = "git::https://github.com/org/modules.git//vpc?ref=v1.0.0"
  cidr    = "10.0.0.0/16"
  subnets = ["10.0.1.0/24"]
}

A local path is best when the module is still evolving in the same repository as its caller — no need to push to the registry for every change:

source from a local path
module "vpc" {
  source  = "./modules/vpc"
  cidr    = "10.2.0.0/16"
  subnets = ["10.2.1.0/24"]
}

Note

Moving from a local path to Git usually only changes source — the rest stays. A common development pattern: develop on a local path, push to Git once stable, then pin ref to a release tag in the root module. This gives full control without sacrificing iteration speed.

Module Best Practices

  • One module, one responsibility — a module that's too large is hard to test and reuse.
  • Pin versions and leverage lock files for all module sources.
  • Document variables and outputs via description; provide a README when needed.
  • Test modules before sharing — OpenTofu has a native tofu test framework we'll cover next episode.
  • Compose via outputs — chain modules by connecting one module's outputs to another module's inputs rather than copying values.

Conclusion

In episode 10 we built the foundation of modularity:

  • Modules encapsulate resources with inputs and outputs, like reusable LEGO blocks.
  • The standard structure main.tf, variables.tf, and outputs.tf forms a clear module contract.
  • The OpenTofu Registry provides public modules with shorthand source and pinned version.
  • Other sources: Git repositories for private modules, and local paths for internal development.
  • Best practices: single responsibility, pinned versions, documentation, and testing.

With modules, you can build a consistent platform across many teams and environments. In the next episode, episode 11, we'll cover Formatting, Linting & the Native Testing Framework — tidying code with tofu fmt, validating with tofu validate, scanning with Checkov and Trivy, then writing .tftest.hcl tests with tofu test without needing Terratest. See you there!

Learn OpenTofu - Reusable Modules & OpenTofu Registry | Learn OpenTofu