Docs / Cloud & DevOps / Terraform Modules Reusable Infrastructure

Terraform Modules Reusable Infrastructure

By Admin · Mar 15, 2026 · Updated Apr 23, 2026 · 314 views · 4 min read

Terraform modules encapsulate infrastructure patterns into reusable, testable, and versionable components. Instead of copying and pasting configuration blocks, modules let you define infrastructure once and instantiate it across environments and projects. This guide covers creating, structuring, and managing Terraform modules for production use.

Module Structure

# Standard module directory layout
modules/
├── vpc/
│   ├── main.tf          # Resources
│   ├── variables.tf     # Input variables
│   ├── outputs.tf       # Output values
│   ├── versions.tf      # Provider requirements
│   └── README.md        # Documentation
├── web-server/
│   ├── main.tf
│   ├── variables.tf
│   ├── outputs.tf
│   └── templates/
│       └── user-data.sh
└── database/
    ├── main.tf
    ├── variables.tf
    └── outputs.tf

Creating a Module

# modules/web-server/variables.tf
variable "name" {
  description = "Name prefix for resources"
  type        = string
}

variable "instance_type" {
  description = "Server instance type"
  type        = string
  default     = "t3.micro"
}

variable "vpc_id" {
  description = "VPC ID to deploy into"
  type        = string
}

variable "subnet_ids" {
  description = "Subnet IDs for the instances"
  type        = list(string)
}

variable "min_size" {
  description = "Minimum number of instances"
  type        = number
  default     = 1
}

variable "max_size" {
  description = "Maximum number of instances"
  type        = number
  default     = 3
}

variable "environment" {
  description = "Environment name"
  type        = string
  validation {
    condition     = contains(["dev", "staging", "production"], var.environment)
    error_message = "Environment must be dev, staging, or production."
  }
}
# modules/web-server/main.tf
resource "aws_security_group" "web" {
  name_prefix = "${var.name}-web-"
  vpc_id      = var.vpc_id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name        = "${var.name}-web-sg"
    Environment = var.environment
  }
}

resource "aws_launch_template" "web" {
  name_prefix   = "${var.name}-web-"
  image_id      = data.aws_ami.ubuntu.id
  instance_type = var.instance_type

  vpc_security_group_ids = [aws_security_group.web.id]

  user_data = base64encode(templatefile("${path.module}/templates/user-data.sh", {
    environment = var.environment
  }))

  tag_specifications {
    resource_type = "instance"
    tags = { Name = "${var.name}-web", Environment = var.environment }
  }
}

resource "aws_autoscaling_group" "web" {
  name                = "${var.name}-web-asg"
  min_size            = var.min_size
  max_size            = var.max_size
  desired_capacity    = var.min_size
  vpc_zone_identifier = var.subnet_ids

  launch_template {
    id      = aws_launch_template.web.id
    version = "$Latest"
  }
}

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-*-24.04-amd64-server-*"]
  }
}
# modules/web-server/outputs.tf
output "security_group_id" {
  description = "Security group ID for the web servers"
  value       = aws_security_group.web.id
}

output "asg_name" {
  description = "Auto Scaling Group name"
  value       = aws_autoscaling_group.web.name
}

Using the Module

# environments/production/main.tf
module "web_server" {
  source = "../../modules/web-server"

  name          = "myapp"
  instance_type = "t3.medium"
  vpc_id        = module.vpc.vpc_id
  subnet_ids    = module.vpc.private_subnet_ids
  min_size      = 2
  max_size      = 6
  environment   = "production"
}

# Can instantiate multiple times
module "api_server" {
  source = "../../modules/web-server"

  name          = "myapi"
  instance_type = "t3.large"
  vpc_id        = module.vpc.vpc_id
  subnet_ids    = module.vpc.private_subnet_ids
  min_size      = 3
  max_size      = 10
  environment   = "production"
}

Module Versioning

# Use Git tags for module versions
module "web_server" {
  source = "git::https://github.com/myorg/terraform-modules.git//web-server?ref=v1.2.0"
}

# Or Terraform Registry
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"
}

# Local module (no versioning, changes immediately)
module "web_server" {
  source = "../modules/web-server"
}

Testing Modules

# Use Terratest (Go) for integration testing
# test/web_server_test.go
func TestWebServerModule(t *testing.T) {
    terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
        TerraformDir: "../examples/web-server",
        Vars: map[string]interface{}{
            "environment": "test",
        },
    })
    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)

    sgId := terraform.Output(t, terraformOptions, "security_group_id")
    assert.NotEmpty(t, sgId)
}

# Or use terraform test (native, Terraform 1.6+)
# tests/web_server.tftest.hcl
run "create_web_server" {
  command = apply
  variables {
    name        = "test"
    environment = "dev"
  }
  assert {
    condition     = output.security_group_id != ""
    error_message = "Security group ID should not be empty"
  }
}

Best Practices

  • Keep modules focused: One module per logical component (VPC, web server, database)
  • Use semantic versioning: Tag releases with v1.0.0, v1.1.0, v2.0.0
  • Document inputs and outputs: Use description fields and generate docs with terraform-docs
  • Validate inputs: Use variable validation blocks to catch errors early
  • Provide examples: Include an examples/ directory showing how to use the module
  • Test before release: Use Terratest or terraform test for automated validation

Summary

Terraform modules are the key to maintainable infrastructure as code at scale. By encapsulating infrastructure patterns into reusable, versioned modules with clear interfaces, you eliminate configuration drift between environments and enable teams to provision infrastructure consistently. Start by identifying repeated patterns in your Terraform code, extract them into modules, version them with Git tags, and test them before promotion to production.

Was this article helpful?