DevOpsLesson
DevOpsLesson

Free, comprehensive DevOps tutorials and learning roadmaps. Master Docker, Kubernetes, CI/CD, and more.

Stay Updated

Get notified about new tutorials and features.

Tutorials

  • What is DevOps?
  • Docker Tutorial
  • Terraform Tutorial
  • CI/CD Pipeline
  • All Tutorials

Roadmaps

  • DevOps Engineer
  • Cloud Engineer
  • SRE Path
  • All Roadmaps

Company

  • About Us
  • Blog
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 DevOpsLesson. All rights reserved.

DOCKERKUBERNETESTERRAFORMAWSCI/CDLINUXGITDEVOPS ROADMAPCLOUD ROADMAPSRE ROADMAPGIT CHEATSHEETDOCKER CHEATSHEETK8S CHEATSHEETTF CHEATSHEETLINUX CHEATSHEETDOCKERFILE LINTERYAML VALIDATORCRON PARSERREGEX TESTER

Terraform Tutorial

Introduction to Terraform
Installing Terraform
The Terraform Core Workflow
Terraform Variables and Outputs
Terraform State and Backends
Terraform Modules
Terraform Data Sources
Terraform Count and for_each
Terraform Expressions and Functions
Terraform Dynamic Blocks
Terraform Workspaces
Terraform with AWS
Terraform Provisioners
Terraform CI/CD Pipeline
Terraform Security

Using Terraform Modules

PreviousPrev
Next

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

Popular Terraform Modules

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?

PreviousPrev
Next

Continue Learning

Terraform Remote Backends

Learn why Terraform remote backends matter, how to configure S3 with DynamoDB locking, when to use Terraform Cloud or GCS, and how to migrate state safely.

12 min read·Medium

Terraform State Commands

Learn the most important Terraform state and import commands, including terraform state list, show, mv, rm, import, and refresh-related workflows.

10 min read·Medium

Terraform Modules

Learn the big picture of Terraform modules, how to use existing modules safely, and how to write your own reusable modules for cleaner infrastructure code.

5 min read·Medium

Explore Related Topics

AW

AWS Tutorials

Provision AWS infrastructure with Terraform

CI

CI/CD Tutorials

Run terraform plan/apply in CI pipelines

Try the Tool

YAML Validator

Validate Terraform and Kubernetes YAML configs before applying.

On This Page

What Are Terraform Modules?Why Use Modules?Benefits of modulesThe Root Module vs Child ModulesRoot moduleChild moduleMental modelUsing a Module with a `module` BlockImportant parts of the module blockModule Block Syntax in DetailCommon attributesThe Terraform RegistryWhy the Registry is usefulImportant beginner cautionUsing Modules from the Terraform RegistryPopular Terraform ModulesAWS VPC moduleAWS EKS moduleModule Versions and Version ConstraintsVersion pinning exampleVersion constraints exampleWhy version control matters`terraform init` Downloads ModulesUsing a Local ModulePassing Inputs into ModulesAccessing Module OutputsComplete Example: VPC Module + App ModuleChoosing Between Writing Your Own vs Using Registry ModulesUse a Registry module when:Write your own module when:Best Practices for Using Terraform ModulesCommon Beginner MistakesUsing a Registry module without reading the READMEForgetting the versionPassing too many inputs blindlyConfusing root and child modulesFinal ThoughtsKnowledge Check