410 lines
9.7 KiB
Markdown
410 lines
9.7 KiB
Markdown
# Terraform Module Design Patterns Reference
|
|
|
|
## Pattern 1: Flat Module (Single Directory)
|
|
|
|
Best for: Small projects, < 20 resources, single team ownership.
|
|
|
|
```
|
|
project/
|
|
├── main.tf
|
|
├── variables.tf
|
|
├── outputs.tf
|
|
├── versions.tf
|
|
├── locals.tf
|
|
├── backend.tf
|
|
└── terraform.tfvars
|
|
```
|
|
|
|
### Example: Simple VPC + EC2
|
|
|
|
```hcl
|
|
# versions.tf
|
|
terraform {
|
|
required_version = ">= 1.5.0"
|
|
|
|
required_providers {
|
|
aws = {
|
|
source = "hashicorp/aws"
|
|
version = "~> 5.0"
|
|
}
|
|
}
|
|
}
|
|
|
|
# locals.tf
|
|
locals {
|
|
name_prefix = "${var.project}-${var.environment}"
|
|
common_tags = {
|
|
Project = var.project
|
|
Environment = var.environment
|
|
ManagedBy = "terraform"
|
|
}
|
|
}
|
|
|
|
# main.tf
|
|
resource "aws_vpc" "main" {
|
|
cidr_block = var.vpc_cidr
|
|
enable_dns_hostnames = true
|
|
enable_dns_support = true
|
|
|
|
tags = merge(local.common_tags, {
|
|
Name = "${local.name_prefix}-vpc"
|
|
})
|
|
}
|
|
|
|
resource "aws_subnet" "public" {
|
|
count = length(var.public_subnet_cidrs)
|
|
vpc_id = aws_vpc.main.id
|
|
cidr_block = var.public_subnet_cidrs[count.index]
|
|
availability_zone = var.availability_zones[count.index]
|
|
|
|
tags = merge(local.common_tags, {
|
|
Name = "${local.name_prefix}-public-${count.index + 1}"
|
|
Tier = "public"
|
|
})
|
|
}
|
|
|
|
# variables.tf
|
|
variable "project" {
|
|
description = "Project name used for resource naming"
|
|
type = string
|
|
}
|
|
|
|
variable "environment" {
|
|
description = "Deployment environment"
|
|
type = string
|
|
validation {
|
|
condition = contains(["dev", "staging", "prod"], var.environment)
|
|
error_message = "Environment must be dev, staging, or prod."
|
|
}
|
|
}
|
|
|
|
variable "vpc_cidr" {
|
|
description = "CIDR block for the VPC"
|
|
type = string
|
|
default = "10.0.0.0/16"
|
|
validation {
|
|
condition = can(cidrhost(var.vpc_cidr, 0))
|
|
error_message = "Must be a valid CIDR block."
|
|
}
|
|
}
|
|
|
|
variable "public_subnet_cidrs" {
|
|
description = "CIDR blocks for public subnets"
|
|
type = list(string)
|
|
default = ["10.0.1.0/24", "10.0.2.0/24"]
|
|
}
|
|
|
|
variable "availability_zones" {
|
|
description = "AZs for subnet placement"
|
|
type = list(string)
|
|
default = ["us-east-1a", "us-east-1b"]
|
|
}
|
|
|
|
# outputs.tf
|
|
output "vpc_id" {
|
|
description = "ID of the created VPC"
|
|
value = aws_vpc.main.id
|
|
}
|
|
|
|
output "public_subnet_ids" {
|
|
description = "IDs of public subnets"
|
|
value = aws_subnet.public[*].id
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Pattern 2: Nested Modules (Composition)
|
|
|
|
Best for: Multiple environments, shared patterns, team collaboration.
|
|
|
|
```
|
|
infrastructure/
|
|
├── environments/
|
|
│ ├── dev/
|
|
│ │ ├── main.tf
|
|
│ │ ├── backend.tf
|
|
│ │ └── terraform.tfvars
|
|
│ ├── staging/
|
|
│ │ └── ...
|
|
│ └── prod/
|
|
│ └── ...
|
|
└── modules/
|
|
├── networking/
|
|
│ ├── main.tf
|
|
│ ├── variables.tf
|
|
│ └── outputs.tf
|
|
├── compute/
|
|
│ └── ...
|
|
└── database/
|
|
└── ...
|
|
```
|
|
|
|
### Root Module (environments/dev/main.tf)
|
|
|
|
```hcl
|
|
module "networking" {
|
|
source = "../../modules/networking"
|
|
|
|
project = var.project
|
|
environment = "dev"
|
|
vpc_cidr = "10.0.0.0/16"
|
|
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
|
|
private_subnet_cidrs = ["10.0.10.0/24", "10.0.11.0/24"]
|
|
}
|
|
|
|
module "compute" {
|
|
source = "../../modules/compute"
|
|
|
|
project = var.project
|
|
environment = "dev"
|
|
vpc_id = module.networking.vpc_id
|
|
subnet_ids = module.networking.private_subnet_ids
|
|
instance_type = "t3.micro"
|
|
instance_count = 1
|
|
}
|
|
|
|
module "database" {
|
|
source = "../../modules/database"
|
|
|
|
project = var.project
|
|
environment = "dev"
|
|
vpc_id = module.networking.vpc_id
|
|
subnet_ids = module.networking.private_subnet_ids
|
|
instance_class = "db.t3.micro"
|
|
allocated_storage = 20
|
|
db_password = var.db_password
|
|
}
|
|
```
|
|
|
|
### Key Rules
|
|
- Child modules never call other child modules
|
|
- Pass values explicitly — no hidden data source lookups in children
|
|
- Provider configuration only in root module
|
|
- Each module has its own variables.tf, outputs.tf, main.tf
|
|
|
|
---
|
|
|
|
## Pattern 3: Registry Module Pattern
|
|
|
|
Best for: Reusable modules shared across teams or organizations.
|
|
|
|
```
|
|
terraform-aws-vpc/
|
|
├── main.tf
|
|
├── variables.tf
|
|
├── outputs.tf
|
|
├── versions.tf
|
|
├── README.md
|
|
├── examples/
|
|
│ ├── simple/
|
|
│ │ └── main.tf
|
|
│ └── complete/
|
|
│ └── main.tf
|
|
└── modules/
|
|
├── subnet/
|
|
│ ├── main.tf
|
|
│ ├── variables.tf
|
|
│ └── outputs.tf
|
|
└── nat-gateway/
|
|
└── ...
|
|
```
|
|
|
|
### Publishing Conventions
|
|
|
|
```hcl
|
|
# Consumer usage
|
|
module "vpc" {
|
|
source = "terraform-aws-modules/vpc/aws"
|
|
version = "~> 5.0"
|
|
|
|
name = "my-vpc"
|
|
cidr = "10.0.0.0/16"
|
|
|
|
azs = ["us-east-1a", "us-east-1b"]
|
|
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
|
|
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
|
|
|
|
enable_nat_gateway = true
|
|
single_nat_gateway = true
|
|
}
|
|
```
|
|
|
|
### Registry Module Requirements
|
|
- Repository named `terraform-<PROVIDER>-<NAME>`
|
|
- README.md with usage examples
|
|
- Semantic versioning via git tags
|
|
- examples/ directory with working configurations
|
|
- No provider configuration in the module itself
|
|
|
|
---
|
|
|
|
## Pattern 4: Mono-Repo with Workspaces
|
|
|
|
Best for: Teams that prefer single-repo with workspace-based isolation.
|
|
|
|
```hcl
|
|
# backend.tf
|
|
terraform {
|
|
backend "s3" {
|
|
bucket = "my-terraform-state"
|
|
key = "project/terraform.tfstate"
|
|
region = "us-east-1"
|
|
dynamodb_table = "terraform-locks"
|
|
encrypt = true
|
|
}
|
|
}
|
|
|
|
# main.tf
|
|
locals {
|
|
env_config = {
|
|
dev = {
|
|
instance_type = "t3.micro"
|
|
instance_count = 1
|
|
db_class = "db.t3.micro"
|
|
}
|
|
staging = {
|
|
instance_type = "t3.small"
|
|
instance_count = 2
|
|
db_class = "db.t3.small"
|
|
}
|
|
prod = {
|
|
instance_type = "t3.large"
|
|
instance_count = 3
|
|
db_class = "db.r5.large"
|
|
}
|
|
}
|
|
config = local.env_config[terraform.workspace]
|
|
}
|
|
```
|
|
|
|
### Usage
|
|
```bash
|
|
terraform workspace new dev
|
|
terraform workspace new staging
|
|
terraform workspace new prod
|
|
|
|
terraform workspace select dev
|
|
terraform apply
|
|
|
|
terraform workspace select prod
|
|
terraform apply
|
|
```
|
|
|
|
### Workspace Caveats
|
|
- All environments share the same backend — less isolation than separate directories
|
|
- A mistake in the code affects all environments
|
|
- Can't have different provider versions per workspace
|
|
- Recommended only for simple setups; prefer separate directories for production
|
|
|
|
---
|
|
|
|
## Pattern 5: for_each vs count
|
|
|
|
### Use `count` for identical resources
|
|
```hcl
|
|
resource "aws_subnet" "public" {
|
|
count = 3
|
|
vpc_id = aws_vpc.main.id
|
|
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
|
|
availability_zone = data.aws_availability_zones.available.names[count.index]
|
|
}
|
|
```
|
|
|
|
### Use `for_each` for distinct resources
|
|
```hcl
|
|
variable "buckets" {
|
|
type = map(object({
|
|
versioning = bool
|
|
lifecycle_days = number
|
|
}))
|
|
default = {
|
|
logs = { versioning = false, lifecycle_days = 30 }
|
|
backups = { versioning = true, lifecycle_days = 90 }
|
|
assets = { versioning = true, lifecycle_days = 0 }
|
|
}
|
|
}
|
|
|
|
resource "aws_s3_bucket" "this" {
|
|
for_each = var.buckets
|
|
bucket = "${var.project}-${each.key}"
|
|
}
|
|
|
|
resource "aws_s3_bucket_versioning" "this" {
|
|
for_each = { for k, v in var.buckets : k => v if v.versioning }
|
|
bucket = aws_s3_bucket.this[each.key].id
|
|
|
|
versioning_configuration {
|
|
status = "Enabled"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Why `for_each` > `count`
|
|
- `count` uses index — removing item 0 shifts all others, causing destroy/recreate
|
|
- `for_each` uses keys — removing a key only affects that resource
|
|
- Use `count` only for identical resources where order doesn't matter
|
|
|
|
---
|
|
|
|
## Variable Design Patterns
|
|
|
|
### Object Variables for Related Settings
|
|
```hcl
|
|
variable "database" {
|
|
description = "Database configuration"
|
|
type = object({
|
|
engine = string
|
|
instance_class = string
|
|
storage_gb = number
|
|
multi_az = bool
|
|
backup_days = number
|
|
})
|
|
default = {
|
|
engine = "postgres"
|
|
instance_class = "db.t3.micro"
|
|
storage_gb = 20
|
|
multi_az = false
|
|
backup_days = 7
|
|
}
|
|
}
|
|
```
|
|
|
|
### Validation Blocks
|
|
```hcl
|
|
variable "instance_type" {
|
|
description = "EC2 instance type"
|
|
type = string
|
|
|
|
validation {
|
|
condition = can(regex("^t[23]\\.", var.instance_type))
|
|
error_message = "Only t2 or t3 instance types are allowed."
|
|
}
|
|
}
|
|
|
|
variable "cidr_block" {
|
|
description = "VPC CIDR block"
|
|
type = string
|
|
|
|
validation {
|
|
condition = can(cidrhost(var.cidr_block, 0))
|
|
error_message = "Must be a valid IPv4 CIDR block."
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Anti-Patterns to Avoid
|
|
|
|
| Anti-Pattern | Problem | Solution |
|
|
|-------------|---------|----------|
|
|
| God module (100+ resources) | Impossible to reason about, slow plan/apply | Split into focused child modules |
|
|
| Circular module dependencies | Terraform can't resolve dependency graph | Flatten or restructure module boundaries |
|
|
| Data sources in child modules | Hidden dependencies, hard to test | Pass values as variables from root module |
|
|
| Provider config in child modules | Can't reuse module across accounts/regions | Configure providers in root only |
|
|
| Hardcoded values | Not reusable across environments | Use variables with defaults and validation |
|
|
| No outputs | Consumer modules can't reference resources | Output IDs, ARNs, endpoints |
|
|
| No variable descriptions | Users don't know what to provide | Every variable gets a description |
|
|
| `terraform.tfvars` committed | Secrets leak to version control | Use `.gitignore`, env vars, or Vault |
|