Anti-Pattern: Over-Complex Module #
A good module hides implementation complexity and provides a simple interface. A bad module hides simplicity behind unnecessary abstraction, forcing users to learn that abstraction before they can use it. The over-complex module is one of the most common anti-patterns found in mature Terraform repositories — starting with good intentions to make a “flexible” module, ending with a module harder to understand than the resources it wraps.
flowchart LR
subgraph Good["Good Module ✅"]
A["Simple\ninterface"] --> B["Hidden\ncomplexity"]
end
subgraph Bad["Over-Complex ❌"]
C["Complex\ninterface"] --> D["Simple\ninside"]
end
style A fill:#10b981,stroke:#059669,color:#fff
style B fill:#10b981,stroke:#059669,color:#fff
style C fill:#ef4444,stroke:#dc2626,color:#fff
style D fill:#ef4444,stroke:#dc2626,color:#fffSigns of an Overly Complex Module #
# ANTI-PATTERN: A module with dozens of variables
# variables.tf — a module already out of control
variable "enable_monitoring" { type = bool; default = true }
variable "enable_logging" { type = bool; default = true }
variable "enable_enhanced_logging" { type = bool; default = false }
variable "enable_debug_logging" { type = bool; default = false }
variable "log_retention_days" { type = number; default = 30 }
variable "log_format" { type = string; default = "json" }
variable "enable_metrics" { type = bool; default = true }
variable "metrics_namespace" { type = string; default = "" }
variable "enable_alarms" { type = bool; default = false }
variable "alarm_email" { type = string; default = "" }
variable "alarm_threshold_cpu" { type = number; default = 80 }
variable "alarm_threshold_memory" { type = number; default = 85 }
variable "alarm_evaluation_periods" { type = number; default = 3 }
# ... 30 more variables
# Problem: Nobody can know what will happen
# without reading all the module's internal code
SIGNS OF AN OVER-COMPLEX MODULE:
✗ More than 15-20 input variables
✗ Variables named like enable_feature_x (excessive boolean flags)
✗ Complex conditional logic inside the module (count = var.enable_x ? 1 : 0)
✗ The module tries to handle every possible use case
✗ The module documentation is longer than the configuration using it
✗ Module users need to read the source code to understand what it does
✗ Hard to write tests because there are too many input combinations
Excessive Abstraction That Hides Clarity #
# ANTI-PATTERN: An "abstract" module that's actually confusing
module "my_ec2" {
source = "./modules/ec2-abstracted"
# Users must guess what these parameters mean
compute_tier = "standard" # What is this? Maps to what?
redundancy_level = 2 # Is this instance count? AZs? Replicas?
network_exposure = "semi-public" # What's the difference between public, semi-public, private?
persistence_mode = "ephemeral" # Does this have a volume or not?
}
# This abstraction hides the actual configuration —
# users don't know what resources will be created
# CORRECT: A module with a clear interface
module "web_server" {
source = "./modules/ec2"
instance_count = 2
instance_type = "t3.medium"
subnet_ids = var.private_subnet_ids # Clear: private subnets
assign_public_ip = false # Clear: no public IP
}
# From this interface, users immediately know:
# - 2 EC2 instances will be created
# - In private subnets
# - Without public IPs
Proliferating Boolean Flags #
# ANTI-PATTERN: Too many boolean flags
module "rds" {
source = "./modules/rds"
enable_multi_az = var.env == "production"
enable_backup = true
enable_performance_insight = var.env == "production"
enable_deletion_protection = var.env == "production"
enable_enhanced_monitoring = var.env == "production"
enable_iam_auth = false
enable_auto_minor_version_upgrade = true
# ... every caller must set all these flags
# CORRECT: Provide commonly used presets
module "rds_production" {
source = "./modules/rds"
preset = "production" # The module handles all production options internally
# Or better yet: provide two separate modules
# modules/rds-production/ and modules/rds-development/
# with the right defaults for each use case
}
# Implementing a preset inside the module
variable "preset" {
type = string
default = "development"
description = "Configuration preset: development or production"
validation {
condition = contains(["development", "production"], var.preset)
error_message = "Preset must be 'development' or 'production'"
}
}
locals {
config = {
development = {
multi_az = false
backup_retention_period = 1
deletion_protection = false
performance_insights_enabled = false
}
production = {
multi_az = true
backup_retention_period = 7
deletion_protection = true
performance_insights_enabled = true
}
}[var.preset]
}
Modules Trying to Do Too Much #
# ANTI-PATTERN: A "god module" that does everything
module "application_stack" {
source = "./modules/application-stack"
# This module creates:
# - VPC and networking
# - ECS cluster
# - RDS database
# - ElastiCache
# - Load balancer
# - Route53 records
# - ACM certificates
# - IAM roles
# - CloudWatch dashboards
# - SNS topics for alerting
# All in one module!
# Problem: Can't be used partially,
# can't be updated partially, very large blast radius
}
# CORRECT: One module, one responsibility
module "networking" {
source = "./modules/networking"
# Only VPC, subnets, routing
}
module "database" {
source = "./modules/rds"
vpc_id = module.networking.vpc_id
subnet_ids = module.networking.private_subnet_ids
}
module "application" {
source = "./modules/ecs-service"
cluster_id = module.compute.cluster_id
db_endpoint = module.database.endpoint
}
# Each module can be updated, tested, and scaled independently
Good Module Interface Principles #
# A good interface: minimal, clear, with sensible defaults
variable "name" {
type = string
description = "Resource name — used as a prefix for all resources created"
}
variable "environment" {
type = string
description = "Environment: dev, staging, or production"
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "Environment must be dev, staging, or production"
}
}
variable "vpc_id" {
type = string
description = "ID of the VPC where resources will be created"
}
variable "subnet_ids" {
type = list(string)
description = "List of subnet IDs where resources will be distributed"
}
# Additional options with sensible defaults
variable "instance_type" {
type = string
description = "EC2 instance type"
default = "t3.medium"
}
variable "tags" {
type = map(string)
description = "Additional tags added to all resources"
default = {}
}
# NOT needed:
# variable "enable_feature_x" — this should be part of the module logic
# variable "internal_timeout_seconds" — implementation detail, not an interface
# variable "advanced_config_object" — a sign the module is already overloaded
Refactoring an Already-Complex Module #
REFACTORING STRATEGY:
Step 1: Identify functional groups
From 40 variables, group them into: networking, compute, monitoring, security
Each group that can stand alone → a candidate for a separate module
Step 2: Separate "core" from "optional"
Core: variables almost always set by all callers
Optional: variables used by <50% of callers
→ Optional features can become separate optional modules
Step 3: Use moved blocks for refactoring without destruction
When splitting a module, use moved blocks so resources
don't need to be destroyed and recreated
Step 4: Deprecation period
Keep the old module around temporarily
Add a warning in the description that the module is deprecated
Give callers time to migrate to the new module
flowchart TD
A["Module size?"] -->|"Few resources"| B["Inline\nbetter"]
A -->|"Many resources"| C["Module"]
C --> D{"Many\ninputs?"}
D -->|"Yes"| E["Too complex\nsimplify"]
D -->|"No"| F["Good module ✅"]
style A fill:#8b5cf6,stroke:#6d28d9,color:#fff
style B fill:#10b981,stroke:#059669,color:#fff
style C fill:#3b82f6,stroke:#1e40af,color:#fff
style D fill:#f59e0b,stroke:#d97706,color:#fff
style E fill:#ef4444,stroke:#dc2626,color:#fff
style F fill:#10b981,stroke:#059669,color:#fffModule Refactoring Strategy #
# When a module is already too complex:
# 1. Identify sub-components that can be separated
# 2. Create a child module for each sub-component
# 3. The parent module becomes an orchestrator only
# BEFORE: Everything in one module
module "app" {
source = "./modules/app-monolith"
# 50+ variables, 30+ resources
}
# AFTER: Split into sub-modules
module "networking" {
source = "./modules/networking"
}
module "compute" {
source = "./modules/compute"
vpc_id = module.networking.vpc_id
}
module "database" {
source = "./modules/database"
subnet_ids = module.networking.private_subnet_ids
}
Module Interface Documentation #
# Every module should have a README with documentation:
# Module: networking
# Description: Creates a VPC and networking components
#
# Required Variables:
# - vpc_cidr (string): CIDR block for the VPC
# - environment (string): Environment name
#
# Optional Variables:
# - enable_nat_gateway (bool, default: true)
# - single_nat_gateway (bool, default: false)
#
# Outputs:
# - vpc_id (string): ID of the VPC
# - private_subnet_ids (list): IDs of the private subnets
# - public_subnet_ids (list): IDs of the public subnets
#
# Usage:
# module "networking" {
# source = "./modules/networking"
# vpc_cidr = "10.0.0.0/16"
# environment = "production"
# }
Summary #
- A good module has a minimal, clear interface — users don’t need to read the source code to understand what will be created.
- More than 15-20 variables is a danger sign — the module is likely trying to do too many things at once.
- Proliferating boolean flags (
enable_x,enable_y,enable_z) create endless combinations that are hard to test — replace them with presets or split into separate modules.- “God modules” that do everything have a large blast radius and can’t be partially updated — split them into modules with a single responsibility.
- Abstraction is only useful if it simplifies the interface, not if it forces users to learn a new system as complex as the original resources.
- When refactoring an already-complex module: identify functional groups, separate core from optional, use
movedblocks for migration without destruction.
← Previous: Anti-Pattern: Terraform as CM Next: Anti-Pattern: Production Failure Scenario →