Terraform Module Builder
Generates production-ready, reusable Terraform modules with best practices for multi-cloud infrastructure as code.
When to Use
-
"Create Terraform module"
-
"Generate infrastructure module"
-
"Setup Terraform for AWS/Azure/GCP"
-
"Create reusable IaC module"
-
"Generate Terraform boilerplate"
Instructions
- Module Structure
terraform-aws-vpc/ ├── main.tf # Main resources ├── variables.tf # Input variables ├── outputs.tf # Output values ├── versions.tf # Provider versions ├── README.md # Documentation ├── examples/ # Usage examples │ └── complete/ │ ├── main.tf │ └── variables.tf └── tests/ # Terratest └── vpc_test.go
- AWS VPC Module Example
main.tf:
main.tf
locals { name = var.name != "" ? var.name : "${var.environment}-vpc"
common_tags = merge( var.tags, { Environment = var.environment ManagedBy = "Terraform" Module = "terraform-aws-vpc" } ) }
resource "aws_vpc" "this" { cidr_block = var.vpc_cidr enable_dns_hostnames = var.enable_dns_hostnames enable_dns_support = var.enable_dns_support
tags = merge( local.common_tags, { Name = local.name } ) }
resource "aws_subnet" "public" { count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.this.id cidr_block = var.public_subnet_cidrs[count.index] availability_zone = var.availability_zones[count.index] map_public_ip_on_launch = true
tags = merge( local.common_tags, { Name = "${local.name}-public-${var.availability_zones[count.index]}" Type = "public" } ) }
resource "aws_subnet" "private" { count = length(var.private_subnet_cidrs)
vpc_id = aws_vpc.this.id cidr_block = var.private_subnet_cidrs[count.index] availability_zone = var.availability_zones[count.index]
tags = merge( local.common_tags, { Name = "${local.name}-private-${var.availability_zones[count.index]}" Type = "private" } ) }
resource "aws_internet_gateway" "this" { count = length(var.public_subnet_cidrs) > 0 ? 1 : 0
vpc_id = aws_vpc.this.id
tags = merge( local.common_tags, { Name = "${local.name}-igw" } ) }
resource "aws_eip" "nat" { count = var.enable_nat_gateway ? length(var.availability_zones) : 0
domain = "vpc"
tags = merge( local.common_tags, { Name = "${local.name}-nat-${var.availability_zones[count.index]}" } )
depends_on = [aws_internet_gateway.this] }
resource "aws_nat_gateway" "this" { count = var.enable_nat_gateway ? length(var.availability_zones) : 0
allocation_id = aws_eip.nat[count.index].id subnet_id = aws_subnet.public[count.index].id
tags = merge( local.common_tags, { Name = "${local.name}-nat-${var.availability_zones[count.index]}" } )
depends_on = [aws_internet_gateway.this] }
resource "aws_route_table" "public" { count = length(var.public_subnet_cidrs) > 0 ? 1 : 0
vpc_id = aws_vpc.this.id
tags = merge( local.common_tags, { Name = "${local.name}-public" } ) }
resource "aws_route" "public_internet_gateway" { count = length(var.public_subnet_cidrs) > 0 ? 1 : 0
route_table_id = aws_route_table.public[0].id destination_cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.this[0].id
timeouts { create = "5m" } }
resource "aws_route_table_association" "public" { count = length(var.public_subnet_cidrs)
subnet_id = aws_subnet.public[count.index].id route_table_id = aws_route_table.public[0].id }
resource "aws_route_table" "private" { count = length(var.private_subnet_cidrs)
vpc_id = aws_vpc.this.id
tags = merge( local.common_tags, { Name = "${local.name}-private-${var.availability_zones[count.index]}" } ) }
resource "aws_route" "private_nat_gateway" { count = var.enable_nat_gateway ? length(var.private_subnet_cidrs) : 0
route_table_id = aws_route_table.private[count.index].id destination_cidr_block = "0.0.0.0/0" nat_gateway_id = aws_nat_gateway.this[count.index].id
timeouts { create = "5m" } }
resource "aws_route_table_association" "private" { count = length(var.private_subnet_cidrs)
subnet_id = aws_subnet.private[count.index].id route_table_id = aws_route_table.private[count.index].id }
resource "aws_flow_log" "this" { count = var.enable_flow_logs ? 1 : 0
iam_role_arn = aws_iam_role.flow_logs[0].arn log_destination = aws_cloudwatch_log_group.flow_logs[0].arn traffic_type = "ALL" vpc_id = aws_vpc.this.id
tags = merge( local.common_tags, { Name = "${local.name}-flow-logs" } ) }
resource "aws_cloudwatch_log_group" "flow_logs" { count = var.enable_flow_logs ? 1 : 0
name = "/aws/vpc/${local.name}" retention_in_days = var.flow_logs_retention_days
tags = local.common_tags }
resource "aws_iam_role" "flow_logs" { count = var.enable_flow_logs ? 1 : 0
name = "${local.name}-flow-logs"
assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "vpc-flow-logs.amazonaws.com" } } ] })
tags = local.common_tags }
resource "aws_iam_role_policy" "flow_logs" { count = var.enable_flow_logs ? 1 : 0
name = "flow-logs" role = aws_iam_role.flow_logs[0].id
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", "logs:DescribeLogGroups", "logs:DescribeLogStreams" ] Effect = "Allow" Resource = "*" } ] }) }
variables.tf:
variables.tf
variable "name" { description = "Name to be used on all resources as prefix" type = string default = "" }
variable "environment" { description = "Environment name" type = string validation { condition = contains(["dev", "staging", "production"], var.environment) error_message = "Environment must be dev, staging, or production." } }
variable "vpc_cidr" { description = "CIDR block for VPC" type = string default = "10.0.0.0/16"
validation { condition = can(cidrhost(var.vpc_cidr, 0)) error_message = "VPC CIDR must be a valid IPv4 CIDR block." } }
variable "availability_zones" { description = "List of availability zones" type = list(string) }
variable "public_subnet_cidrs" { description = "CIDR blocks for public subnets" type = list(string) default = [] }
variable "private_subnet_cidrs" { description = "CIDR blocks for private subnets" type = list(string) default = [] }
variable "enable_dns_hostnames" { description = "Enable DNS hostnames in VPC" type = bool default = true }
variable "enable_dns_support" { description = "Enable DNS support in VPC" type = bool default = true }
variable "enable_nat_gateway" { description = "Enable NAT Gateway for private subnets" type = bool default = true }
variable "enable_flow_logs" { description = "Enable VPC Flow Logs" type = bool default = false }
variable "flow_logs_retention_days" { description = "Flow logs retention in days" type = number default = 7 }
variable "tags" { description = "Additional tags for all resources" type = map(string) default = {} }
outputs.tf:
outputs.tf
output "vpc_id" { description = "ID of the VPC" value = aws_vpc.this.id }
output "vpc_cidr" { description = "CIDR block of the VPC" value = aws_vpc.this.cidr_block }
output "public_subnet_ids" { description = "IDs of public subnets" value = aws_subnet.public[*].id }
output "private_subnet_ids" { description = "IDs of private subnets" value = aws_subnet.private[*].id }
output "nat_gateway_ids" { description = "IDs of NAT Gateways" value = aws_nat_gateway.this[*].id }
output "internet_gateway_id" { description = "ID of Internet Gateway" value = try(aws_internet_gateway.this[0].id, null) }
output "public_route_table_ids" { description = "IDs of public route tables" value = aws_route_table.public[*].id }
output "private_route_table_ids" { description = "IDs of private route tables" value = aws_route_table.private[*].id }
versions.tf:
versions.tf
terraform { required_version = ">= 1.0"
required_providers { aws = { source = "hashicorp/aws" version = ">= 5.0" } } }
- Usage Example
examples/complete/main.tf:
provider "aws" { region = "us-west-2" }
module "vpc" { source = "../../"
name = "my-app" environment = "production"
vpc_cidr = "10.0.0.0/16" availability_zones = ["us-west-2a", "us-west-2b", "us-west-2c"]
public_subnet_cidrs = [ "10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24", ]
private_subnet_cidrs = [ "10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24", ]
enable_nat_gateway = true enable_flow_logs = true
tags = { Project = "MyApp" Owner = "Platform Team" } }
output "vpc_id" { value = module.vpc.vpc_id }
- Multi-Cloud: Azure Module
main.tf (Azure):
resource "azurerm_resource_group" "this" { name = "${var.name}-rg" location = var.location
tags = var.tags }
resource "azurerm_virtual_network" "this" { name = "${var.name}-vnet" resource_group_name = azurerm_resource_group.this.name location = azurerm_resource_group.this.location address_space = [var.vnet_cidr]
tags = var.tags }
resource "azurerm_subnet" "public" { count = length(var.public_subnet_cidrs)
name = "${var.name}-public-${count.index + 1}" resource_group_name = azurerm_resource_group.this.name virtual_network_name = azurerm_virtual_network.this.name address_prefixes = [var.public_subnet_cidrs[count.index]] }
resource "azurerm_subnet" "private" { count = length(var.private_subnet_cidrs)
name = "${var.name}-private-${count.index + 1}" resource_group_name = azurerm_resource_group.this.name virtual_network_name = azurerm_virtual_network.this.name address_prefixes = [var.private_subnet_cidrs[count.index]] }
resource "azurerm_network_security_group" "this" { name = "${var.name}-nsg" location = azurerm_resource_group.this.location resource_group_name = azurerm_resource_group.this.name
tags = var.tags }
- State Management
backend.tf:
backend.tf - S3 backend
terraform { backend "s3" { bucket = "my-terraform-state" key = "vpc/terraform.tfstate" region = "us-west-2" encrypt = true dynamodb_table = "terraform-locks" } }
Remote state (Azure):
terraform { backend "azurerm" { resource_group_name = "terraform-state-rg" storage_account_name = "tfstate" container_name = "tfstate" key = "vpc.terraform.tfstate" } }
- Testing with Terratest
tests/vpc_test.go:
package test
import ( "testing" "github.com/gruntwork-io/terratest/modules/terraform" "github.com/stretchr/testify/assert" )
func TestVPCModule(t *testing.T) { t.Parallel()
terraformOptions := &terraform.Options{
TerraformDir: "../examples/complete",
Vars: map[string]interface{}{
"environment": "test",
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
vpcID := terraform.Output(t, terraformOptions, "vpc_id")
assert.NotEmpty(t, vpcID)
publicSubnets := terraform.OutputList(t, terraformOptions, "public_subnet_ids")
assert.Equal(t, 3, len(publicSubnets))
}
- Documentation (README.md)
AWS VPC Terraform Module
Creates a VPC with public and private subnets across multiple availability zones.
Features
- Multi-AZ deployment
- Public and private subnets
- NAT Gateways (optional)
- VPC Flow Logs (optional)
- Customizable CIDR blocks
- Comprehensive tagging
Usage
```hcl module "vpc" { source = "github.com/your-org/terraform-aws-vpc"
name = "my-vpc" environment = "production"
vpc_cidr = "10.0.0.0/16" availability_zones = ["us-west-2a", "us-west-2b"]
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"] private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24"]
enable_nat_gateway = true enable_flow_logs = true
tags = { Project = "MyApp" } } ```
Inputs
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| name | VPC name | string | - | yes |
| environment | Environment | string | - | yes |
| vpc_cidr | VPC CIDR | string | "10.0.0.0/16" | no |
| availability_zones | AZs | list(string) | - | yes |
| enable_nat_gateway | Enable NAT | bool | true | no |
Outputs
| Name | Description |
|---|---|
| vpc_id | VPC ID |
| public_subnet_ids | Public subnet IDs |
| private_subnet_ids | Private subnet IDs |
Requirements
| Name | Version |
|---|---|
| terraform | >= 1.0 |
| aws | >= 5.0 |
Best Practices
DO:
-
Use semantic versioning
-
Document all variables
-
Provide examples
-
Add validation rules
-
Use locals for DRY code
-
Tag all resources
-
Use remote state
-
Write tests
-
Follow naming conventions
DON'T:
-
Hardcode values
-
Skip validation
-
Use default values in production
-
Ignore dependencies
-
Skip documentation
-
Commit .tfstate files
-
Use latest provider versions
-
Forget outputs
Checklist
-
Module structure created
-
Variables defined with validation
-
Resources created
-
Outputs defined
-
Documentation written
-
Examples provided
-
Tests written
-
Versioned and tagged