AskLearn
Loading...
← Back to Terraform Course
AdvancedConfiguration

Conditional Expressions

Dynamic configuration patterns

Tutorial 23: Conditional Expressions and Dynamic Blocks

Learning Objectives

By the end of this tutorial, you will be able to:

  • Use conditional expressions for dynamic resource configuration
  • Implement dynamic blocks for flexible resource structures
  • Apply conditional logic in variable validation and outputs
  • Create adaptive configurations based on environment and feature flags
  • Design maintainable conditional infrastructure patterns

Prerequisites

  • Understanding of HCL expressions and Terraform functions
  • Completed Tutorial 22: Built-in Functions and Expressions
  • Knowledge of Terraform resource configuration

Introduction

Conditional expressions and dynamic blocks enable you to create flexible, adaptive Terraform configurations that respond to different environments, feature flags, and runtime conditions. These features allow your infrastructure code to be more maintainable and reusable across various scenarios.

Conditional Expressions

Basic Conditional Syntax

# Basic ternary operator: condition ? true_value : false_value

# variables.tf
variable "environment" {
  description = "Environment name"
  type        = string
  default     = "dev"
}

variable "enable_monitoring" {
  description = "Enable monitoring features"
  type        = bool
  default     = null  # null means auto-determine based on environment
}

variable "instance_count" {
  description = "Number of instances"
  type        = number
  default     = null
}

# main.tf
locals {
  # Simple conditionals
  is_production = var.environment == "prod"
  is_development = var.environment == "dev"
  
  # Conditional values with defaults
  monitoring_enabled = var.enable_monitoring != null ? var.enable_monitoring : local.is_production
  
  # Multi-condition expressions
  instance_type = (
    var.environment == "prod" ? "t3.large" :
    var.environment == "staging" ? "t3.medium" :
    "t3.micro"
  )
  
  # Conditional instance count
  default_instance_count = (
    local.is_production ? 3 :
    var.environment == "staging" ? 2 : 1
  )
  
  final_instance_count = var.instance_count != null ? var.instance_count : local.default_instance_count
  
  # Conditional resource naming
  name_prefix = local.is_production ? var.project_name : "${var.project_name}-${var.environment}"
  
  # Conditional tags
  environment_tags = local.is_production ? {
    Environment = "production"
    Critical    = "true"
    Backup      = "enabled"
  } : {
    Environment = var.environment
    Critical    = "false"
    Backup      = "disabled"
  }
}

# Conditional resource creation
resource "aws_instance" "app" {
  count = local.final_instance_count
  
  ami           = data.aws_ami.ubuntu.id
  instance_type = local.instance_type
  
  monitoring = local.monitoring_enabled
  
  tags = merge({
    Name = "${local.name_prefix}-app-${count.index + 1}"
  }, local.environment_tags)
}

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]
  
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }
}

Complex Conditional Logic

# variables.tf
variable "features" {
  description = "Feature flags"
  type = object({
    enable_ssl      = bool
    enable_cdn      = bool
    enable_waf      = bool
    enable_logging  = bool
    enable_backup   = bool
  })
  default = {
    enable_ssl     = false
    enable_cdn     = false
    enable_waf     = false
    enable_logging = false
    enable_backup  = false
  }
}

variable "security_level" {
  description = "Security level: basic, standard, or enhanced"
  type        = string
  default     = "basic"
  
  validation {
    condition     = contains(["basic", "standard", "enhanced"], var.security_level)
    error_message = "Security level must be basic, standard, or enhanced."
  }
}

# main.tf
locals {
  # Conditional feature enabling based on environment and security level
  features = {
    ssl_enabled = (
      var.features.enable_ssl || 
      local.is_production || 
      contains(["standard", "enhanced"], var.security_level)
    )
    
    cdn_enabled = (
      var.features.enable_cdn || 
      (local.is_production && var.security_level == "enhanced")
    )
    
    waf_enabled = (
      var.features.enable_waf || 
      var.security_level == "enhanced"
    )
    
    logging_enabled = (
      var.features.enable_logging || 
      !local.is_development
    )
    
    backup_enabled = (
      var.features.enable_backup || 
      var.environment != "dev"
    )
  }
  
  # Conditional SSL certificate configuration
  ssl_config = local.features.ssl_enabled ? {
    certificate_arn = var.existing_cert_arn != null ? var.existing_cert_arn : aws_acm_certificate.app[0].arn
    ssl_policy     = var.security_level == "enhanced" ? "ELBSecurityPolicy-TLS-1-2-2017-01" : "ELBSecurityPolicy-2016-08"
    redirect_http  = true
  } : {
    certificate_arn = null
    ssl_policy     = null
    redirect_http  = false
  }
  
  # Conditional backup configuration
  backup_config = local.features.backup_enabled ? {
    retention_days = (
      local.is_production ? 30 :
      var.environment == "staging" ? 7 : 1
    )
    backup_window = local.is_production ? "03:00-04:00" : "02:00-03:00"
    maintenance_window = local.is_production ? "sun:04:00-sun:05:00" : "sat:03:00-sat:04:00"
  } : null
  
  # Conditional monitoring configuration
  monitoring_config = local.features.logging_enabled ? {
    log_retention_days = (
      local.is_production ? 90 :
      var.environment == "staging" ? 30 : 7
    )
    detailed_monitoring = local.is_production || var.security_level == "enhanced"
    alarm_threshold = local.is_production ? 80 : 90
  } : null
}

# Conditional SSL certificate
resource "aws_acm_certificate" "app" {
  count = local.features.ssl_enabled && var.existing_cert_arn == null ? 1 : 0
  
  domain_name               = var.domain_name
  subject_alternative_names = ["*.${var.domain_name}"]
  validation_method         = "DNS"
  
  lifecycle {
    create_before_destroy = true
  }
  
  tags = {
    Name = "${local.name_prefix}-certificate"
  }
}

# Conditional CloudFront distribution
resource "aws_cloudfront_distribution" "app" {
  count = local.features.cdn_enabled ? 1 : 0
  
  origin {
    domain_name = aws_lb.app.dns_name
    origin_id   = "ALB-${local.name_prefix}"
    
    custom_origin_config {
      http_port              = 80
      https_port             = 443
      origin_protocol_policy = local.features.ssl_enabled ? "https-only" : "http-only"
      origin_ssl_protocols   = local.features.ssl_enabled ? ["TLSv1.2"] : ["TLSv1"]
    }
  }
  
  enabled = true
  
  default_cache_behavior {
    allowed_methods        = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
    cached_methods         = ["GET", "HEAD"]
    target_origin_id       = "ALB-${local.name_prefix}"
    compress               = true
    viewer_protocol_policy = local.features.ssl_enabled ? "redirect-to-https" : "allow-all"
    
    forwarded_values {
      query_string = false
      cookies {
        forward = "none"
      }
    }
  }
  
  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }
  
  viewer_certificate {
    acm_certificate_arn      = local.features.ssl_enabled ? local.ssl_config.certificate_arn : null
    cloudfront_default_certificate = !local.features.ssl_enabled
    ssl_support_method       = local.features.ssl_enabled ? "sni-only" : null
  }
  
  web_acl_id = local.features.waf_enabled ? aws_wafv2_web_acl.app[0].arn : null
  
  tags = {
    Name = "${local.name_prefix}-cdn"
  }
}

# Conditional WAF
resource "aws_wafv2_web_acl" "app" {
  count = local.features.waf_enabled ? 1 : 0
  
  name  = "${local.name_prefix}-waf"
  scope = "CLOUDFRONT"
  
  default_action {
    allow {}
  }
  
  rule {
    name     = "RateLimitRule"
    priority = 1
    
    action {
      block {}
    }
    
    statement {
      rate_based_statement {
        limit              = var.security_level == "enhanced" ? 2000 : 10000
        aggregate_key_type = "IP"
      }
    }
    
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "${local.name_prefix}-RateLimit"
      sampled_requests_enabled   = true
    }
  }
  
  tags = {
    Name = "${local.name_prefix}-waf"
  }
}

Dynamic Blocks

Basic Dynamic Block Usage

# variables.tf
variable "ingress_rules" {
  description = "List of ingress rules"
  type = list(object({
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
    description = string
  }))
  default = [
    {
      from_port   = 80
      to_port     = 80
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
      description = "HTTP"
    },
    {
      from_port   = 443
      to_port     = 443
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
      description = "HTTPS"
    }
  ]
}

variable "egress_rules" {
  description = "List of egress rules"
  type = list(object({
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
    description = string
  }))
  default = [
    {
      from_port   = 0
      to_port     = 0
      protocol    = "-1"
      cidr_blocks = ["0.0.0.0/0"]
      description = "All outbound traffic"
    }
  ]
}

# main.tf
resource "aws_security_group" "app" {
  name_prefix = "${local.name_prefix}-"
  vpc_id      = aws_vpc.main.id
  description = "Security group for ${local.name_prefix}"
  
  # Dynamic ingress rules
  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.from_port
      to_port     = ingress.value.to_port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
      description = ingress.value.description
    }
  }
  
  # Dynamic egress rules
  dynamic "egress" {
    for_each = var.egress_rules
    content {
      from_port   = egress.value.from_port
      to_port     = egress.value.to_port
      protocol    = egress.value.protocol
      cidr_blocks = egress.value.cidr_blocks
      description = egress.value.description
    }
  }
  
  tags = {
    Name = "${local.name_prefix}-security-group"
  }
}

Conditional Dynamic Blocks

# variables.tf
variable "load_balancer_config" {
  description = "Load balancer configuration"
  type = object({
    enable_access_logs = bool
    enable_waf        = bool
    ssl_certificates  = list(string)
    custom_headers    = map(string)
  })
  default = {
    enable_access_logs = false
    enable_waf        = false
    ssl_certificates  = []
    custom_headers    = {}
  }
}

variable "auto_scaling_policies" {
  description = "Auto scaling policies"
  type = list(object({
    name               = string
    policy_type        = string
    adjustment_type    = string
    scaling_adjustment = number
    cooldown          = number
  }))
  default = []
}

# main.tf
resource "aws_lb" "app" {
  name               = "${local.name_prefix}-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.app.id]
  subnets           = aws_subnet.public[*].id
  
  enable_deletion_protection = local.is_production
  
  # Conditional access logs
  dynamic "access_logs" {
    for_each = var.load_balancer_config.enable_access_logs ? [1] : []
    content {
      bucket  = aws_s3_bucket.lb_logs[0].bucket
      prefix  = local.name_prefix
      enabled = true
    }
  }
  
  tags = {
    Name = "${local.name_prefix}-load-balancer"
  }
}

# Conditional S3 bucket for access logs
resource "aws_s3_bucket" "lb_logs" {
  count = var.load_balancer_config.enable_access_logs ? 1 : 0
  
  bucket        = "${local.name_prefix}-lb-logs-${random_id.bucket_suffix.hex}"
  force_destroy = !local.is_production
  
  tags = {
    Name    = "${local.name_prefix}-lb-logs"
    Purpose = "load-balancer-logs"
  }
}

# Load balancer listener with conditional SSL
resource "aws_lb_listener" "app" {
  load_balancer_arn = aws_lb.app.arn
  port              = local.features.ssl_enabled ? "443" : "80"
  protocol          = local.features.ssl_enabled ? "HTTPS" : "HTTP"
  ssl_policy        = local.features.ssl_enabled ? local.ssl_config.ssl_policy : null
  certificate_arn   = local.features.ssl_enabled ? local.ssl_config.certificate_arn : null
  
  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.app.arn
  }
  
  # Dynamic additional certificates
  dynamic "certificate" {
    for_each = local.features.ssl_enabled ? var.load_balancer_config.ssl_certificates : []
    content {
      certificate_arn = certificate.value
    }
  }
}

# Auto scaling group with dynamic policies
resource "aws_autoscaling_group" "app" {
  name                = "${local.name_prefix}-asg"
  vpc_zone_identifier = aws_subnet.private[*].id
  target_group_arns   = [aws_lb_target_group.app.arn]
  health_check_type   = "ELB"
  
  min_size         = local.final_instance_count
  max_size         = local.final_instance_count * 3
  desired_capacity = local.final_instance_count
  
  launch_template {
    id      = aws_launch_template.app.id
    version = "$Latest"
  }
  
  # Dynamic tags
  dynamic "tag" {
    for_each = merge({
      Name = "${local.name_prefix}-asg"
    }, local.environment_tags)
    
    content {
      key                 = tag.key
      value               = tag.value
      propagate_at_launch = true
    }
  }
}

# Dynamic auto scaling policies
resource "aws_autoscaling_policy" "app" {
  for_each = {
    for policy in var.auto_scaling_policies :
    policy.name => policy
  }
  
  name                   = "${local.name_prefix}-${each.key}"
  scaling_adjustment     = each.value.scaling_adjustment
  adjustment_type        = each.value.adjustment_type
  cooldown              = each.value.cooldown
  autoscaling_group_name = aws_autoscaling_group.app.name
  policy_type           = each.value.policy_type
}

resource "random_id" "bucket_suffix" {
  byte_length = 4
}

Advanced Dynamic Block Patterns

# variables.tf
variable "applications" {
  description = "Application configurations"
  type = map(object({
    port         = number
    protocol     = string
    health_path  = string
    environments = map(object({
      replicas        = number
      cpu_request     = string
      memory_request  = string
      env_vars       = map(string)
      volumes        = list(object({
        name       = string
        mount_path = string
        size       = string
        type       = string
      }))
      init_containers = list(object({
        name    = string
        image   = string
        command = list(string)
        env     = map(string)
      }))
    }))
  }))
  default = {
    frontend = {
      port        = 80
      protocol    = "HTTP"
      health_path = "/health"
      environments = {
        dev = {
          replicas       = 1
          cpu_request    = "100m"
          memory_request = "128Mi"
          env_vars = {
            NODE_ENV = "development"
            LOG_LEVEL = "debug"
          }
          volumes = [
            {
              name       = "logs"
              mount_path = "/var/log"
              size       = "1Gi"
              type       = "emptyDir"
            }
          ]
          init_containers = [
            {
              name    = "db-migrate"
              image   = "migrate/migrate"
              command = ["migrate", "-path", "/migrations", "up"]
              env = {
                DB_URL = "postgres://localhost/app"
              }
            }
          ]
        }
      }
    }
  }
}

# main.tf
locals {
  # Flatten applications and environments for iteration
  app_deployments = flatten([
    for app_name, app_config in var.applications : [
      for env_name, env_config in app_config.environments : {
        app_name    = app_name
        environment = env_name
        key         = "${app_name}-${env_name}"
        app_config  = app_config
        env_config  = env_config
      }
    ]
  ])
}

# Kubernetes deployment with complex dynamic blocks
resource "kubernetes_deployment" "apps" {
  for_each = {
    for deployment in local.app_deployments :
    deployment.key => deployment
  }
  
  metadata {
    name      = each.value.key
    namespace = each.value.environment
    
    labels = {
      app         = each.value.app_name
      environment = each.value.environment
    }
  }
  
  spec {
    replicas = each.value.env_config.replicas
    
    selector {
      match_labels = {
        app         = each.value.app_name
        environment = each.value.environment
      }
    }
    
    template {
      metadata {
        labels = {
          app         = each.value.app_name
          environment = each.value.environment
        }
      }
      
      spec {
        # Dynamic init containers
        dynamic "init_container" {
          for_each = each.value.env_config.init_containers
          content {
            name    = init_container.value.name
            image   = init_container.value.image
            command = init_container.value.command
            
            # Dynamic environment variables for init containers
            dynamic "env" {
              for_each = init_container.value.env
              content {
                name  = env.key
                value = env.value
              }
            }
          }
        }
        
        container {
          name  = each.value.app_name
          image = "${each.value.app_name}:latest"
          
          port {
            container_port = each.value.app_config.port
          }
          
          resources {
            requests = {
              cpu    = each.value.env_config.cpu_request
              memory = each.value.env_config.memory_request
            }
          }
          
          # Dynamic environment variables
          dynamic "env" {
            for_each = each.value.env_config.env_vars
            content {
              name  = env.key
              value = env.value
            }
          }
          
          # Dynamic volume mounts
          dynamic "volume_mount" {
            for_each = each.value.env_config.volumes
            content {
              name       = volume_mount.value.name
              mount_path = volume_mount.value.mount_path
            }
          }
          
          liveness_probe {
            http_get {
              path = each.value.app_config.health_path
              port = each.value.app_config.port
            }
            initial_delay_seconds = 30
            period_seconds       = 10
          }
        }
        
        # Dynamic volumes
        dynamic "volume" {
          for_each = each.value.env_config.volumes
          content {
            name = volume.value.name
            
            # Conditional volume types
            dynamic "empty_dir" {
              for_each = volume.value.type == "emptyDir" ? [1] : []
              content {
                size_limit = volume.value.size
              }
            }
            
            dynamic "persistent_volume_claim" {
              for_each = volume.value.type == "pvc" ? [1] : []
              content {
                claim_name = "${each.value.key}-${volume.value.name}-pvc"
              }
            }
          }
        }
      }
    }
  }
}

# Conditional PVCs for persistent volumes
resource "kubernetes_persistent_volume_claim" "app_volumes" {
  for_each = {
    for volume in flatten([
      for deployment in local.app_deployments : [
        for vol in deployment.env_config.volumes : {
          key         = "${deployment.key}-${vol.name}-pvc"
          volume      = vol
          deployment  = deployment
        }
        if vol.type == "pvc"
      ]
    ]) : volume.key => volume
  }
  
  metadata {
    name      = each.key
    namespace = each.value.deployment.environment
  }
  
  spec {
    access_modes = ["ReadWriteOnce"]
    resources {
      requests = {
        storage = each.value.volume.size
      }
    }
  }
}

Conditional Module Usage

# modules/database/main.tf
variable "create_read_replica" {
  description = "Whether to create read replica"
  type        = bool
  default     = false
}

variable "backup_config" {
  description = "Backup configuration"
  type = object({
    enabled             = bool
    retention_days      = number
    backup_window       = string
    maintenance_window  = string
  })
  default = {
    enabled            = false
    retention_days     = 0
    backup_window      = ""
    maintenance_window = ""
  }
}

resource "aws_db_instance" "main" {
  identifier = var.db_identifier
  
  engine         = var.engine
  engine_version = var.engine_version
  instance_class = var.instance_class
  
  allocated_storage = var.allocated_storage
  storage_encrypted = var.storage_encrypted
  
  db_name  = var.db_name
  username = var.username
  password = var.password
  
  # Conditional backup configuration
  backup_retention_period = var.backup_config.enabled ? var.backup_config.retention_days : 0
  backup_window          = var.backup_config.enabled ? var.backup_config.backup_window : null
  maintenance_window     = var.backup_config.enabled ? var.backup_config.maintenance_window : null
  
  skip_final_snapshot = !var.backup_config.enabled
  final_snapshot_identifier = var.backup_config.enabled ? "${var.db_identifier}-final-snapshot" : null
  
  tags = var.tags
}

resource "aws_db_instance" "read_replica" {
  count = var.create_read_replica ? 1 : 0
  
  identifier             = "${var.db_identifier}-replica"
  replicate_source_db    = aws_db_instance.main.identifier
  instance_class         = var.replica_instance_class
  publicly_accessible    = false
  
  tags = merge(var.tags, {
    Role = "read-replica"
  })
}

# Root main.tf using conditional modules
module "database" {
  source = "./modules/database"
  
  db_identifier = "${var.project_name}-${var.environment}"
  
  engine         = "mysql"
  engine_version = "8.0"
  instance_class = local.is_production ? "db.t3.medium" : "db.t3.micro"
  
  allocated_storage = local.is_production ? 100 : 20
  storage_encrypted = local.is_production
  
  db_name  = var.db_name
  username = var.db_username
  password = var.db_password
  
  # Conditional read replica
  create_read_replica = local.is_production
  replica_instance_class = "db.t3.small"
  
  # Conditional backup configuration
  backup_config = local.features.backup_enabled ? {
    enabled            = true
    retention_days     = local.backup_config.retention_days
    backup_window      = local.backup_config.backup_window
    maintenance_window = local.backup_config.maintenance_window
  } : {
    enabled            = false
    retention_days     = 0
    backup_window      = ""
    maintenance_window = ""
  }
  
  tags = local.environment_tags
}

# Conditional monitoring module
module "monitoring" {
  count = local.features.logging_enabled ? 1 : 0
  
  source = "./modules/monitoring"
  
  project_name = var.project_name
  environment  = var.environment
  
  # Pass resources to monitor
  database_id      = module.database.db_instance_id
  load_balancer_arn = aws_lb.app.arn
  auto_scaling_group_name = aws_autoscaling_group.app.name
  
  # Monitoring configuration
  log_retention_days = local.monitoring_config.log_retention_days
  alarm_threshold   = local.monitoring_config.alarm_threshold
  
  tags = local.environment_tags
}

Best Practices

1. Readable Conditional Logic

# Good: Clear, readable conditions
locals {
  is_production = var.environment == "prod"
  is_high_security = contains(["enhanced", "strict"], var.security_level)
  
  enable_ssl = (
    var.force_ssl ||
    local.is_production ||
    local.is_high_security
  )
}

# Avoid: Complex nested ternary operators
locals {
  # Don't do this
  instance_type = var.environment == "prod" ? "t3.large" : var.environment == "staging" ? "t3.medium" : var.environment == "dev" ? "t3.micro" : "t3.nano"
  
  # Do this instead
  instance_type = (
    var.environment == "prod" ? "t3.large" :
    var.environment == "staging" ? "t3.medium" :
    var.environment == "dev" ? "t3.micro" :
    "t3.nano"  # default
  )
}

2. Consistent Conditional Patterns

# Establish consistent patterns for feature flags
locals {
  features = {
    ssl_enabled     = var.enable_ssl || local.is_production
    backup_enabled  = var.enable_backup || var.environment != "dev"
    monitoring_enabled = var.enable_monitoring || !local.is_development
  }
  
  # Use the same pattern for all conditional configurations
  ssl_config = local.features.ssl_enabled ? var.ssl_config : null
  backup_config = local.features.backup_enabled ? var.backup_config : null
  monitoring_config = local.features.monitoring_enabled ? var.monitoring_config : null
}

3. Dynamic Block Organization

# Organize dynamic blocks logically
resource "aws_security_group" "app" {
  name_prefix = "${var.project_name}-"
  vpc_id      = var.vpc_id
  
  # Group related dynamic blocks together
  # Application-specific ingress rules
  dynamic "ingress" {
    for_each = var.application_ports
    content {
      from_port   = ingress.value
      to_port     = ingress.value
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
      description = "Application port ${ingress.value}"
    }
  }
  
  # Management ingress rules
  dynamic "ingress" {
    for_each = local.is_production ? [] : var.management_ports
    content {
      from_port   = ingress.value
      to_port     = ingress.value
      protocol    = "tcp"
      cidr_blocks = var.management_cidrs
      description = "Management port ${ingress.value}"
    }
  }
}

Key Takeaways

  1. Conditional Logic: Use ternary operators and conditional expressions for dynamic configurations
  2. Dynamic Blocks: Implement dynamic blocks for flexible resource structures
  3. Feature Flags: Create consistent patterns for feature enablement across environments
  4. Readability: Keep conditional logic clear and well-documented
  5. Consistency: Establish patterns and stick to them throughout your configurations
  6. Environment Awareness: Design configurations that adapt to different environments

Next Steps

  • Tutorial 24: Learn about resource targeting and partial application
  • Practice implementing complex conditional logic patterns
  • Experiment with dynamic blocks for various resource types
  • Review the Terraform Expressions Documentation

Additional Resources