| name | tsh-implementing-terraform-modules |
| description | Build reusable Terraform modules for AWS, Azure, and GCP infrastructure following infrastructure-as-code best practices. Use when creating infrastructure modules, standardizing cloud provisioning, or implementing reusable IaC components. |
| user-invocable | false |
Terraform Module Library
Production-ready Terraform module patterns for AWS, Azure, and GCP infrastructure.
Purpose
Create reusable, well-tested Terraform modules for common cloud infrastructure patterns across multiple cloud providers.
When to Use
- Build reusable infrastructure components
- Standardize cloud resource provisioning
- Implement infrastructure as code best practices
- Create multi-cloud compatible modules
- Establish organizational Terraform standards
Module Structure
terraform-modules/
āāā aws/
ā āāā vpc/
ā āāā eks/
ā āāā rds/
ā āāā s3/
āāā azure/
ā āāā vnet/
ā āāā aks/
ā āāā storage/
āāā gcp/
āāā vpc/
āāā gke/
āāā cloud-sql/
Standard Module Pattern
module-name/
āāā 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 files
āāā module_test.go
AWS VPC Module Example
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(
{
Name = var.name
},
var.tags
)
}
resource "aws_subnet" "private" {
count = length(var.private_subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.private_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
tags = merge(
{
Name = "${var.name}-private-${count.index + 1}"
Tier = "private"
},
var.tags
)
}
resource "aws_internet_gateway" "main" {
count = var.create_internet_gateway ? 1 : 0
vpc_id = aws_vpc.main.id
tags = merge(
{
Name = "${var.name}-igw"
},
var.tags
)
}
variables.tf:
variable "name" {
description = "Name of the VPC"
type = string
}
variable "cidr_block" {
description = "CIDR block for VPC"
type = string
validation {
condition = can(cidrnetmask(var.cidr_block))
error_message = "CIDR block must be valid IPv4 CIDR notation."
}
}
variable "availability_zones" {
description = "List of availability zones"
type = list(string)
}
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 "create_internet_gateway" {
description = "Whether to create an Internet Gateway"
type = bool
default = true
}
variable "tags" {
description = "Additional tags"
type = map(string)
default = {}
}
outputs.tf:
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "private_subnet_ids" {
description = "IDs of private subnets"
value = aws_subnet.private[*].id
}
output "vpc_cidr_block" {
description = "CIDR block of VPC"
value = aws_vpc.main.cidr_block
}
Best Practices
- Use semantic versioning for modules
- Pin provider versions in versions.tf ā use
aws ~> 6.0, azurerm ~> 4.0, google ~> 5.0
- Document all variables with descriptions
- Provide examples in examples/ directory
- Use validation blocks for input validation
- Output important attributes for module composition
- Use locals for computed values
- Implement conditional resources with count/for_each
- Test modules with Terratest
- Tag all resources consistently
Reference Files
references/aws-modules.md - AWS module patterns
references/azure-modules.md - Azure module patterns
references/gcp-modules.md - GCP module patterns
Testing
Use Terratest (Go) to test Terraform modules. Every module must include:
examples/complete/ ā a root module that calls the module under test with realistic values and re-exports its outputs
tests/module_test.go ā Go test file that runs InitAndApply, reads outputs, asserts expected values, and always defers Destroy
Apply the following rules when writing tests:
- Always call
t.Parallel()
- Always wrap options with
terraform.WithDefaultRetryableErrors
- Always use
runtime.Caller(0) to resolve examples/complete/ path relative to the test file ā never use hardcoded relative paths
- Always use
single_nat_gateway = true (or equivalent cost-reducing flags) in test examples
- Provide a plan-only variant of each test (using
InitAndPlanAndShowWithStruct) for fast PR validation that requires no AWS credentials
- Use a dedicated AWS test account ā never run against production
- Set
-timeout 30m in CI to avoid hanging runs
Terraform vs Terragrunt Decision
Use plain Terraform when:
- Single environment, single region
- 2ā3 environments in the same region (use workspaces or directory layout)
- Existing project without Terragrunt (don't migrate mid-project)
Use Terragrunt when:
- 4+ environments or multi-region deployments
- Monorepo with many independent stacks (need
run-all, dependency orchestration)
- Team needs strict environment parity via inheritance
- Multi-account AWS (landing zone pattern)
- Greenfield with expected growth
Terragrunt Golden Path structure:
infrastructure/
āāā terragrunt.hcl # Root config (remote_state, generate provider)
āāā _envcommon/ # Shared module references
ā āāā vpc.hcl
ā āāā eks.hcl
ā āāā rds.hcl
āāā dev/
ā āāā env.hcl # Environment-level vars
ā āāā vpc/terragrunt.hcl
ā āāā eks/terragrunt.hcl
āāā staging/
ā āāā ...
āāā prod/
āāā ...
Related Skills
tsh-designing-multi-cloud-architecture - For architectural decisions
tsh-optimizing-cloud-cost - For cost-effective designs