bookworm-smart-assistant/skills/terraform-engineer/references/module-patterns.md

6.1 KiB

Terraform Module Patterns

Module Structure

terraform-aws-vpc/
├── main.tf           # Primary resource definitions
├── variables.tf      # Input variable declarations
├── outputs.tf        # Output value definitions
├── versions.tf       # Provider version constraints
├── README.md         # Module documentation
├── examples/
│   └── complete/
│       ├── main.tf
│       └── variables.tf
└── tests/
    └── vpc_test.go

Basic Module Pattern

main.tf

resource "aws_vpc" "this" {
  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_subnet" "private" {
  for_each = var.private_subnets

  vpc_id            = aws_vpc.this.id
  cidr_block        = each.value.cidr_block
  availability_zone = each.value.az

  tags = merge(
    var.tags,
    {
      Name = "${var.name}-private-${each.key}"
      Type = "private"
    }
  )
}

variables.tf

variable "name" {
  description = "Name prefix for all resources"
  type        = string

  validation {
    condition     = length(var.name) > 0 && length(var.name) <= 32
    error_message = "Name must be 1-32 characters"
  }
}

variable "cidr_block" {
  description = "CIDR block for VPC"
  type        = string

  validation {
    condition     = can(cidrhost(var.cidr_block, 0))
    error_message = "Must be valid IPv4 CIDR block"
  }
}

variable "private_subnets" {
  description = "Map of private subnet configurations"
  type = map(object({
    cidr_block = string
    az         = string
  }))
  default = {}
}

variable "tags" {
  description = "Common tags for all resources"
  type        = map(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
}

outputs.tf

output "vpc_id" {
  description = "ID of the VPC"
  value       = aws_vpc.this.id
}

output "vpc_cidr_block" {
  description = "CIDR block of the VPC"
  value       = aws_vpc.this.cidr_block
}

output "private_subnet_ids" {
  description = "IDs of private subnets"
  value       = { for k, v in aws_subnet.private : k => v.id }
}

output "private_subnet_cidrs" {
  description = "CIDR blocks of private subnets"
  value       = { for k, v in aws_subnet.private : k => v.cidr_block }
}

versions.tf

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

Module Composition

# Composite module using child modules
module "networking" {
  source = "./modules/vpc"

  name       = "production"
  cidr_block = "10.0.0.0/16"

  private_subnets = {
    app1 = { cidr_block = "10.0.1.0/24", az = "us-east-1a" }
    app2 = { cidr_block = "10.0.2.0/24", az = "us-east-1b" }
  }

  tags = local.common_tags
}

module "security" {
  source = "./modules/security-groups"

  vpc_id = module.networking.vpc_id

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

Dynamic Blocks

resource "aws_security_group" "this" {
  name   = var.name
  vpc_id = var.vpc_id

  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.from_port
      to_port     = ingress.value.to_port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
      description = ingress.value.description
    }
  }

  dynamic "egress" {
    for_each = var.egress_rules
    content {
      from_port   = egress.value.from_port
      to_port     = egress.value.to_port
      protocol    = egress.value.protocol
      cidr_blocks = egress.value.cidr_blocks
      description = egress.value.description
    }
  }
}

Conditional Resources

# Create NAT gateway only if enabled
resource "aws_nat_gateway" "this" {
  count = var.enable_nat_gateway ? 1 : 0

  allocation_id = aws_eip.nat[0].id
  subnet_id     = aws_subnet.public[0].id

  tags = {
    Name = "${var.name}-nat"
  }

  depends_on = [aws_internet_gateway.this]
}

# Use for_each for multiple optional resources
resource "aws_route53_zone" "private" {
  for_each = var.create_private_zone ? { main = var.domain_name } : {}

  name = each.value

  vpc {
    vpc_id = aws_vpc.this.id
  }
}

Module Versioning

# Pin to specific version
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.1.2"

  # ... configuration
}

# Use version constraints
module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 19.0"  # >= 19.0, < 20.0

  # ... configuration
}

# Reference Git tags
module "custom" {
  source = "git::https://github.com/org/terraform-modules.git//vpc?ref=v1.2.3"

  # ... configuration
}

Module Testing Example

# examples/complete/main.tf
module "vpc_test" {
  source = "../.."

  name       = "test-vpc"
  cidr_block = "10.100.0.0/16"

  private_subnets = {
    app = { cidr_block = "10.100.1.0/24", az = "us-east-1a" }
  }

  tags = {
    Environment = "test"
    ManagedBy   = "terraform"
  }
}

output "vpc_id" {
  value = module.vpc_test.vpc_id
}

Best Practices

  • Keep modules focused and single-purpose
  • Use for_each over count for resources
  • Validate all inputs with validation blocks
  • Document all variables and outputs
  • Use semantic versioning (MAJOR.MINOR.PATCH)
  • Provide complete examples
  • Test modules before publishing
  • Use consistent naming conventions
  • Tag all taggable resources
  • Avoid hardcoded values