Terraform Β· IaC Β· DevOps

Terraform Modules Complete Guide

Master reusable infrastructure code. Learn module structure, best practices, and real-world patterns with 70+ practical examples.

AY
Anji Yarra
AWS SAA-C03 Β· Cloud & DevOps Engineer
πŸ“š ~20 min read
πŸ’» 70+ examples
Terraform modules are the foundation of scalable infrastructure as code. They enable you to write reusable, maintainable infrastructure code that you can share across projects, teams, and organizations. Learning modules properly transforms you from writing basic Terraform to building enterprise-grade infrastructure.

Module Fundamentals

Definition
What Are Terraform Modules?

A Terraform module is a folder containing Terraform files that define infrastructure resources as a reusable, self-contained unit. It's a way to package related resources (VPC, subnets, security groups) into a single component you can use across multiple projects.

Key concept: Modules are like functions in programming. You write a function once, then call it multiple times with different parameters. Terraform modules work the same wayβ€”define infrastructure once in a module, then use it in many projects.
Module concept
# Without modules (repetitive) # Project 1 - main.tf resource "aws_vpc" "vpc" { ... } resource "aws_subnet" "public" { ... } # Project 2 - main.tf (same code again!) resource "aws_vpc" "vpc" { ... } resource "aws_subnet" "public" { ... } # With modules (DRY) # modules/vpc/main.tf resource "aws_vpc" "vpc" { ... } resource "aws_subnet" "public" { ... } # Project 1 - main.tf module "vpc" { source = "../modules/vpc" } # Project 2 - main.tf module "vpc" { source = "../modules/vpc" }
Benefits
Why Use Modules?

Modules provide major benefits: code reusability, maintainability, consistency, organization, and testing. Without modules, you duplicate code, make updates difficult, and create inconsistencies. Modules solve all these problems.

Real example: You manage 5 AWS projects, each needing identical VPC setup. Without modules: update one setting = fix it in 5 places. With modules: update once in the module, all 5 projects inherit the change instantly.
Benefits comparison
WITHOUT MODULES Code Reusability: ❌ Copy-paste 200+ lines 5 times Maintainability: ❌ Update code in 5 places Organization: ❌ 1000+ line monolithic file Consistency: ❌ Each person configures differently WITH MODULES Code Reusability: βœ… Write once, use everywhere Maintainability: βœ… Update module, affects all uses Organization: βœ… Logical, focused components Consistency: βœ… Same configuration every time
Timing
When Should You Use Modules?

Use modules when you have reusable infrastructure or multiple projects. Start with modules from day one if building multiple environments or projects. Even a single project benefits from modular structure.

Decision Tree

  • Single project, simple (< 10 resources): Can keep in single main.tf, but modules still improve organization
  • Single project, complex (> 10 resources): MUST USE MODULES for organization
  • Multiple projects (same infrastructure): DEFINITELY USE MODULES for code reuse
  • Enterprise (many teams, many projects): USE MODULE REGISTRY with versioning

Module Structure & Organization

Layout
Module Directory Structure

A module is just a directory with Terraform files. The standard structure includes main.tf (resources), variables.tf (inputs), outputs.tf (returns), and optionally README.md (documentation).

Standard module structure
project/ β”œβ”€β”€ main.tf β”œβ”€β”€ terraform.tfvars β”œβ”€β”€ outputs.tf β”œβ”€β”€ variables.tf └── modules/ β”œβ”€β”€ vpc/ β”‚ β”œβ”€β”€ main.tf # VPC resources β”‚ β”œβ”€β”€ variables.tf # Input variables β”‚ β”œβ”€β”€ outputs.tf # Return values β”‚ └── README.md β”œβ”€β”€ rds/ β”‚ β”œβ”€β”€ main.tf β”‚ β”œβ”€β”€ variables.tf β”‚ └── outputs.tf └── security/ β”œβ”€β”€ main.tf β”œβ”€β”€ variables.tf └── outputs.tf
Why this structure: Following the standard layout makes modules predictable. Other Terraform developers expect this organization. It's the community standard.
Files
Purpose of Each Module File

main.tf

Defines all resources. Create VPCs, subnets, databases, security groups here.

variables.tf

Defines input parameters. These are values callers pass in to customize the module.

outputs.tf

Returns values from the module. Callers use these to get results (IDs, endpoints, etc.).

README.md

Documents the module. Include usage example, input descriptions, and output explanations.

Example files
# main.tf - Resources resource "aws_vpc" "this" { cidr_block = var.cidr_block } resource "aws_subnet" "public" { vpc_id = aws_vpc.this.id cidr_block = var.public_subnet_cidr } # variables.tf - Inputs variable "cidr_block" { description = "CIDR block for VPC" type = string default = "10.0.0.0/16" } # outputs.tf - Return values output "vpc_id" { description = "ID of the VPC" value = aws_vpc.this.id }
Sources
Module Sources (Where to Load From)

Modules can be loaded from multiple sources: local filesystem, Git repositories, Terraform Registry, S3, and more. This flexibility enables code sharing across teams and organizations.

Different module sources
# LOCAL - During development module "vpc" { source = "../modules/vpc" } # GIT - Team shared repos module "vpc" { source = "git::https://github.com/company/modules.git//vpc?ref=v1.0.0" } # TERRAFORM REGISTRY - Official public modules module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "~> 5.0" } # S3 - Private enterprise modules module "vpc" { source = "s3::https://s3.amazonaws.com/my-bucket/modules/vpc.zip" }
Hierarchy
Root Module vs Child Modules

Every Terraform configuration has a root module (the directory where you run terraform commands). Child modules are modules called by the root module. The root module orchestrates everything, child modules provide components.

Module hierarchy
ROOT MODULE main.tf - Orchestrates module "network" { source = "./modules/vpc" cidr = var.network_cidr } module "database" { source = "./modules/rds" subnet = module.network.subnet_id } HIERARCHY User runs: terraform apply ↓ Root module gets user variables ↓ Root module calls child modules ↓ Child modules create infrastructure ↓ Child modules return outputs to root ↓ Root module returns final outputs to user

Module Variables & Inputs

Inputs
Module Input Variables

Input variables make modules flexible. Instead of hardcoding values, define variables that callers pass in. This way, one module can create different infrastructure based on inputs.

Module variables example
modules/vpc/variables.tf variable "vpc_cidr" { description = "CIDR block for VPC" type = string default = "10.0.0.0/16" } variable "environment" { description = "Environment name" type = string } main.tf - ROOT MODULE module "prod_vpc" { source = "./modules/vpc" vpc_cidr = "10.0.0.0/16" environment = "production" } module "dev_vpc" { source = "./modules/vpc" vpc_cidr = "10.1.0.0/16" environment = "development" }
Types
Variable Types & Validation

Variables have types: string, number, bool, list, map, etc. Terraform validates types, catching errors early. Add validation rules to ensure inputs are reasonable.

Variable types and validation
Basic types variable "instance_name" { type = string } variable "instance_count" { type = number } variable "enable_monitoring" { type = bool } Complex types variable "availability_zones" { type = list(string) } variable "tags" { type = map(string) } With validation variable "port" { type = number validation { condition = var.port >= 1 && var.port <= 65535 error_message = "Port must be 1-65535" } }
Defaults
Defaults and Required Variables

Variables with defaults are optional. Variables without defaults are required. Critical settings should be required. Advanced options should have sensible defaults.

Defaults vs required
REQUIRED - User MUST provide variable "environment" { description = "Environment (prod/staging/dev)" type = string # NO default } OPTIONAL - Has sensible default variable "instance_type" { description = "EC2 instance type" type = string default = "t3.micro" } variable "enable_encryption" { description = "Enable encryption?" type = bool default = true }
Security
Sensitive Variables

Mark sensitive variables with sensitive=true to prevent Terraform from displaying them in logs. This protects passwords, API keys, and other secrets.

Sensitive variables
modules/rds/variables.tf variable "database_password" { description = "RDS master password" type = string sensitive = true } Supply via environment variables: export TF_VAR_database_password="secret" terraform apply

Module Outputs

Outputs
Module Outputs Basics

Module outputs return values that other modules or the root module can use. Define outputs in outputs.tf, then reference them with module.name.output_name.

Module outputs example
modules/vpc/outputs.tf output "vpc_id" { description = "ID of the VPC" value = aws_vpc.this.id } output "public_subnet_id" { description = "ID of public subnet" value = aws_subnet.public.id } main.tf - Use VPC outputs module "vpc" { source = "./modules/vpc" } module "rds" { source = "./modules/rds" subnet_id = module.vpc.public_subnet_id vpc_id = module.vpc.vpc_id }
Function analogy: In programming, functions return values. Terraform modules return values via outputs. This enables composition: one module's output becomes another's input.
Types
Output Types and Sensitivity

Outputs can return any value type: strings, numbers, lists, maps, objects. Mark outputs sensitive=true if they contain passwords or API keys.

Various output types
Simple outputs output "database_endpoint" { value = aws_db_instance.this.endpoint } List output output "instance_ids" { value = aws_instance.this[*].id } Map output output "instance_ips" { value = { for inst in aws_instance.this : inst.tags.Name => inst.private_ip } } Sensitive output output "database_password" { value = aws_db_instance.this.password sensitive = true }
Doc
Documenting Outputs

Always describe outputs clearly. Users need to know what each output contains and how to use it. Good descriptions prevent confusion and misuse.

Well-documented outputs
GOOD - Clear description output "vpc_id" { description = "ID of the created VPC. Use this when creating resources that must connect to this VPC." value = aws_vpc.this.id } output "private_subnet_ids" { description = "List of private subnet IDs. Pass these to RDS subnet group for database deployment." value = aws_subnet.private[*].id }

Advanced Module Patterns

Composition
Module Composition

Compose modules by calling multiple modules in your root configuration. Each module handles one concern (VPC, RDS, security), then you wire them together.

Module composition example
# Step 1: Create networking module "networking" { source = "./modules/vpc" cidr_block = "10.0.0.0/16" } # Step 2: Create security (depends on VPC) module "security" { source = "./modules/security" vpc_id = module.networking.vpc_id } # Step 3: Create database (depends on VPC & security) module "database" { source = "./modules/rds" subnet_ids = module.networking.private_subnet_ids sg_id = module.security.rds_security_group_id } # Benefits of composition: βœ“ Each module has single responsibility βœ“ Independently testable βœ“ Easy to reuse in other projects βœ“ Changes to one module don't break others βœ“ Clear data flow through outputs/inputs
Versioning
Module Versioning & Stability

When modules live in Git repos, use Git tags for versioning (v1.0.0, v1.1.0, v2.0.0). Callers pin to specific versions to ensure stability.

Module versioning
# GIT TAG YOUR MODULES git tag -a v1.0.0 -m "Initial release" git push --tags # CALLERS PIN TO VERSION module "vpc" { source = "git::https://github.com/company/tf-modules.git//vpc?ref=v1.1.0" } # SEMANTIC VERSIONING v1.0.0 = first release (safe to use) v1.1.0 = backward compatible feature v1.1.1 = bug fix v2.0.0 = breaking change (requires action!)
Testing
Testing Terraform Modules

Test modules with terraform validate and terraform plan. Create test configurations that use the module with various inputs. Verify outputs and resources are created correctly.

Testing modules
# BASIC: terraform validate cd modules/vpc terraform init terraform validate # INTERMEDIATE: terraform plan test # test/main.tf module "vpc_test" { source = ".." cidr_block = "10.0.0.0/16" environment = "test" } cd test terraform plan # TEST CHECKLIST βœ“ Validate syntax (terraform validate) βœ“ Plan with minimal inputs βœ“ Plan with all variables βœ“ Verify all outputs exist βœ“ Test variable validation βœ“ Check dependencies βœ“ Verify no hardcoded secrets
Best Practices
Module Best Practices

Follow best practices: keep modules focused (one responsibility), use clear naming, document thoroughly, validate inputs, handle defaults wisely, make outputs comprehensive, version everything, and test systematically.

Production mindset: Someone else will use your module. Write it as if you won't be around to explain it. Clear names, complete documentation, and validation go a long way.

Best Practices Checklist

  • Design: Single responsibility, clear variable names, required for critical settings, sensible defaults
  • Organization: main.tf, variables.tf, outputs.tf, README.md, examples/, tests/
  • Documentation: Clear README with examples, description for every variable and output
  • Quality: Validate syntax, test with multiple configs, pin versions, run linting
  • Security: No hardcoded secrets, mark sensitive variables, restrict IAM policies, enable encryption