terraform-module-builder
SKILL.md
Terraform Module Builder
Build reusable, production-ready Terraform modules for cloud infrastructure.
Core Workflow
- Define module structure: Organize files properly
- Declare variables: Input parameters with validation
- Create resources: Infrastructure definitions
- Configure outputs: Export useful values
- Setup state: Remote backend configuration
- Document: README and examples
Module Structure
modules/
└── vpc/
├── main.tf # Primary resources
├── variables.tf # Input variables
├── outputs.tf # Output values
├── versions.tf # Provider versions
├── locals.tf # Local values
├── data.tf # Data sources
├── README.md # Documentation
└── examples/
└── complete/
├── main.tf
└── outputs.tf
VPC Module Example
Main Configuration
# modules/vpc/main.tf
resource "aws_vpc" "main" {
cidr_block = var.cidr_block
enable_dns_hostnames = var.enable_dns_hostnames
enable_dns_support = var.enable_dns_support
tags = merge(
var.tags,
{
Name = var.name
}
)
}
resource "aws_internet_gateway" "main" {
count = var.create_igw ? 1 : 0
vpc_id = aws_vpc.main.id
tags = merge(
var.tags,
{
Name = "${var.name}-igw"
}
)
}
resource "aws_subnet" "public" {
count = length(var.public_subnets)
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnets[count.index]
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = merge(
var.tags,
{
Name = "${var.name}-public-${var.availability_zones[count.index]}"
Tier = "public"
}
)
}
resource "aws_subnet" "private" {
count = length(var.private_subnets)
vpc_id = aws_vpc.main.id
cidr_block = var.private_subnets[count.index]
availability_zone = var.availability_zones[count.index]
tags = merge(
var.tags,
{
Name = "${var.name}-private-${var.availability_zones[count.index]}"
Tier = "private"
}
)
}
resource "aws_eip" "nat" {
count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(var.public_subnets)) : 0
domain = "vpc"
tags = merge(
var.tags,
{
Name = "${var.name}-nat-${count.index + 1}"
}
)
depends_on = [aws_internet_gateway.main]
}
resource "aws_nat_gateway" "main" {
count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(var.public_subnets)) : 0
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = merge(
var.tags,
{
Name = "${var.name}-nat-${count.index + 1}"
}
)
depends_on = [aws_internet_gateway.main]
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
tags = merge(
var.tags,
{
Name = "${var.name}-public-rt"
}
)
}
resource "aws_route" "public_internet" {
count = var.create_igw ? 1 : 0
route_table_id = aws_route_table.public.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main[0].id
}
resource "aws_route_table_association" "public" {
count = length(var.public_subnets)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table" "private" {
count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(var.private_subnets)) : 0
vpc_id = aws_vpc.main.id
tags = merge(
var.tags,
{
Name = "${var.name}-private-rt-${count.index + 1}"
}
)
}
resource "aws_route" "private_nat" {
count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(var.private_subnets)) : 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.main[var.single_nat_gateway ? 0 : count.index].id
}
resource "aws_route_table_association" "private" {
count = length(var.private_subnets)
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private[var.single_nat_gateway ? 0 : count.index].id
}
Variables
# modules/vpc/variables.tf
variable "name" {
description = "Name prefix for all resources"
type = string
validation {
condition = length(var.name) <= 32
error_message = "Name must be 32 characters or less."
}
}
variable "cidr_block" {
description = "CIDR block for the VPC"
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 "availability_zones" {
description = "List of availability zones"
type = list(string)
}
variable "public_subnets" {
description = "List of public subnet CIDR blocks"
type = list(string)
default = []
validation {
condition = alltrue([for cidr in var.public_subnets : can(cidrhost(cidr, 0))])
error_message = "All public subnets must be valid CIDR blocks."
}
}
variable "private_subnets" {
description = "List of private subnet CIDR blocks"
type = list(string)
default = []
}
variable "enable_dns_hostnames" {
description = "Enable DNS hostnames in the VPC"
type = bool
default = true
}
variable "enable_dns_support" {
description = "Enable DNS support in the VPC"
type = bool
default = true
}
variable "create_igw" {
description = "Create Internet Gateway"
type = bool
default = true
}
variable "enable_nat_gateway" {
description = "Enable NAT Gateway for private subnets"
type = bool
default = true
}
variable "single_nat_gateway" {
description = "Use a single NAT Gateway (cost savings)"
type = bool
default = false
}
variable "tags" {
description = "Tags to apply to all resources"
type = map(string)
default = {}
}
Outputs
# modules/vpc/outputs.tf
output "vpc_id" {
description = "The ID of the VPC"
value = aws_vpc.main.id
}
output "vpc_cidr_block" {
description = "The CIDR block of the VPC"
value = aws_vpc.main.cidr_block
}
output "public_subnet_ids" {
description = "List of public subnet IDs"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "List of private subnet IDs"
value = aws_subnet.private[*].id
}
output "public_subnet_cidr_blocks" {
description = "List of public subnet CIDR blocks"
value = aws_subnet.public[*].cidr_block
}
output "private_subnet_cidr_blocks" {
description = "List of private subnet CIDR blocks"
value = aws_subnet.private[*].cidr_block
}
output "nat_gateway_ids" {
description = "List of NAT Gateway IDs"
value = aws_nat_gateway.main[*].id
}
output "internet_gateway_id" {
description = "The ID of the Internet Gateway"
value = try(aws_internet_gateway.main[0].id, null)
}
Versions
# modules/vpc/versions.tf
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
}
Remote State Configuration
# backend.tf
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "production/vpc/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
# State locking table
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
EKS Module Example
# modules/eks/main.tf
resource "aws_eks_cluster" "main" {
name = var.cluster_name
version = var.cluster_version
role_arn = aws_iam_role.cluster.arn
vpc_config {
subnet_ids = var.subnet_ids
endpoint_private_access = var.endpoint_private_access
endpoint_public_access = var.endpoint_public_access
security_group_ids = [aws_security_group.cluster.id]
}
encryption_config {
provider {
key_arn = var.kms_key_arn
}
resources = ["secrets"]
}
enabled_cluster_log_types = var.enabled_log_types
depends_on = [
aws_iam_role_policy_attachment.cluster_policy,
aws_iam_role_policy_attachment.vpc_resource_controller,
]
tags = var.tags
}
resource "aws_eks_node_group" "main" {
for_each = var.node_groups
cluster_name = aws_eks_cluster.main.name
node_group_name = each.key
node_role_arn = aws_iam_role.node.arn
subnet_ids = var.subnet_ids
instance_types = each.value.instance_types
capacity_type = each.value.capacity_type
disk_size = each.value.disk_size
scaling_config {
desired_size = each.value.desired_size
max_size = each.value.max_size
min_size = each.value.min_size
}
update_config {
max_unavailable_percentage = 25
}
labels = each.value.labels
dynamic "taint" {
for_each = each.value.taints
content {
key = taint.value.key
value = taint.value.value
effect = taint.value.effect
}
}
tags = merge(var.tags, each.value.tags)
depends_on = [
aws_iam_role_policy_attachment.node_policy,
aws_iam_role_policy_attachment.cni_policy,
aws_iam_role_policy_attachment.ecr_policy,
]
lifecycle {
ignore_changes = [scaling_config[0].desired_size]
}
}
# modules/eks/variables.tf
variable "cluster_name" {
description = "Name of the EKS cluster"
type = string
}
variable "cluster_version" {
description = "Kubernetes version"
type = string
default = "1.28"
}
variable "node_groups" {
description = "Map of node group configurations"
type = map(object({
instance_types = list(string)
capacity_type = string
disk_size = number
desired_size = number
max_size = number
min_size = number
labels = map(string)
taints = list(object({
key = string
value = string
effect = string
}))
tags = map(string)
}))
}
Environment Configuration
# environments/production/main.tf
terraform {
required_version = ">= 1.0"
backend "s3" {
bucket = "company-terraform-state"
key = "production/main.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
provider "aws" {
region = var.region
default_tags {
tags = {
Environment = "production"
ManagedBy = "terraform"
Project = var.project_name
}
}
}
module "vpc" {
source = "../../modules/vpc"
name = "${var.project_name}-production"
cidr_block = "10.0.0.0/16"
availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
public_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
private_subnets = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"]
enable_nat_gateway = true
single_nat_gateway = false
tags = var.tags
}
module "eks" {
source = "../../modules/eks"
cluster_name = "${var.project_name}-production"
cluster_version = "1.28"
subnet_ids = module.vpc.private_subnet_ids
node_groups = {
general = {
instance_types = ["m6i.xlarge"]
capacity_type = "ON_DEMAND"
disk_size = 100
desired_size = 3
max_size = 10
min_size = 2
labels = { workload = "general" }
taints = []
tags = {}
}
spot = {
instance_types = ["m6i.xlarge", "m5.xlarge"]
capacity_type = "SPOT"
disk_size = 50
desired_size = 2
max_size = 20
min_size = 0
labels = { workload = "batch" }
taints = [{
key = "spot"
value = "true"
effect = "NO_SCHEDULE"
}]
tags = {}
}
}
tags = var.tags
}
Locals and Data Sources
# modules/vpc/locals.tf
locals {
az_count = length(var.availability_zones)
subnet_bits = ceil(log(local.az_count * 2, 2))
public_subnet_cidrs = [
for i in range(local.az_count) :
cidrsubnet(var.cidr_block, local.subnet_bits, i)
]
private_subnet_cidrs = [
for i in range(local.az_count) :
cidrsubnet(var.cidr_block, local.subnet_bits, i + local.az_count)
]
common_tags = merge(
var.tags,
{
Module = "vpc"
CreatedBy = "terraform"
}
)
}
# modules/vpc/data.tf
data "aws_region" "current" {}
data "aws_availability_zones" "available" {
state = "available"
}
data "aws_caller_identity" "current" {}
Best Practices
- Version constraints: Pin provider versions
- Variable validation: Add validation rules
- Consistent naming: Use name prefixes
- Default tags: Apply common tags
- Remote state: Use S3 + DynamoDB locking
- Module composition: Small, focused modules
- Documentation: README with examples
- Output everything: Useful values for consumers
Output Checklist
Every Terraform module should include:
- Proper file structure (main, variables, outputs, versions)
- Variable validation rules
- Meaningful default values
- Comprehensive outputs
- Version constraints
- Remote state configuration
- Tags for all resources
- README with examples
- Locals for computed values
- Data sources for dynamic values
Weekly Installs
11
Repository
patricio0312rev/skillsFirst Seen
10 days ago
Installed on
claude-code9
gemini-cli7
antigravity7
windsurf7
github-copilot7
codex7