Modules are how you write reusable, composable Terraform code. Without modules, you copy-paste infrastructure definitions across environments. With modules, you define once and configure per use.
Module Structure
modules/
vpc/
main.tf # Resources
variables.tf # Input variables
outputs.tf # Output values
versions.tf # Provider requirements
README.md # Documentationvariables.tf
variable "name" {
description = "VPC name"
type = string
}
variable "cidr_block" {
description = "VPC CIDR block"
type = string
default = "10.0.0.0/16"
validation {
condition = can(cidrhost(var.cidr_block, 0))
error_message = "Must be a valid CIDR block."
}
}
variable "az_count" {
description = "Number of availability zones"
type = number
default = 3
validation {
condition = var.az_count >= 1 && var.az_count <= 6
error_message = "Must be between 1 and 6."
}
}
variable "enable_nat" {
description = "Enable NAT gateway for private subnets"
type = bool
default = true
}
variable "tags" {
description = "Additional tags"
type = map(string)
default = {}
}main.tf
data "aws_availability_zones" "available" {
state = "available"
}
locals {
azs = slice(data.aws_availability_zones.available.names, 0, var.az_count)
}
resource "aws_vpc" "this" {
cidr_block = var.cidr_block
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(var.tags, {
Name = var.name
})
}
resource "aws_subnet" "public" {
for_each = toset(local.azs)
vpc_id = aws_vpc.this.id
cidr_block = cidrsubnet(var.cidr_block, 8, index(local.azs, each.key))
availability_zone = each.key
map_public_ip_on_launch = true
tags = merge(var.tags, {
Name = "${var.name}-public-${each.key}"
Tier = "public"
})
}
resource "aws_subnet" "private" {
for_each = toset(local.azs)
vpc_id = aws_vpc.this.id
cidr_block = cidrsubnet(var.cidr_block, 8, index(local.azs, each.key) + var.az_count)
availability_zone = each.key
tags = merge(var.tags, {
Name = "${var.name}-private-${each.key}"
Tier = "private"
})
}outputs.tf
output "vpc_id" {
description = "VPC ID"
value = aws_vpc.this.id
}
output "public_subnet_ids" {
description = "Public subnet IDs"
value = [for s in aws_subnet.public : s.id]
}
output "private_subnet_ids" {
description = "Private subnet IDs"
value = [for s in aws_subnet.private : s.id]
}
output "vpc_cidr" {
description = "VPC CIDR block"
value = aws_vpc.this.cidr_block
}Using Modules
module "production_vpc" {
source = "./modules/vpc"
name = "production"
cidr_block = "10.0.0.0/16"
az_count = 3
enable_nat = true
tags = { Environment = "production" }
}
module "staging_vpc" {
source = "./modules/vpc"
name = "staging"
cidr_block = "10.1.0.0/16"
az_count = 2
enable_nat = false
tags = { Environment = "staging" }
}
# Reference module outputs
resource "aws_instance" "app" {
subnet_id = module.production_vpc.private_subnet_ids[0]
}Master this topic with hands-on labs
Go beyond reading — build real projects in sandboxed environments with expert video guidance.
Browse Courses →Remote Module Sources
# Terraform Registry
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
}
# GitHub
module "vpc" {
source = "github.com/my-org/terraform-modules//vpc?ref=v1.2.0"
}
# S3
module "vpc" {
source = "s3::https://my-bucket.s3.amazonaws.com/modules/vpc.zip"
}
# Git tag (versioned)
module "vpc" {
source = "git::https://github.com/my-org/modules.git//vpc?ref=v1.2.0"
}Module Composition
Build complex infrastructure from simple modules:
module "vpc" {
source = "./modules/vpc"
name = "production"
cidr_block = "10.0.0.0/16"
}
module "database" {
source = "./modules/rds"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
engine = "postgres"
version = "16"
}
module "cluster" {
source = "./modules/eks"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
}
module "app" {
source = "./modules/deployment"
cluster_id = module.cluster.cluster_id
database_url = module.database.connection_string
}Get weekly IT automation tips
Docker, Ansible, Terraform, MLOps — curated insights delivered to your inbox. No spam.
Subscribe Free →Testing Modules
Terraform Test (Built-in)
# tests/vpc.tftest.hcl
run "creates_vpc" {
command = plan
variables {
name = "test"
cidr_block = "10.0.0.0/16"
az_count = 2
}
assert {
condition = aws_vpc.this.cidr_block == "10.0.0.0/16"
error_message = "VPC CIDR doesn't match"
}
assert {
condition = length(aws_subnet.public) == 2
error_message = "Expected 2 public subnets"
}
}terraform testBest Practices
| Practice | Why |
|---|---|
| One module, one purpose | VPC module shouldn't create databases |
| Validate all inputs | Catch errors before apply |
| Document with README | Other teams need to understand your module |
| Version with Git tags | v1.0.0, v1.1.0 for breaking changes |
Use for_each over count | Stable resource addresses |
| Output everything useful | Consumers shouldn't need to know internals |
| Pin provider versions | Prevent breaking changes |
Test with terraform test | Catch regressions |
What's Next?
Our Terraform for Beginners course covers module development from scratch across 15 hands-on lessons. First lesson is free.
---
Ready to go deeper? Check out our hands-on course: Terraform for Beginners — practical exercises you can follow along on your own machine.
Ready to learn by doing?
Stop reading tutorials — start building. Expert video courses with hands-on labs in real sandboxed environments.
Related Articles
Terraform AWS Exercises for Practice
Hands-on Terraform AWS exercises from beginner to advanced. Practice EC2, S3, VPC, RDS provisioning with real infrastructure.
Terraform Expert Tips and Patterns
Level up Terraform skills with expert patterns. Dynamic blocks, for_each, moved blocks, validation, and production workflows.
Terraform Course: What You Learn
What a Terraform course teaches you. Skills, exercises, AWS provisioning, and career benefits of learning Terraform for IaC.
Terraform Modules Guide
Learn how to create and use Terraform modules to organize, share, and reuse infrastructure code across projects and teams.
Terraform Providers Deep Dive
Terraform providers for multi-cloud. Configuration, version pinning, aliases for multi-region, and debugging provider issues.
Terraform Security Practices
Secure your Terraform workflows — manage secrets, control access, encrypt state, and implement policy-as-code for safe infrastructure deployments.
Explore topics
Browse more articles on the topics covered here.