Write Once, Deploy Everywhere: Mastering Terraform's Expression Toolkit

Published: (December 7, 2025 at 05:56 PM EST)
4 min read
Source: Dev.to

Source: Dev.to

It’s Day 10 of the AWS Challenge, and today I’m exploring the features that transform Terraform from a simple declarative tool into a flexible, intelligent configuration powerhouse. Yesterday’s lifecycle meta‑arguments gave us surgical control; today’s expressions give us surgical precision with maximum reusability.

We’ll look at Conditional Expressions, Dynamic Blocks, and Splat Expressions—the trio that eliminates code duplication and makes your infrastructure truly adaptable across environments.

Conditional Expressions

Syntax

condition ? true_value : false_value

Terraform evaluates the condition. If true, it uses the first value; otherwise it uses the second.

Real‑World Example

resource "aws_instance" "app_server" {
  ami           = var.ami
  instance_type = var.environment == "production" ? "t3.large" : "t3.micro"

  monitoring = var.environment == "production" ? true : false

  tags = {
    Name        = "${var.environment}-app-server"
    Environment = var.environment
    CostCenter  = var.environment == "production" ? "prod-ops" : "dev-team"
  }
}

Production gets beefier instances with monitoring enabled, while dev environments stay lean—all from a single configuration.

Use Cases

  • Selecting instance sizes based on environment
  • Enabling expensive features (e.g., enhanced monitoring) only in production
  • Choosing different AMIs per region
  • Setting resource counts (scale production higher than dev)
  • Applying environment‑specific security policies

Pro Tip: If you find yourself chaining multiple ternary operators, break the logic out into locals blocks for readability.

Dynamic Blocks

Syntax

dynamic "block_name" {
  for_each = var.collection
  content {
    # Configuration using block_name.value
  }
}

Note: for_each on a resource creates multiple resources. Dynamic blocks create multiple nested blocks within a single resource.

Real‑World Example: Security Group Rules

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

resource "aws_security_group" "app_sg" {
  name = "app-security-group"

  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
      description = ingress.value.description
    }
  }
}

Why This Is Brilliant

  • Add a new rule by updating the variable—no code changes needed.
  • Provide environment‑specific rule sets by passing different lists.
  • Disable all external access with an empty list.
  • Audit open ports by inspecting a single variable.

Other Powerful Use Cases

  • Attaching multiple EBS volumes to EC2 instances
  • Building IAM policy statements
  • Populating route‑table routes
  • Defining CloudWatch alarm actions
  • Any nested block that repeats with variation

Important: Dynamic blocks work only for nested blocks within a resource. To create multiple resources, use count or for_each on the resource itself.

Splat Expressions

Syntax

resource_list[*].attribute_name

Real‑World Example

resource "aws_instance" "web_servers" {
  count         = 3
  ami           = var.ami_id
  instance_type = "t3.micro"

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

# Output all instance IDs in one line
output "instance_ids" {
  value = aws_instance.web_servers[*].id
}

# Output all private IPs
output "private_ips" {
  value = aws_instance.web_servers[*].private_ip
}

# Use those IPs in another resource
resource "aws_lb_target_group_attachment" "web" {
  count            = length(aws_instance.web_servers)
  target_group_arn = aws_lb_target_group.web.arn
  target_id        = aws_instance.web_servers[*].id[count.index]
}

Without splat expressions you’d need a loop or repetitive code; with them it’s a single elegant line.

Common Use Cases

  • Extracting IDs from multiple EC2 instances for downstream resources
  • Collecting subnet IDs from a VPC data source
  • Getting security‑group IDs to attach to resources
  • Gathering ARNs for IAM policies
  • Building lists for outputs and dependencies

Pro Tip: Combine splat with functions like join() for extra power:

output "all_ips_comma_separated" {
  value = join(",", aws_instance.web_servers[*].private_ip)
}

Putting It All Together

Below is a production‑grade configuration that demonstrates how Conditional Expressions, Dynamic Blocks, and Splat Expressions complement each other.

variable "environment" {
  type = string
}

variable "ingress_rules" {
  type = map(object({
    port        = number
    cidr_blocks = list(string)
  }))
}

# Conditional: Different instance counts and types per environment
resource "aws_instance" "app" {
  count         = var.environment == "production" ? 3 : 1
  ami           = var.ami_id
  instance_type = var.environment == "production" ? "t3.large" : "t3.micro"

  vpc_security_group_ids = [aws_security_group.app.id]

  tags = {
    Name        = "${var.environment}-app-${count.index}"
    Environment = var.environment
  }
}

# Dynamic: Generate security rules from a variable
resource "aws_security_group" "app" {
  name = "${var.environment}-app-sg"

  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = "tcp"
      cidr_blocks = ingress.value.cidr_blocks
    }
  }
}

# Splat: Extract all instance IDs for output
output "instance_ids" {
  description = "IDs of all app instances"
  value       = aws_instance.app[*].id
}

# Splat: Get all private IPs for load balancer configuration
output "private_ips" {
  description = "Private IPs for load balancer config"
  value       = aws_instance.app[*].private_ip
}
  • Conditional expressions give you environment‑aware configurations without file duplication.
  • Dynamic blocks eliminate repetitive nested blocks, making security groups and policies data‑driven and maintainable.
  • Splat expressions make data extraction effortless, removing the need for manual loops.

Happy Terraforming! 🚀

Back to Blog

Related posts

Read more »

Day 8 - Terraform Meta-Arguments

Whenever we create any resource using Terraform—whether it is an S3 bucket, an EC2 instance, or a security group—we have to pass certain arguments that are spec...