Wednesday, November 12, 2025

Loops and Conditionals: Dynamic Configurations with Count & For_Each

Loops and Conditionals: Dynamic Configurations with Count & For_Each
Terraform Loops Conditionals Count For_Each Dynamic

Loops and Conditionals: Dynamic Configurations with Count & For_Each

Published on: November 12, 2025 | Author: DevOps Engineering Team

Dynamic Infrastructure with Terraform Loops

Welcome to Part 4 of our Terraform Mastery Series! Learn how to create flexible, scalable infrastructure using Terraform's powerful looping and conditional constructs. Master count, for_each, and conditional expressions to eliminate configuration duplication and build dynamic systems.

Why Use Loops in Terraform?

Loops and conditionals transform static infrastructure configurations into dynamic, scalable systems. Instead of copying and pasting resource blocks, you can create multiple similar resources with a single, maintainable definition.

Benefits of Using Loops

  • DRY Principle: Don't Repeat Yourself
  • Scalability: Easily scale resources up or down
  • Maintainability: One change updates all instances
  • Consistency: Ensure uniform configuration
  • Flexibility: Adapt to different environments

Common Use Cases

  • Multiple similar EC2 instances
  • Creating subnets across availability zones
  • Setting up security group rules
  • Deploying to multiple regions
  • Environment-specific configurations

Terraform Loop Constructs

Terraform provides two main looping mechanisms: count for creating multiple identical resources, and for_each for creating resources from a map or set of strings. Each has its strengths and use cases.

The Count Meta-Argument

The count meta-argument creates multiple resource instances based on a numeric value. It's perfect for creating identical or nearly identical resources.

How Count Works

1
Terraform evaluates the count expression
2
Creates N resource instances (count.index = 0 to N-1)
3
Each instance can reference count.index for uniqueness

Basic Count Example

# Create 3 identical EC2 instances
resource "aws_instance" "web_servers" {
  count = 3  # Creates 3 instances
  
  ami           = "ami-0c02fb55956c7d316"
  instance_type = "t3.micro"
  
  tags = {
    Name = "web-server-${count.index}"  # web-server-0, web-server-1, web-server-2
  }
}

# Output the public IPs of all instances
output "instance_ips" {
  value = aws_instance.web_servers[*].public_ip
}

Result: Creates 3 EC2 instances with names web-server-0, web-server-1, and web-server-2.

Count with Variables

# Define variable for instance count
variable "web_instance_count" {
  description = "Number of web instances to create"
  type        = number
  default     = 2
}

resource "aws_instance" "web" {
  count = var.web_instance_count
  
  ami           = "ami-0c02fb55956c7d316"
  instance_type = "t3.micro"
  subnet_id     = aws_subnet.public[count.index % length(aws_subnet.public)].id
  
  tags = {
    Name    = "${var.environment}-web-${count.index}"
    Role    = "web"
    AZ      = "${count.index % length(var.availability_zones)}"
  }
}

# Create load balancer targeting all instances
resource "aws_lb_target_group_attachment" "web" {
  count = var.web_instance_count
  
  target_group_arn = aws_lb_target_group.web.arn
  target_id        = aws_instance.web[count.index].id
  port             = 80
}

Flexibility: Easily scale instances by changing a single variable value.

Conditional Creation with Count

# Conditionally create monitoring instance
resource "aws_instance" "monitoring" {
  count = var.enable_monitoring ? 1 : 0  # Create 1 if true, 0 if false
  
  ami           = "ami-0c02fb55956c7d316"
  instance_type = "t3.small"
  
  tags = {
    Name = "${var.environment}-monitoring"
    Role = "monitoring"
  }
}

# Create bastion host only in production
resource "aws_instance" "bastion" {
  count = var.environment == "production" ? 1 : 0
  
  ami           = "ami-0c02fb55956c7d316"
  instance_type = "t3.micro"
  
  tags = {
    Name = "${var.environment}-bastion"
  }
}

# Safe reference to conditionally created resource
output "monitoring_ip" {
  value = var.enable_monitoring ? aws_instance.monitoring[0].private_ip : "Not enabled"
}

Conditional Logic: Use ternary operators to control resource creation based on conditions.

The For_Each Meta-Argument

The for_each meta-argument creates resources from a map or set of strings, providing more flexibility and better state management than count.

For_Each with Sets

# Create instances for each environment
resource "aws_instance" "app" {
  for_each = toset(["api", "worker", "cache"])
  
  ami           = "ami-0c02fb55956c7d316"
  instance_type = "t3.micro"
  
  tags = {
    Name = "${var.environment}-${each.key}"
    Role = each.value
  }
}

For_Each with Maps

# Create instances with specific configurations
resource "aws_instance" "servers" {
  for_each = {
    "web" = "t3.micro"
    "app" = "t3.small"
    "db"  = "t3.medium"
  }
  
  ami           = "ami-0c02fb55956c7d316"
  instance_type = each.value  # Use the value from map
  
  tags = {
    Name = "${var.environment}-${each.key}"
    Role = each.key
  }
}

Complex For_Each with Objects

# Define server configurations
locals {
  server_configs = {
    "web-1" = {
      instance_type = "t3.micro"
      subnet       = "public"
      role         = "web"
    }
    "web-2" = {
      instance_type = "t3.micro"
      subnet       = "public"
      role         = "web"
    }
    "app-1" = {
      instance_type = "t3.small"
      subnet       = "private"
      role         = "application"
    }
    "db-1" = {
      instance_type = "t3.medium"
      subnet       = "private"
      role         = "database"
    }
  }
}

# Create instances from configuration map
resource "aws_instance" "servers" {
  for_each = local.server_configs
  
  ami           = "ami-0c02fb55956c7d316"
  instance_type = each.value.instance_type
  subnet_id     = each.value.subnet == "public" ? 
    aws_subnet.public.id : aws_subnet.private.id
  
  tags = {
    Name    = each.key
    Role    = each.value.role
    Subnet  = each.value.subnet
  }
}

# Output specific instance attributes
output "web_server_ips" {
  value = {
    for name, instance in aws_instance.servers :
    name => instance.private_ip
    if instance.tags.Role == "web"
  }
}

Dynamic Subnet Creation

# Create subnets in multiple availability zones
locals {
  availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
}

resource "aws_subnet" "public" {
  for_each = toset(local.availability_zones)
  
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, index(local.availability_zones, each.key))
  availability_zone = each.key
  
  tags = {
    Name = "public-${each.key}"
    Type = "public"
  }
}

resource "aws_subnet" "private" {
  for_each = toset(local.availability_zones)
  
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, index(local.availability_zones, each.key) + 10)
  availability_zone = each.key
  
  tags = {
    Name = "private-${each.key}"
    Type = "private"
  }
}

# Reference specific subnet
resource "aws_instance" "nat" {
  ami           = "ami-0c02fb55956c7d316"
  instance_type = "t3.micro"
  subnet_id     = aws_subnet.public["us-east-1a"].id
}

Count vs For_Each: When to Use Each

Understanding when to use count versus for_each is crucial for effective Terraform configurations.

Feature Count For_Each
Use Case Identical resources Distinct resources with different configurations
Input Type Number Map or set of strings
State Management Index-based (0, 1, 2...) Key-based (more stable)
Adding/Removing Can cause resource recreation More granular, less recreation
Reference Syntax resource.type.name[0] resource.type.name["key"]
Best For Scaling identical resources Multiple similar but distinct resources

Use Count When:

  • Creating multiple identical resources
  • Simple scaling based on a number
  • Conditional resource creation (0 or 1)
  • Resources don't need unique identifiers

Use For_Each When:

  • Resources have different configurations
  • You need stable resource identifiers
  • Working with maps of configuration data
  • Adding/removing specific resources

Avoid This Count Pitfall

When using count, adding or removing items from the middle of the list can cause unexpected resource recreation. for_each with maps provides more stable resource addressing.

Conditional Expressions

Terraform's conditional expressions allow you to make decisions within your configurations, enabling environment-specific logic and feature toggles.

Condition
True Value
False Value
Result

Basic Conditional Expressions

# Simple ternary operator
instance_type = var.environment == "production" ? "m5.large" : "t3.micro"

# Nested conditionals
backup_retention = var.environment == "production" ? 30 : 
                   (var.environment == "staging" ? 7 : 1)

# Conditional with functions
bucket_name = var.use_custom_name ? var.custom_bucket_name : 
  "${random_pet.bucket.id}-${var.environment}"

# In resource blocks
resource "aws_instance" "web" {
  ami           = "ami-0c02fb55956c7d316"
  instance_type = var.environment == "production" ? "m5.large" : "t3.micro"
  monitoring    = var.environment == "production" ? true : false
  
  tags = {
    Environment = var.environment
    Critical    = var.environment == "production" ? "yes" : "no"
  }
}

Advanced Conditional Patterns

# Conditional resource attributes
resource "aws_db_instance" "database" {
  engine               = "mysql"
  instance_class       = var.database_instance_class
  allocated_storage    = var.database_storage
  
  # Only set these in production
  backup_retention_period = var.environment == "production" ? 7 : null
  multi_az               = var.environment == "production" ? true : false
  deletion_protection    = var.environment == "production" ? true : false
}

# Conditional dynamic blocks
resource "aws_security_group" "web" {
  name = "web-sg"
  
  # Only add admin access in non-production
  dynamic "ingress" {
    for_each = var.environment != "production" ? [1] : []
    content {
      description = "Admin SSH"
      from_port   = 22
      to_port     = 22
      protocol    = "tcp"
      cidr_blocks = ["10.0.0.0/8"]
    }
  }
}

# Conditional locals
locals {
  production_settings = {
    min_size     = 3
    max_size     = 10
    desired_size = 5
  }
  non_production_settings = {
    min_size     = 1
    max_size     = 3
    desired_size = 1
  }
  autoscaling_settings = var.environment == "production" ? 
    local.production_settings : local.non_production_settings
}

Dynamic Blocks for Nested Configuration

Dynamic blocks allow you to dynamically construct nested configuration blocks within resources, perfect for security group rules, tags, and other repetitive nested structures.

Dynamic Blocks Interactive Example

See how dynamic blocks transform list/map data into nested configuration:

Select an example to see dynamic blocks in action...

Security Group with Dynamic Blocks

# Define ingress rules as a variable
variable "ingress_rules" {
  type = list(object({
    description = string
    port        = number
    protocol    = string
    cidr_blocks = list(string)
  }))
  default = [
    {
      description = "HTTP"
      port        = 80
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      description = "HTTPS"
      port        = 443
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      description = "SSH"
      port        = 22
      protocol    = "tcp"
      cidr_blocks = ["10.0.0.0/8"]
    }
  ]
}

resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Security group for web servers"
  vpc_id      = aws_vpc.main.id
  
  # Dynamic ingress blocks
  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      description = ingress.value.description
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
    }
  }
  
  # Egress rule
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  
  tags = {
    Name = "web-security-group"
  }
}

Advanced Dynamic Patterns

# Conditional dynamic blocks
resource "aws_launch_template" "web" {
  name          = "web-template"
  image_id      = "ami-0c02fb55956c7d316"
  instance_type = "t3.micro"
  
  # Only add block device mappings if specified
  dynamic "block_device_mappings" {
    for_each = var.ebs_optimized ? [1] : []
    content {
      device_name = "/dev/xvda"
      ebs {
        volume_size = 50
        volume_type = "gp3"
        encrypted   = true
      }
    }
  }
  
  # Dynamic tag specifications
  dynamic "tag_specifications" {
    for_each = ["instance", "volume"]
    content {
      resource_type = tag_specifications.value
      tags = merge(var.common_tags, {
        Name = "${var.environment}-web"
      })
    }
  }
}

# Nested dynamic blocks
resource "aws_network_acl" "main" {
  vpc_id = aws_vpc.main.id
  
  dynamic "egress" {
    for_each = var.network_acls.egress
    content {
      protocol   = egress.value.protocol
      rule_no    = egress.value.rule_no
      action     = egress.value.action
      cidr_block = egress.value.cidr_block
      from_port  = egress.value.from_port
      to_port    = egress.value.to_port
    }
  }
  
  tags = {
    Name = "main"
  }
}

Real-World Examples

Let's look at some practical examples that combine loops, conditionals, and dynamic blocks for real-world scenarios.

Multi-Region Deployment
locals {
  regions = {
    "us-east-1" = "primary"
    "us-west-2" = "secondary"
    "eu-west-1" = "backup"
  }
}

# Create resources in multiple regions
resource "aws_s3_bucket" "cross_region" {
  for_each = local.regions
  
  bucket = "${var.app_name}-${each.key}-${random_pet.bucket.id}"
  region = each.key
  
  tags = {
    Region    = each.key
    Role      = each.value
    App       = var.app_name
  }
}
Environment-Specific Configurations
locals {
  environment_configs = {
    "dev" = {
      instance_type = "t3.micro"
      instance_count = 1
      enable_monitoring = false
    }
    "staging" = {
      instance_type = "t3.small"
      instance_count = 2
      enable_monitoring = true
    }
    "production" = {
      instance_type = "m5.large"
      instance_count = 3
      enable_monitoring = true
    }
  }
  
  config = local.environment_configs[var.environment]
}

# Use environment-specific configuration
resource "aws_instance" "app" {
  count = local.config.instance_count
  
  ami           = "ami-0c02fb55956c7d316"
  instance_type = local.config.instance_type
  monitoring    = local.config.enable_monitoring
  
  tags = {
    Name        = "${var.environment}-app-${count.index}"
    Environment = var.environment
  }
}

Best Practices and Pitfalls

Best Practices

  • Use for_each with maps for stable resource addressing
  • Prefer toset() and tomap() for type conversion
  • Use try() for safe attribute access
  • Validate input variables with condition blocks
  • Use null for optional resource arguments

Common Pitfalls

  • Using count with lists that may change order
  • Not handling empty collections properly
  • Forgetting that count starts at 0
  • Mixing count and for_each in same resource
  • Not testing edge cases (0, 1, many)

Key Takeaways

  • Count creates multiple identical resources using numeric indexing
  • For_Each creates distinct resources from maps or sets with stable addressing
  • Conditional expressions enable environment-specific logic and feature toggles
  • Dynamic blocks generate nested configuration blocks from collections
  • Choose for_each over count when resources have different configurations
  • Always test your loops with edge cases (0, 1, many instances)

In our next tutorial, we'll explore Terraform Data Sources and Dependencies, where you'll learn how to fetch information from existing infrastructure and manage complex dependency relationships.


No comments:

Post a Comment

Loops and Conditionals: Dynamic Configurations with Count & For_Each

Loops and Conditionals: Dynamic Configurations with Count & For_Each ...