Skip to main content

Terraform Module

A Terraform module is a container for multiple Terraform resources that are used together. Every Terraform configuration is technically a module — but the term usually refers to reusable child modules that are called from a root module to create a specific piece of infrastructure.

What Is a Terraform Module?

A Terraform module is a folder containing one or more .tf files that work together to create a single, reusable piece of infrastructure — like a VPC, a database, or an EKS cluster. Think of it as a recipe card: you write the recipe once, and anyone on your team can cook the same dish just by changing the ingredients — the input variables.

Here is something most beginners do not realise: every Terraform configuration you have ever written is technically a module. The folder where you run terraform apply is called the root module. Any folder you call from inside that root module using a module block is called a child module. When engineers say "write a module," they almost always mean a reusable child module.

At Swiggy, the platform team writes one module called ecs-service that creates an ECS task definition, a service, a target group, and the right IAM role together. Every team that needs a new microservice — orders, payments, delivery-tracking — calls that same module instead of writing 200 lines of ECS configuration from scratch. One module, fifty services, zero copy-paste bugs.

Root Module vs Child Module

The root module is the entry point — the directory where you run Terraform commands. It usually contains a module block that calls one or more child modules:

HCL
# main.tf — this is the ROOT module
module "vpc" {
source = "./modules/vpc" # this is a CHILD module
cidr_block = "10.0.0.0/16"
}
module "ecs_service" {
source = "./modules/ecs-service" # another CHILD module
service_name = "orders-api"
vpc_id = module.vpc.vpc_id # output from one module feeds into another
}

The child module itself looks like a normal Terraform configuration — it has resources, variables, and outputs — but it never runs terraform apply on its own. It is only ever called.

Calling a Module — Source, Version, Inputs

Every module block needs a source argument that tells Terraform where to find the module code. Everything else inside the block is an input variable being passed in:

HCL
module "rds_postgres" {
source = "terraform-aws-modules/rds/aws" # a registry module
version = "~> 6.0" # pin the version — never leave this out
identifier = "phonepay-payments-db"
engine = "postgres"
engine_version = "15.4"
instance_class = "db.r6g.large"
allocated_storage = 100
db_name = "payments"
username = "payments_admin"
password = var.db_password # never hardcode this — see terraform-secrets-management
vpc_security_group_ids = [aws_security_group.rds.id]
subnet_ids = module.vpc.private_subnets
}

Using Module Outputs

A module's outputs.tf decides what values it exposes to the caller. You reference them with module.<name>.<output>:

HCL
# Inside the module: modules/vpc/outputs.tf
output "vpc_id" {
value = aws_vpc.main.id
}
output "private_subnets" {
value = aws_subnet.private[*].id
}
HCL
# In the root module, calling that output
resource "aws_instance" "app" {
subnet_id = module.vpc.private_subnets[0] # using the module's output
}

Module Versioning with Git Tags

When you pull a module from a Git repository instead of the public registry, pin it to a tag — never to a branch like main. A branch can change underneath you without warning:

HCL
module "networking" {
source = "git::https://github.com/razorpay-platform/terraform-modules.git//vpc?ref=v2.3.0"
# ^^^^^^^^^^^^^^^^
# pinned to an exact release tag
}

If v2.3.0 of the module changes its security group rules tomorrow, your infrastructure does not move — you control exactly when you upgrade by bumping the ref value yourself.

The Module Composition Pattern

Large platform teams rarely write one giant module. Instead, they compose small, focused modules together — a networking module, a compute module, a database module — and a thin root module wires them up:

◈ DIAGRAM
+----------------------------------------------------------+
| ROOT MODULE |
| (environments/prod/main.tf) |
+----------------------------------------------------------+
| | |
v v v
+----------------+ +----------------+ +----------------+
| modules/vpc | | modules/ecs | | modules/rds |
| (networking) | | (compute) | | (database) |
+----------------+ +----------------+ +----------------+
| ^ ^
+------ vpc_id ----+------ vpc_id ------+
(output from vpc feeds into both)

Each module does one job well. The root module's only responsibility is connecting their inputs and outputs.

When to Write a Module vs a Flat Configuration

Common mistake

Common mistake: turning every single resource into its own module. A module is for things you create more than once. If you are building one VPC for one account that will never be copied, a flat configuration is fine — and far easier to read than chasing five module folders to find one security group rule.

Write a module when at least one of these is true:

  • The same group of resources will be created two or more times (dev, staging, prod — or service A, B, C)
  • A different team needs to use this infrastructure pattern without learning its internals
  • You want a single, tested, versioned source of truth for a pattern (like "how we create an RDS instance at our company")

Quick Reference

Concept What It Means
Root module The directory where you run terraform apply
Child module A reusable folder called via a module block
source Where Terraform finds the module code
version Pins a registry module to a specific release
module.<name>.<output> How you reference a module's exposed value
Error Root Cause Fix
Module not installed You added a module but never ran init Run terraform init to download it
Unsupported argument inside module call Passing a variable the module does not declare Check the module's variables.tf for the exact name
Output "x" not found Referencing an output the module does not expose Check the module's outputs.tf
Module changes did not apply Source path changed but .terraform cache is stale Run terraform init -upgrade

Frequently Asked Questions

What's the actual difference between the root module and a child module?

The root module is just the working directory you run `terraform apply` from — every Terraform config is technically a module, even a flat one with no reusable structure. A child module is a separate directory (local path, Git repo, or Terraform Registry package) that you call via a `module` block from the root, parameterized with input variables and exposing output values, letting you package a repeatable pattern like 'a VPC with standard subnets' and reuse it across multiple environments or projects.

What's a common mistake when writing reusable Terraform modules?

Over-parameterizing a module with dozens of optional variables trying to handle every possible use case, which makes it harder to reason about than just writing the resources directly — a good module usually encodes an opinionated, narrow pattern, not a fully generic wrapper around a provider resource. Also, hardcoding a `backend` block inside a child module is a mistake — backend configuration belongs only in the root module, since Terraform doesn't allow modules to have their own state.