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
1. 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
2. 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"
}
}
}
3. 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
}
4. 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
}
5. 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"
}
}
6. 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))
}
7. 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