One Module, Two Environments, Zero Code Duplication
Day 11 of the 30-Day Terraform Challenge — and today I learned that conditionals are the secret sauce that turns a rigid configuration into a flexible, environment-aware system.
Yesterday I had a module that worked for dev. Today I have a module that works for dev, staging, AND production — all from the same codebase.
No copy-pasting. No separate branches. Just one environment variable driving everything.
The Problem: Hardcoded Values for Every Environment
Before today, if I wanted different instance sizes for dev and production, I had to:
# dev/main.tf
instance_type = "t3.micro"
min_size = 1
max_size = 3
# production/main.tf
instance_type = "t3.medium"
min_size = 3
max_size = 10
Same code. Different files. Change something? Update both places. Forget one? Inconsistent infrastructure.
This works until you have 5 environments. Then it becomes a maintenance nightmare.
The Solution: Centralized Conditional Logic with Locals
Instead of scattering ? : operators everywhere, I put all my conditional decisions in one place:
locals {
# Environment checks
is_production = var.environment == "production"
is_staging = var.environment == "staging"
is_dev = var.environment == "dev"
# Compute sizing based on environment
instance_type = local.is_production ? "t3.medium" : (
local.is_staging ? "t3.small" : "t3.micro"
)
# Auto Scaling settings
min_size = local.is_production ? 3 : (
local.is_staging ? 2 : 1
)
max_size = local.is_production ? 10 : (
local.is_staging ? 5 : 3
)
# Feature toggles
enable_autoscaling = !local.is_dev # Enabled for staging and production
enable_detailed_monitoring = local.is_production
enable_ssh = !local.is_production # Disabled in production
deletion_protection = local.is_production
}
Why this is better:
- All logic in one place — easy to see what changes per environment
- No scattered ternaries through resource arguments
- Easy to test — change one variable, see all impacts
- Easy to maintain — add a new environment? Update one block
Conditional Resource Creation with count
The count = condition ? 1 : 0 pattern makes entire resources optional:
# Only create autoscaling policies for staging and production
resource "aws_autoscaling_policy" "scale_out" {
count = local.enable_autoscaling ? 1 : 0
name = "${var.cluster_name}-scale-out"
scaling_adjustment = 1
autoscaling_group_name = aws_autoscaling_group.web.name
}
# Only create CloudWatch alarms for production
resource "aws_cloudwatch_metric_alarm" "high_cpu" {
count = local.enable_detailed_monitoring ? 1 : 0
alarm_name = "${var.cluster_name}-high-cpu"
threshold = 80
}
When enable_autoscaling = false, the policy isn't created at all. No resources. No costs.
Safe Output References
When resources are conditional, you can't reference them directly. This fails when count = 0:
# BROKEN — errors when resource doesn't exist
output "alarm_arn" {
value = aws_cloudwatch_metric_alarm.high_cpu[0].arn
}
The fix is a ternary guard:
# CORRECT — returns null when resource doesn't exist
output "alarm_arn" {
value = local.enable_detailed_monitoring ?
aws_cloudwatch_metric_alarm.high_cpu[0].arn : null
}
For outputs that return objects:
output "autoscaling_policies" {
value = local.enable_autoscaling ? {
scale_out = aws_autoscaling_policy.scale_out[0].name
scale_in = aws_autoscaling_policy.scale_in[0].name
} : {}
}
Without this guard, Terraform throws Invalid index errors when the resource doesn't exist.
The Validation Block: Catch Mistakes Early
Add a validation block to prevent invalid inputs:
variable "environment" {
description = "Deployment environment: dev, staging, or production"
type = string
default = "dev"
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "Environment must be one of: dev, staging, production."
}
}
Try to pass an invalid value:
terraform plan -var="environment=stg"
│ Error: Invalid value for variable
│ Environment must be one of: dev, staging, production.
Caught at plan time — before anything is deployed.
The Results: Dev vs Production
Same module. Same code. Different outputs.
Dev Environment:
instance_type = "t3.micro"
min_size = 1
max_size = 3
enable_autoscaling = false
enable_monitoring = false
enable_ssh = true
autoscaling_policies = {}
Production Environment:
instance_type = "t3.medium"
min_size = 3
max_size = 10
enable_autoscaling = true
enable_monitoring = true
enable_ssh = false
autoscaling_policies = {
scale_in = "prod-webserver-scale-in"
scale_out = "prod-webserver-scale-out"
}
Dev gets small, cheap, debuggable. Production gets big, scalable, secure. All from one module.
The Conditional Data Source Pattern
Sometimes you need to either create new infrastructure or use existing. This pattern handles both:
variable "use_existing_vpc" {
type = bool
default = false
}
# Conditionally look up existing VPC
data "aws_vpc" "existing" {
count = var.use_existing_vpc ? 1 : 0
id = var.existing_vpc_id
}
# Conditionally create new VPC
resource "aws_vpc" "new" {
count = var.use_existing_vpc ? 0 : 1
cidr_block = "10.0.0.0/16"
}
locals {
vpc_id = var.use_existing_vpc ? data.aws_vpc.existing[0].id : aws_vpc.new[0].id
}
This enables:
- Greenfield deployments — create everything new
- Brownfield deployments — use existing infrastructure
One toggle switches between them.
What I Learned
Conditional expressions choose values. They answer "what should this be?"
Conditional resource creation chooses existence. It answers "should this exist at all?"
You can't directly choose between two resource types with one conditional. You need two resources with count on each.
Locals are better than scattered ternaries. Centralize your logic. Future you will thank you.
Validation blocks are your first line of defense. Catch invalid inputs before they cause damage.
The Bottom Line
Today I transformed my module from hardcoded values to a single environment variable that drives everything:
- Instance sizing (t3.micro → t3.medium)
- Cluster size (1 → 3 → 10 instances)
- Feature toggles (autoscaling, monitoring, SSH)
- Protection settings (deletion protection)
One codebase. Multiple environments. Zero duplication.
This is how infrastructure at scale works.
P.S. The best part? When someone asks "what does dev look like vs production?" I just point to my locals block. Everything is there. One place. No hunting through 500 lines of code.
Top comments (0)