DEV Community

Mukami
Mukami

Posted on

How Conditionals Make Terraform Infrastructure Dynamic and Efficient

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
  } : {}
}
Enter fullscreen mode Exit fullscreen mode

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."
  }
}
Enter fullscreen mode Exit fullscreen mode

Try to pass an invalid value:

terraform plan -var="environment=stg"
Enter fullscreen mode Exit fullscreen mode
│ Error: Invalid value for variable
│ Environment must be one of: dev, staging, production.
Enter fullscreen mode Exit fullscreen mode

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 = {}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)