module-patterns

lgbarn's avatarfrom lgbarn

Terraform module development patterns and best practices. Provides structure, versioning, and output scaffolds. Use when creating reusable modules.

⭐0stars🔀0forks📁View on GitHub🕐Updated Dec 22, 2025

When & Why to Use This Skill

This Claude skill provides a comprehensive framework for Terraform module development, focusing on industry-standard patterns and best practices. It offers structured scaffolds for directory layouts, versioning, variable validation, and complex output logic, enabling DevOps engineers to create reusable, maintainable, and production-ready Infrastructure as Code (IaC).

Use Cases

  • Standardizing Module Architecture: Establishing a consistent directory structure (main, variables, outputs, versions) across all internal infrastructure projects to improve team collaboration.
  • Implementing Robust Input Validation: Creating complex variable definitions with custom validation rules and type constraints to prevent configuration errors before deployment.
  • Secure Resource Management: Handling sensitive data in outputs and implementing resource lifecycle rules to prevent accidental destruction of critical cloud components.
  • Automated Infrastructure Testing: Developing unit tests using the native Terraform test framework (tftest.hcl) to validate resource naming, tagging, and logic conditions.
  • Seamless Infrastructure Refactoring: Utilizing 'moved' blocks to rename resources or migrate them into modules without triggering resource recreation or downtime.
namemodule-patterns
descriptionTerraform module development patterns and best practices. Provides structure, versioning, and output scaffolds. Use when creating reusable modules.

Module Patterns

Terraform module development patterns and conventions.

Module Structure

Standard Layout

modules/
└── <module-name>/
    ├── main.tf           # Primary resources
    ├── variables.tf      # Input variables
    ├── outputs.tf        # Module outputs
    ├── versions.tf       # Version constraints
    ├── locals.tf         # Local values
    ├── data.tf           # Data sources (optional)
    ├── README.md         # Documentation
    ├── examples/
    │   ├── basic/
    │   │   ├── main.tf
    │   │   ├── outputs.tf
    │   │   └── README.md
    │   └── complete/
    │       ├── main.tf
    │       ├── outputs.tf
    │       └── README.md
    └── tests/
        ├── basic.tftest.hcl
        └── complete.tftest.hcl

versions.tf Pattern

terraform {
  required_version = ">= 1.5.0"

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

variables.tf Patterns

Required Variable

variable "project" {
  description = "Project name used in resource naming"
  type        = string

  validation {
    condition     = can(regex("^[a-z][a-z0-9-]*$", var.project))
    error_message = "Project name must start with a letter and contain only lowercase letters, numbers, and hyphens."
  }
}

variable "environment" {
  description = "Environment name (dev, staging, prod)"
  type        = string

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

Optional with Default

variable "instance_type" {
  description = "EC2 instance type for compute resources"
  type        = string
  default     = "t3.medium"
}

variable "enable_encryption" {
  description = "Enable encryption at rest for all supported resources"
  type        = bool
  default     = true
}

Complex Type with Defaults

variable "node_groups" {
  description = "Map of EKS managed node group definitions"
  type = map(object({
    instance_types = list(string)
    min_size       = number
    max_size       = number
    desired_size   = number
    disk_size      = optional(number, 100)
    disk_type      = optional(string, "gp3")
    capacity_type  = optional(string, "ON_DEMAND")
    labels         = optional(map(string), {})
    taints = optional(list(object({
      key    = string
      value  = string
      effect = string
    })), [])
  }))
  default = {}
}

Sensitive Variable

variable "database_password" {
  description = "Database master password"
  type        = string
  sensitive   = true

  validation {
    condition     = length(var.database_password) >= 16
    error_message = "Database password must be at least 16 characters."
  }
}

Tags Variable

variable "tags" {
  description = "Additional tags to apply to all resources"
  type        = map(string)
  default     = {}
}

outputs.tf Patterns

Resource Identifiers

output "id" {
  description = "The ID of the primary resource"
  value       = aws_resource.this.id
}

output "arn" {
  description = "The ARN of the primary resource"
  value       = aws_resource.this.arn
}

Connection Information

output "endpoint" {
  description = "Endpoint for connecting to the resource"
  value       = aws_resource.this.endpoint
}

output "security_group_id" {
  description = "ID of the associated security group"
  value       = aws_security_group.this.id
}

Sensitive Outputs

output "connection_string" {
  description = "Database connection string"
  value       = "postgres://${var.username}:${random_password.db.result}@${aws_db_instance.this.endpoint}/${var.database_name}"
  sensitive   = true
}

Conditional Outputs

output "cluster_endpoint" {
  description = "EKS cluster endpoint (null if cluster not created)"
  value       = var.create_cluster ? module.eks[0].cluster_endpoint : null
}

output "private_subnets" {
  description = "List of private subnet IDs"
  value       = var.create_vpc ? module.vpc[0].private_subnets : var.private_subnet_ids
}

locals.tf Patterns

Name Construction

locals {
  name_prefix = "${var.project}-${var.environment}"

  resource_names = {
    cluster = "${local.name_prefix}-eks"
    vpc     = "${local.name_prefix}-vpc"
    rds     = "${local.name_prefix}-db"
  }
}

Tag Merging

locals {
  default_tags = {
    Project     = var.project
    Environment = var.environment
    Terraform   = "true"
    Module      = "module-name"
  }

  tags = merge(local.default_tags, var.tags)
}

Configuration Defaults

locals {
  node_group_defaults = {
    instance_types = ["m6i.large", "m5.large"]
    disk_size      = 100
    disk_type      = "gp3"
    capacity_type  = "ON_DEMAND"
  }

  node_groups = {
    for k, v in var.node_groups : k => merge(local.node_group_defaults, v)
  }
}

Conditional Logic

locals {
  create_kms_key = var.kms_key_arn == null
  kms_key_arn    = local.create_kms_key ? aws_kms_key.this[0].arn : var.kms_key_arn

  azs = var.azs != null ? var.azs : slice(data.aws_availability_zones.available.names, 0, 3)
}

main.tf Patterns

Conditional Resource Creation

resource "aws_kms_key" "this" {
  count = var.create_kms_key ? 1 : 0

  description             = "KMS key for ${local.name_prefix}"
  deletion_window_in_days = 7
  enable_key_rotation     = true

  tags = local.tags
}

For Each with Maps

resource "aws_subnet" "private" {
  for_each = var.private_subnets

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

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

Lifecycle Rules

resource "aws_rds_cluster" "this" {
  cluster_identifier = local.resource_names.rds

  # ... configuration ...

  lifecycle {
    prevent_destroy = true
    ignore_changes  = [master_password]
  }
}

Timeouts

resource "aws_eks_cluster" "this" {
  name = local.resource_names.cluster

  # ... configuration ...

  timeouts {
    create = "45m"
    update = "60m"
    delete = "30m"
  }
}

Terraform Test Pattern

# tests/basic.tftest.hcl

provider "aws" {
  region = "us-east-1"
}

variables {
  project     = "test"
  environment = "dev"
}

run "validate_resources" {
  command = plan

  assert {
    condition     = aws_s3_bucket.this.bucket != null
    error_message = "S3 bucket should be created"
  }

  assert {
    condition     = aws_s3_bucket.this.tags["Environment"] == "dev"
    error_message = "Environment tag should be 'dev'"
  }
}

run "validate_naming" {
  command = plan

  assert {
    condition     = can(regex("^test-dev-", aws_s3_bucket.this.bucket))
    error_message = "Bucket name should follow naming convention"
  }
}

run "validate_encryption" {
  command = plan

  variables {
    enable_encryption = true
  }

  assert {
    condition     = length(aws_s3_bucket_server_side_encryption_configuration.this) > 0
    error_message = "Encryption should be enabled"
  }
}

README.md Template

# Module Name

Brief description of what this module creates.

## Usage

```hcl
module "example" {
  source = "path/to/module"

  project     = "myapp"
  environment = "prod"

  # Additional configuration
}

Requirements

Name Version
terraform >= 1.5.0
aws >= 5.0

Providers

Name Version
aws >= 5.0

Resources

Name Type
aws_resource.name resource
aws_data.name data source

Inputs

Name Description Type Default Required
project Project name string n/a yes
environment Environment string n/a yes
tags Additional tags map(string) {} no

Outputs

Name Description
id Resource ID
arn Resource ARN

Examples

  • Basic - Minimal configuration
  • Complete - Full-featured configuration

License

Apache 2.0 Licensed.


## Example basic/main.tf

```hcl
provider "aws" {
  region = "us-east-1"
}

module "example" {
  source = "../../"

  project     = "myapp"
  environment = "dev"
}

output "id" {
  value = module.example.id
}

Moved Blocks for Refactoring

# moves.tf - Use when renaming resources
moved {
  from = aws_instance.web
  to   = aws_instance.application
}

moved {
  from = aws_s3_bucket.data
  to   = module.storage.aws_s3_bucket.main
}

moved {
  from = module.old_name
  to   = module.new_name
}