Using Terraform Modules

Learn how to use Terraform modules from local paths and the Terraform Registry, pass inputs, pin versions, run terraform init, and consume module outputs.

What Are Terraform Modules?

A Terraform module is a directory that contains Terraform configuration files. That definition is surprisingly simple, but it is incredibly powerful.

Every Terraform working directory is already a module. The directory where you run terraform init, terraform plan, and terraform apply is called the root module. When the root module calls another module using a module block, that other module is called a child module.

Modules matter because they make infrastructure code reusable. Instead of copying the same VPC, subnets, security groups, and tags into many projects, you can define them once and reuse them with different inputs.

Why Use Modules?

As infrastructure grows, copy-paste becomes dangerous:

  • bugs get duplicated
  • security fixes are inconsistent
  • naming standards drift
  • onboarding becomes harder
  • reviews get noisier

Modules reduce those problems by standardizing common infrastructure patterns.

Benefits of modules

  1. Reusability: use the same infrastructure pattern many times.
  2. Consistency: enforce approved defaults for security, tagging, and naming.
  3. Simpler root modules: your top-level configuration becomes cleaner and more readable.
  4. Team productivity: app teams can consume proven modules instead of building everything from scratch.
  5. Safer upgrades: versioned modules help teams adopt changes deliberately.

The Root Module vs Child Modules

Understanding this distinction is essential.

Root module

The root module is the Terraform directory you run commands in.

Example:

project-root/
  main.tf
  variables.tf
  outputs.tf

If you run Terraform from project-root/, that directory is the root module.

Child module

A child module is any module called by another module.

Example structure:

project-root/
  main.tf
  modules/
    vpc/
      main.tf
      variables.tf
      outputs.tf

If project-root/main.tf contains:

module "network" {
  source = "./modules/vpc"
}

then modules/vpc is a child module.

Mental model

  • root module = your deployment entry point
  • child module = reusable building block called by the root or another module

Using a Module with a module Block

Terraform uses the module block to call a module.

module "network" {
  source = "./modules/vpc"

  project_name = var.project_name
  environment  = var.environment
  vpc_cidr     = var.vpc_cidr
}

Important parts of the module block

  • module "network" gives the module call a local name
  • source tells Terraform where to find the module
  • the remaining arguments are inputs passed into the child module

This is similar to calling a function in programming:

  • the module source is the function definition
  • input variables are parameters
  • outputs are return values

Module Block Syntax in Detail

Here is a more complete example:

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.8.1"

  name = "platform-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24"]

  enable_nat_gateway = true
  single_nat_gateway = true

  tags = {
    Environment = "prod"
    ManagedBy   = "Terraform"
  }
}

Common attributes

AttributeMeaning
sourceWhere the module lives
versionWhich version of a Registry module to use
other argumentsValues for the child module's input variables

version is used for Registry modules and some remote sources. It is not used with plain local paths.

The Terraform Registry

The Terraform Registry at registry.terraform.io is the central catalog of published Terraform providers and modules.

It includes:

  • official HashiCorp modules
  • community modules
  • partner-maintained modules

Using Registry modules can save huge amounts of time because common infrastructure patterns already exist.

Why the Registry is useful

Instead of writing a VPC module from scratch, you may be able to start from a mature, widely used module that already handles:

  • subnets
  • route tables
  • NAT gateways
  • tags
  • optional features
  • outputs

Important beginner caution

Registry modules are powerful, but do not treat them as magic. Always read:

  • the module README
  • input variable docs
  • outputs
  • version history
  • examples

Using a module without understanding what it creates is risky.

Using Modules from the Terraform Registry

A Registry module source usually looks like this:

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.8.1"

  name = "demo-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24"]
}

The source format often follows:

<namespace>/<name>/<provider>

In this example:

  • namespace = terraform-aws-modules
  • name = vpc
  • provider = aws

Two widely used AWS examples are the AWS VPC module and the AWS EKS module.

AWS VPC module

The VPC module from terraform-aws-modules is popular because it handles a lot of standard networking complexity.

Common use cases include:

  • creating public and private subnets
  • setting up route tables
  • managing NAT gateways
  • standardizing VPC outputs

Example:

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.8.1"

  name = "app-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]

  enable_nat_gateway = true
  single_nat_gateway = true
}

AWS EKS module

The EKS module is popular for provisioning Kubernetes clusters on AWS.

It can help manage:

  • cluster creation
  • node groups
  • IAM integrations
  • cluster networking inputs
  • outputs used by later automation

Example conceptually:

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "20.13.1"

  cluster_name    = "platform-eks"
  cluster_version = "1.31"
  subnet_ids      = module.vpc.private_subnets
  vpc_id          = module.vpc.vpc_id
}

You do not need to memorize these examples; the important lesson is that modules let you consume proven infrastructure patterns instead of reinventing everything yourself.

Module Versions and Version Constraints

When using Registry modules, always think about versions.

If you omit a version entirely, Terraform may download the latest available version. That can be risky because new versions may introduce behavior changes or breaking changes.

Version pinning example

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.8.1"
  # inputs omitted
}

This pins the module to one exact version.

Version constraints example

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.8"
}

This allows patch-level upgrades within the compatible 5.8.x series.

Why version control matters

Pinning versions gives you:

  • reproducible runs
  • safer upgrades
  • easier debugging
  • clearer change management in pull requests

For beginners and production teams alike, explicit versions are a good habit.

terraform init Downloads Modules

When Terraform sees a module block, it does not automatically know where the module code lives locally. terraform init handles that setup.

terraform init

During initialization, Terraform:

  • downloads providers
  • downloads referenced modules
  • prepares the working directory
  • configures the backend if one exists

If you add or change module sources, run terraform init again.

This is why terraform init is often the first command after pulling new Terraform code.

Using a Local Module

Not every module comes from the Registry. Many teams write internal modules and store them in the same repository.

Example:

module "logging_bucket" {
  source = "./modules/s3-logging-bucket"

  bucket_name = "company-prod-logs"
  environment = "prod"
}

Here, the module source is a relative local path.

This is a good pattern when:

  • the module is specific to your organization
  • you are learning modules
  • you want to keep everything in one repository initially

Passing Inputs into Modules

Module inputs are just variables defined inside the child module.

If the child module declares:

variable "environment" {
  type = string
}

then the caller can pass:

module "app" {
  source      = "./modules/app"
  environment = "prod"
}

The caller must satisfy required variables and may optionally override defaults.

Accessing Module Outputs

Child modules return values using output blocks. The parent reads them with:

module.<module_name>.<output_name>

Example child output:

# modules/vpc/outputs.tf
output "vpc_id" {
  value = aws_vpc.main.id
}

Example parent usage:

module "vpc" {
  source = "./modules/vpc"
  # inputs omitted
}

resource "aws_security_group" "app" {
  name   = "app-sg"
  vpc_id = module.vpc.vpc_id
}

This is how modules connect together cleanly.

Complete Example: VPC Module + App Module

module "network" {
  source = "./modules/vpc"

  project_name = var.project_name
  environment  = var.environment
  vpc_cidr     = var.vpc_cidr
}

module "web_app" {
  source = "./modules/web-app"

  project_name = var.project_name
  environment  = var.environment
  vpc_id       = module.network.vpc_id
  subnet_ids   = module.network.public_subnet_ids
}

output "application_url" {
  value = module.web_app.application_url
}

This demonstrates a common Terraform pattern:

  • one module creates shared networking
  • another module creates the application stack
  • outputs connect them together

Choosing Between Writing Your Own vs Using Registry Modules

There is no single right answer.

Use a Registry module when:

  • the problem is common and already well solved
  • the module is mature and widely adopted
  • you want faster delivery
  • you accept the module's opinionated design

Write your own module when:

  • your organization has specific standards
  • you want a simpler interface than a large public module
  • you need custom behavior not covered by existing options
  • you want tighter control over implementation details

Often teams use a mix of both.

Best Practices for Using Terraform Modules

  1. Read the module documentation before using it.
  2. Pin module versions deliberately.
  3. Keep your root module simple and readable.
  4. Use outputs instead of re-creating knowledge in the parent module.
  5. Prefer proven modules for common infrastructure patterns.
  6. Do not pass every possible input unless you truly need to customize it.
  7. Re-run terraform init after adding or changing modules.

Common Beginner Mistakes

Using a Registry module without reading the README

This can lead to surprising resource creation, unnecessary cost, or incorrect assumptions.

Forgetting the version

An unpinned module may change later in ways you did not expect.

Passing too many inputs blindly

Start from the minimal required inputs. Add optional settings only when you understand them.

Confusing root and child modules

Remember: the directory you run Terraform in is the root module. Everything called from there is a child module.

Final Thoughts

Using Terraform modules is a major step from beginner infrastructure code to maintainable real-world infrastructure as code. Modules help you avoid copy-paste, adopt proven patterns, and design cleaner root configurations.

The key ideas to remember are:

  • every Terraform directory is a module
  • the directory you run Terraform in is the root module
  • called modules are child modules
  • module blocks pass inputs in
  • outputs return useful values back out
  • terraform init downloads modules
  • Registry modules are powerful, but should be used thoughtfully and versioned carefully

Once you are comfortable using modules, the next step is learning how to design and write your own reusable modules.

The biggest mindset shift is simple: stop seeing a module as a shortcut and start seeing it as a reusable contract. Good module usage means understanding the inputs, trusting the outputs, pinning the version, and reviewing what the module really creates before you rely on it in production.


Knowledge Check

Exercise

Question 1: Root vs Child Module

What is the root module in Terraform?

Exercise

Question 2: Module Source

Which argument in a module block tells Terraform where to find the module?

Exercise

Question 3: Versioning

Why is pinning a Terraform Registry module version considered a best practice?

Continue Learning

Explore Related Topics

Try the Tool

Related Resources