Basic Terraform works for simple infrastructure. Advanced Terraform handles complex, dynamic infrastructure with less code. Count creates multiple resources conditionally. for_each iterates over maps and sets. Dynamic blocks generate nested configuration. In this lesson, you'll learn the features that make Terraform truly powerful.

1. Learning Objectives

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

  • Use `count` for conditional resources and loops
  • Use `for_each` with maps and sets
  • Create dynamic blocks for repeated configuration
  • Use Terraform functions (merge, lookup, flatten, etc.)
  • Combine count and for_each in complex scenarios
  • Understand when to use each meta-argument

2. Why This Matters

Real-world scenario: You need to create 50 IAM users with different permissions. Without for_each, you'd copy-paste 50 times. With for_each, you define a map and Terraform creates all 50.

3. Core Concepts

Count Parameter

# Simple count for multiple resources
resource "aws_instance" "web" {
  count = 3

  ami           = data.aws_ami.amazon_linux.id
  instance_type = "t3.micro"

  tags = {
    Name = "web-server-${count.index + 1}"
  }
}

# Conditional count (create only in production)
variable "environment" {
  type = string
}

resource "aws_instance" "production_only" {
  count = var.environment == "production" ? 1 : 0

  ami           = data.aws_ami.amazon_linux.id
  instance_type = "t3.large"
}

# Count with list
variable "server_names" {
  type    = list(string)
  default = ["web", "app", "db"]
}

resource "aws_instance" "named" {
  count = length(var.server_names)

  ami = data.aws_ami.amazon_linux.id
  instance_type = "t3.micro"

  tags = {
    Name = var.server_names[count.index]
  }
}

# Accessing count resources
output "instance_ids" {
  value = aws_instance.named[*].id  # Splat expression
}

output "first_instance_id" {
  value = aws_instance.named[0].id
}
Using count for multiple and conditional resources

For_Each with Maps

# for_each with map - creates resource per map key
variable "servers" {
  type = map(object({
    instance_type = string
    environment   = string
  }))
  default = {
    web = {
      instance_type = "t3.micro"
      environment   = "prod"
    }
    app = {
      instance_type = "t3.small"
      environment   = "prod"
    }
    db = {
      instance_type = "t3.medium"
      environment   = "prod"
    }
  }
}

resource "aws_instance" "servers" {
  for_each = var.servers

  ami           = data.aws_ami.amazon_linux.id
  instance_type = each.value.instance_type

  tags = {
    Name        = each.key
    Environment = each.value.environment
  }
}

# for_each with set
variable "availability_zones" {
  type    = set(string)
  default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}

resource "aws_subnet" "public" {
  for_each = var.availability_zones

  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, index(var.availability_zones, each.key))
  availability_zone = each.key

  tags = {
    Name = "public-${each.key}"
  }
}

# for_each with list (convert to set first)
variable "security_groups" {
  type    = list(string)
  default = ["web-sg", "app-sg", "db-sg"]
}

resource "aws_security_group" "this" {
  for_each = toset(var.security_groups)

  name        = each.key
  description = "Security group for ${each.key}"
  vpc_id      = aws_vpc.main.id
}

# Accessing for_each resources
output "server_ids" {
  value = { for k, instance in aws_instance.servers : k => instance.id }
}
Using for_each with maps and sets

Count vs For_Each Comparison

# Count - Use when:
# - Creating N identical resources
# - Conditional resource (0 or 1)
# - Order matters (array indexing)
resource "aws_instance" "count_example" {
  count = 3  # Creates 3 identical instances
}

# for_each - Use when:
# - Creating resources from a map/set
# - Need stable identifiers
# - Removing an element shouldn't shift indexes
resource "aws_instance" "for_each_example" {
  for_each = toset(["web", "app", "db"])  # Creates 3 named instances
}

# Example: Problem with count when removing from middle
# Count list: ["a", "b", "c"]
# Remove "b" → resources shift: index 1 becomes "c"
# Terraform destroys index 2 and recreates index 1

# For_each solves this with stable identifiers
resource "aws_s3_bucket" "buckets" {
  for_each = toset(["logs", "backups", "static"])
  bucket = "myapp-${each.key}"
}
# Removing "backups" only removes that bucket, no effect on others
Count vs for_each comparison

Dynamic Blocks

# Dynamic blocks for repeated nested configuration
variable "ingress_rules" {
  type = list(object({
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
  }))
  default = [
    {
      from_port   = 80
      to_port     = 80
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      from_port   = 443
      to_port     = 443
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      from_port   = 22
      to_port     = 22
      protocol    = "tcp"
      cidr_blocks = ["10.0.0.0/8"]
    }
  ]
}

resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Web server security group"
  vpc_id      = aws_vpc.main.id

  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
    }
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# Dynamic block with map
variable "tags" {
  type    = map(string)
  default = {
    Environment = "production"
    ManagedBy   = "Terraform"
    CostCenter  = "engineering"
  }
}

resource "aws_instance" "app" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = "t3.micro"

  dynamic "tags" {
    for_each = var.tags
    content {
      key   = tags.key
      value = tags.value
    }
  }
}

# Nested dynamic blocks
variable "routes" {
  type = list(object({
    cidr_block = string
    gateway_id = string
    nat_gateway_id = string
  }))
  default = []
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  dynamic "route" {
    for_each = var.routes
    content {
      cidr_block      = route.value.cidr_block
      gateway_id      = route.value.gateway_id
      nat_gateway_id  = route.value.nat_gateway_id
    }
  }
}
Dynamic blocks for nested configuration

Useful Terraform Functions

# Collection Functions
variable "tags" {
  type = map(string)
  default = {
    Environment = "prod"
    Owner       = "platform"
  }
}

# merge - combine maps
locals {
  default_tags = {
    ManagedBy   = "Terraform"
    Terraform   = "true"
  }
  all_tags = merge(local.default_tags, var.tags)
}

# lookup - safe map access
output "environment" {
  value = lookup(var.tags, "Environment", "dev")
}

# flatten - flatten nested lists
variable "security_groups" {
  type = list(list(string))
  default = [
    ["sg-1", "sg-2"],
    ["sg-3", "sg-4"]
  ]
}

locals {
  flattened_sgs = flatten(var.security_groups)
  # Result: ["sg-1", "sg-2", "sg-3", "sg-4"]
}

# String Functions
locals {
  instance_name = "web-server-01"
  
  # split
  name_parts = split("-", local.instance_name)  # ["web", "server", "01"]
  
  # join
  full_name = join("_", local.name_parts)  # "web_server_01"
  
  # replace
  clean_name = replace(local.instance_name, "/[^a-zA-Z0-9-]/", "-")
  
  # lower/upper
  upper_name = upper(local.instance_name)  # "WEB-SERVER-01"
  lower_name = lower(local.instance_name)  # "web-server-01"
}

# Numeric Functions
locals {
  # max/min
  max_value = max(10, 20, 30)  # 30
  min_value = min(10, 20, 30)  # 10
  
  # ceil/floor
  rounded_up = ceil(5.2)    # 6
  rounded_down = floor(5.8) # 5
}

# Type Conversion Functions
variable "instance_count_string" {
  type    = string
  default = "3"
}

locals {
  instance_count = tonumber(var.instance_count_string)  # 3
  instance_count_string_again = tostring(local.instance_count)  # "3"
  instance_ids = toset(["i-1", "i-2", "i-3"])  # set of strings
}

# File Functions
locals {
  user_data = file("${path.module}/user_data.sh")
  template_rendered = templatefile("${path.module}/user_data.tpl", {
    environment = var.environment
  })
}
Essential Terraform functions

4. Complete Example: Auto Scaling Group with Dynamic Config

# variables.tf
variable "environment" {
  type    = string
  default = "production"
}

variable "instance_types" {
  type    = list(string)
  default = ["t3.micro", "t3.small", "t3.medium"]
}

variable "azs" {
  type    = list(string)
  default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}

variable "tags" {
  type = map(string)
  default = {
    ManagedBy = "Terraform"
  }
}

# main.tf
locals {
  # Determine instance type based on environment
  instance_type = {
    dev        = "t3.micro"
    staging    = "t3.small"
    production = "t3.medium"
  }
  
  selected_type = local.instance_type[var.environment]
  
  # Combine all tags
  all_tags = merge(var.tags, {
    Environment = var.environment
  })
  
  # Number of instances per environment
  instance_count = {
    dev        = 1
    staging    = 2
    production = 3
  }
  
  count = local.instance_count[var.environment]
}

# Launch Template with dynamic user data
data "template_file" "user_data" {
  template = file("${path.module}/user_data.sh")
  vars = {
    environment = var.environment
    app_version = var.app_version
  }
}

resource "aws_launch_template" "app" {
  name_prefix   = "app-${var.environment}"
  image_id      = data.aws_ami.amazon_linux.id
  instance_type = local.selected_type
  user_data     = base64encode(data.template_file.user_data.rendered)

  dynamic "tag_specifications" {
    for_each = ["instance", "volume", "network-interface"]
    content {
      resource_type = tag_specifications.value
      tags = merge(local.all_tags, {
        Name = "app-${var.environment}"
      })
    }
  }

  lifecycle {
    create_before_destroy = true
  }
}

# Auto Scaling Group
resource "aws_autoscaling_group" "app" {
  name               = "app-asg-${var.environment}"
  vpc_zone_identifier = var.subnet_ids
  target_group_arns   = [aws_lb_target_group.app.arn]
  health_check_type   = "ELB"
  min_size           = local.count
  max_size           = local.count * 2
  desired_capacity   = local.count

  launch_template {
    id      = aws_launch_template.app.id
    version = "$Latest"
  }

  tag {
    key                 = "Name"
    value               = "app-${var.environment}"
    propagate_at_launch = true
  }

  dynamic "tag" {
    for_each = local.all_tags
    content {
      key                 = tag.key
      value               = tag.value
      propagate_at_launch = true
    }
  }
}

# Scaling Policy
resource "aws_autoscaling_policy" "scale_up" {
  name                   = "scale-up-${var.environment}"
  scaling_adjustment     = 1
  adjustment_type        = "ChangeInCapacity"
  cooldown               = 300
  autoscaling_group_name = aws_autoscaling_group.app.name
}

resource "aws_autoscaling_policy" "scale_down" {
  name                   = "scale-down-${var.environment}"
  scaling_adjustment     = -1
  adjustment_type        = "ChangeInCapacity"
  cooldown               = 300
  autoscaling_group_name = aws_autoscaling_group.app.name
}

# CloudWatch Alarm for scaling
resource "aws_cloudwatch_metric_alarm" "scale_up" {
  alarm_name          = "scale-up-${var.environment}"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "2"
  metric_name         = "CPUUtilization"
  namespace           = "AWS/EC2"
  period              = "120"
  statistic           = "Average"
  threshold           = "70"
  alarm_description   = "Scale up when CPU > 70%"

  dimensions = {
    AutoScalingGroupName = aws_autoscaling_group.app.name
  }

  alarm_actions = [aws_autoscaling_policy.scale_up.arn]
}

resource "aws_cloudwatch_metric_alarm" "scale_down" {
  alarm_name          = "scale-down-${var.environment}"
  comparison_operator = "LessThanThreshold"
  evaluation_periods  = "2"
  metric_name         = "CPUUtilization"
  namespace           = "AWS/EC2"
  period              = "120"
  statistic           = "Average"
  threshold           = "30"
  alarm_description   = "Scale down when CPU < 30%"

  dimensions = {
    AutoScalingGroupName = aws_autoscaling_group.app.name
  }

  alarm_actions = [aws_autoscaling_policy.scale_down.arn]
}

# Outputs
output "asg_name" {
  value = aws_autoscaling_group.app.name
}

output "instance_count" {
  value = local.count
}

output "instance_type" {
  value = local.selected_type
}
Complete Auto Scaling Group with dynamic configuration

5. Common Errors & Solutions

6. Summary Checklist

7. Next Steps

Next lesson: Terraform Lesson 8.5: Testing Terraform with Terratest

Gataya Med

DevOps Engineer & Backend Developer. Sharing insights on cloud, automation, and scalable systems.

Comments (0)

Sarah Chen February 20, 2025

This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!

Reply

Leave a Comment