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
Comments (0)
This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!
ReplyLeave a Comment