DEV Community

Mary Mutua
Mary Mutua

Posted on

How Conditionals Make Terraform Infrastructure Dynamic and Efficient

Day 11 of my Terraform journey was all about going deeper on conditionals.

I had already used conditionals briefly before, but today I focused on how they make a single Terraform configuration behave differently across environments without duplicating code.

This is what makes Terraform feel much smarter in real projects:

  • dev and production can use the same module
  • optional resources can be turned on or off cleanly
  • invalid input can be caught before deployment
  • one codebase can support different use cases

Why Conditionals Matter

Without conditionals, infrastructure code becomes repetitive very quickly.

You end up with:

  • separate dev and production files that mostly repeat the same logic
  • optional resources that require manual commenting in and out
  • brittle outputs that fail when a resource is disabled
  • confusing module behavior when bad input slips through

Conditionals solve that by making Terraform react to input values in a controlled way.

1. The Ternary Expression

The basic Terraform conditional is the ternary expression:

condition ? true_value : false_value
Enter fullscreen mode Exit fullscreen mode

A common mistake is scattering that logic directly inside many resource arguments.

Before

resource "aws_instance" "web" {
  instance_type = var.environment == "production" ? "t3.small" : "t3.micro"
}
Enter fullscreen mode Exit fullscreen mode

This works, but if you repeat the same logic in many places, the configuration becomes hard to read.

Better Pattern: Use locals

locals {
  is_production = var.environment == "production"
  instance_type = local.is_production ? "t3.small" : "t3.micro"
  min_size      = local.is_production ? 3 : 1
  max_size      = local.is_production ? 10 : 3
}

resource "aws_instance" "web" {
  instance_type = local.instance_type
}
Enter fullscreen mode Exit fullscreen mode

This solves a real problem:

  • the decision logic stays in one place
  • resources stay easier to read
  • testing environment differences becomes simpler

2. Optional Resources with count = condition ? 1 : 0

This is one of the most useful Terraform patterns.

It lets you create a resource only when a condition is true.

resource "aws_cloudwatch_metric_alarm" "high_cpu" {
  count = var.enable_detailed_monitoring ? 1 : 0

  alarm_name          = "${var.cluster_name}-high-cpu"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 2
  metric_name         = "CPUUtilization"
  namespace           = "AWS/EC2"
  period              = 120
  statistic           = "Average"
  threshold           = 80
}
Enter fullscreen mode Exit fullscreen mode

This solves the problem of optional infrastructure.

Instead of:

  • duplicating code
  • maintaining separate files
  • commenting blocks in and out

you can simply say:

  • create it when enabled
  • skip it when disabled

I used the same idea for optional DNS creation too.

resource "aws_route53_record" "web" {
  count = var.create_dns_record ? 1 : 0

  zone_id = data.aws_route53_zone.primary[0].zone_id
  name    = var.domain_name
  type    = "A"
  ttl     = 300
  records = [aws_instance.web.public_ip]
}
Enter fullscreen mode Exit fullscreen mode

3. Referencing Conditionally Created Resources Safely

This is where many people get tripped up.

If a resource uses count, Terraform no longer sees it as a single object. It becomes a list.

Wrong

output "alarm_arn" {
  value = aws_cloudwatch_metric_alarm.high_cpu.arn
}
Enter fullscreen mode Exit fullscreen mode

This breaks when count = 0 because the resource does not exist.

Correct

output "alarm_arn" {
  value = var.enable_detailed_monitoring ? aws_cloudwatch_metric_alarm.high_cpu[0].arn : null
}
Enter fullscreen mode Exit fullscreen mode

This solves a very real problem:

  • outputs remain safe
  • Terraform does not crash when the optional resource is missing
  • the module can behave differently without breaking downstream consumers

In my Day 11 module, I used the same pattern for both:

  • alarm_arn
  • dns_record_fqdn

4. Input Validation Blocks

Validation is one of the most practical Terraform features for shared modules.

variable "environment" {
  description = "Deployment environment: dev, staging, or production"
  type        = string

  validation {
    condition     = contains(["dev", "staging", "production"], var.environment)
    error_message = "Environment must be dev, staging, or production."
  }
}
Enter fullscreen mode Exit fullscreen mode

This solves the problem of bad input reaching plan or apply.

Without validation:

  • someone could pass prodution by mistake
  • the module might behave unexpectedly
  • debugging becomes harder

With validation:

  • Terraform fails early
  • the error is clear
  • invalid values are caught before deployment starts

That is especially useful when modules are shared across teams.

5. The Environment-Aware Module Pattern

This was the biggest takeaway of the day.

Instead of maintaining separate modules for dev and production, I built one module that changes behavior based on a single environment variable.

locals {
  is_production = var.environment == "production"

  instance_type               = local.is_production ? "t3.small" : "t3.micro"
  min_cluster_size            = local.is_production ? 3 : 1
  max_cluster_size            = local.is_production ? 10 : 3
  detailed_monitoring_enabled = local.is_production
  dns_record_enabled          = local.is_production
}
Enter fullscreen mode Exit fullscreen mode

Then the root configurations become very small.

Dev

module "webserver_cluster" {
  source = "../../modules/services/webserver-cluster"

  cluster_name = "day11-web-dev"
  environment  = "dev"
}
Enter fullscreen mode Exit fullscreen mode

Production

module "webserver_cluster" {
  source = "../../modules/services/webserver-cluster"

  cluster_name               = "day11-web-production"
  environment                = "production"
  create_dns_record          = false
  enable_detailed_monitoring = true
}
Enter fullscreen mode Exit fullscreen mode

This solves a major real-world problem:

  • one reusable module
  • environment-specific behavior
  • less duplication
  • fewer chances for config drift between environments

A Real Problem I Hit

One useful lesson from today came from Route53.

Because production defaulted to enabling DNS, Terraform tried to look up a hosted zone during planning. Since that hosted zone did not exist in my AWS account, terraform plan failed.

That showed why conditionals matter so much:

  • optional behavior must be guarded carefully
  • data lookups should only happen when the related feature is actually enabled
  • environment-aware defaults sometimes need explicit overrides

That is exactly why I made DNS override-friendly in the production caller.

My Main Takeaway

Day 11 showed me that conditionals are not just small syntax tricks.

They are what make Terraform:

  • reusable
  • environment-aware
  • safer
  • easier to maintain

The biggest lessons for me were:

  • keep conditionals in locals
  • use count = condition ? 1 : 0 for optional resources
  • reference count-based resources safely with [0] and null
  • add validation blocks to fail early
  • let one module adapt to multiple environments instead of duplicating code

That is what makes Terraform infrastructure dynamic and efficient.

Full Code

👉 https://github.com/mary20205090/30-day-Terraform-Challenge/tree/main/day_11

Follow My Journey

This is Day 11 of my 30-Day Terraform Challenge.

See you on Day 12 🚀

Top comments (0)