Fatskills
Practice. Master. Repeat.
Study Guide: TECH **Terraform Dynamic Blocks & Conditional Expressions: Zero-Fluff Study Guide**
Source: https://www.fatskills.com/cloud-application-developer/chapter/tech-terraform-dynamic-blocks-conditional-expressions-zero-fluff-study-guide

TECH **Terraform Dynamic Blocks & Conditional Expressions: Zero-Fluff Study Guide**

By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.

⏱️ ~9 min read

Terraform Dynamic Blocks & Conditional Expressions: Zero-Fluff Study Guide

For engineers who need to write clean, maintainable IaC—fast.


1. What This Is & Why It Matters

You’re managing a Terraform module for an AWS ALB (Application Load Balancer). The team needs to support two environments: - Dev: No WAF (Web Application Firewall) rules.
- Prod: Strict WAF rules (block SQLi, XSS, and rate-limiting).

Without dynamic blocks and conditional expressions, you’d have to: - Copy-paste the entire aws_lb_listener block twice (once for dev, once for prod).
- Manually comment/uncomment WAF rules when switching environments.
- Risk drift when someone forgets to update both blocks.

Dynamic blocks let you generate nested configurations dynamically (e.g., WAF rules, security group rules, IAM policies).
Conditional expressions let you toggle resources or attributes on/off (e.g., "Only create a NAT Gateway if var.env == "prod"").

Why this matters in production:
- DRY (Don’t Repeat Yourself): One block, many configurations. Change once, deploy everywhere.
- Safety: No manual edits = fewer human errors.
- Scalability: Add 10 WAF rules? Just update a list variable—no code changes.
- Auditability: Terraform plan shows exactly what will change (e.g., "Adding 3 WAF rules").

Real-world scenario:
You inherit a Terraform repo where every aws_security_group has 20+ hardcoded rules. A new compliance requirement adds 5 more rules—but only for prod. With dynamic blocks, you refactor the SG in 10 minutes. Without them? You’d spend hours copy-pasting and risk breaking dev.


2. Core Concepts & Components


Dynamic Blocks

  • What: A way to generate repeated nested blocks (e.g., ingress, egress, rule) from a list or map.
  • Production insight: If you’re manually writing 5+ nested blocks (e.g., security group rules), you’re doing it wrong. Dynamic blocks scale to 100+ rules without bloating code.
  • Syntax: hcl dynamic "<BLOCK_TYPE>" {
    for_each = <COLLECTION> # list, map, or set
    content {
    <ATTRIBUTE> = <VALUE> # Use `each.key` or `each.value` to reference the collection
    } }

Conditional Expressions

  • What: A ternary operator (condition ? true_val : false_val) to toggle resources/attributes.
  • Production insight: Use this to avoid creating expensive resources (e.g., NAT Gateways, RDS instances) in non-prod environments.
  • Syntax: hcl count = var.env == "prod" ? 1 : 0 # Creates 1 resource in prod, 0 otherwise

for_each vs count

  • for_each:
  • Best for dynamic blocks or multiple resources (e.g., 10 S3 buckets from a map).
  • Creates a map of resources (keys = unique identifiers).
  • Production insight: Use when you need to reference individual resources later (e.g., aws_s3_bucket.example["logs"].arn).
  • count:
  • Best for conditional creation (e.g., "Create this resource only in prod").
  • Creates a list of resources (indexed by count.index).
  • ⚠️ Trap: Avoid count for dynamic blocks—it can cause resource recreation if the list order changes.

each.key and each.value

  • What: Variables available inside dynamic blocks to reference the current item in the collection.
  • Production insight: Use each.key for unique identifiers (e.g., rule names) and each.value for attributes (e.g., port numbers).

Splat Expressions ([*])

  • What: A shorthand to extract attributes from a list of resources (e.g., aws_instance.web[*].private_ip).
  • Production insight: Useful for outputs (e.g., "Get all private IPs of EC2 instances").

try() and can() Functions

  • try(): Returns the first non-error value (e.g., try(var.port, 80)).
  • can(): Returns true if the expression succeeds (e.g., can(aws_s3_bucket.example)).
  • Production insight: Use try() to handle optional variables gracefully (e.g., "If var.waf_rules is undefined, use an empty list").

Terraform Workspaces

  • What: A way to manage environment-specific state (e.g., dev, prod).
  • Production insight: Combine with dynamic blocks to avoid hardcoding environment names in variables.


3. Step-by-Step Hands-On: Dynamic WAF Rules for an ALB


Prerequisites

  • AWS account with admin IAM permissions.
  • Terraform v1.0+ installed.
  • Basic familiarity with aws_lb and aws_wafv2_web_acl.

Goal

Deploy an ALB with: - Dev: No WAF rules.
- Prod: 3 WAF rules (SQLi, XSS, rate-limiting).

Step 1: Define Variables

Create variables.tf:


variable "env" {
  description = "Environment (dev or prod)"
  type        = string
  default     = "dev"
}

variable "waf_rules" {
  description = "List of WAF rules (only used in prod)"
  type = list(object({
name = string
priority = number
action = string # "ALLOW", "BLOCK", "COUNT"
rule = string # "AWS-AWSManagedRulesSQLiRuleSet", etc.
})) default = [] }

Step 2: Create the ALB with Dynamic WAF Rules

Create main.tf:


resource "aws_lb" "example" {
  name               = "example-alb-${var.env}"
  internal           = false
  load_balancer_type = "application"
  subnets            = ["subnet-12345678", "subnet-87654321"]  # Replace with your subnets
}

resource "aws_wafv2_web_acl" "example" {
  count = var.env == "prod" ? 1 : 0  # Only create in prod
  name  = "example-waf-${var.env}"

  scope = "REGIONAL"

  default_action {
allow {} } dynamic "rule" {
for_each = var.waf_rules
content {
name = rule.value.name
priority = rule.value.priority
action {
dynamic "block" {
for_each = rule.value.action == "BLOCK" ? [1] : []
content {}
}
dynamic "allow" {
for_each = rule.value.action == "ALLOW" ? [1] : []
content {}
}
dynamic "count" {
for_each = rule.value.action == "COUNT" ? [1] : []
content {}
}
}
statement {
managed_rule_group_statement {
name = rule.value.rule
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = rule.value.name
sampled_requests_enabled = true
}
} } visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "example-waf-${var.env}"
sampled_requests_enabled = true } } resource "aws_wafv2_web_acl_association" "example" { count = var.env == "prod" ? 1 : 0 # Only associate in prod resource_arn = aws_lb.example.arn web_acl_arn = aws_wafv2_web_acl.example[0].arn }

Step 3: Deploy to Dev (No WAF)

Run:


terraform init
terraform plan -var="env=dev"

Expected output: - ALB is created.
- No WAF resources are created.

Step 4: Deploy to Prod (With WAF Rules)

Create prod.tfvars:


env = "prod"
waf_rules = [
  {
name = "AWS-AWSManagedRulesSQLiRuleSet"
priority = 1
action = "BLOCK"
rule = "AWS-AWSManagedRulesSQLiRuleSet" }, {
name = "AWS-AWSManagedRulesCommonRuleSet"
priority = 2
action = "BLOCK"
rule = "AWS-AWSManagedRulesCommonRuleSet" }, {
name = "RateLimit100"
priority = 3
action = "BLOCK"
rule = "RateLimit100" # Custom rule (not shown here) } ]

Run:


terraform plan -var-file="prod.tfvars"

Expected output: - ALB is created.
- WAF ACL with 3 rules is created and associated with the ALB.

Step 5: Verify in AWS Console

  1. Go to AWS WAF & Shield > Web ACLs.
  2. Confirm example-waf-prod exists with 3 rules.
  3. Go to EC2 > Load Balancers and check the ALB’s Web ACL tab.

4. ? Production-Ready Best Practices


Security

  • Least privilege: Use for_each with IAM policies to avoid over-permissive roles.
    hcl dynamic "statement" {
    for_each = var.iam_policies
    content {
    actions = statement.value.actions
    resources = statement.value.resources
    } }
  • Secrets: Never hardcode secrets in dynamic blocks. Use aws_secretsmanager_secret_version or vault_generic_secret.

Cost Optimization

  • Conditional resources: Use count or for_each to avoid creating expensive resources (e.g., NAT Gateways, RDS) in non-prod.
    hcl resource "aws_nat_gateway" "example" {
    count = var.env == "prod" ? 1 : 0 }
  • Dynamic sizing: Use try() to set instance sizes based on environment.
    hcl instance_type = try(var.instance_sizes[var.env], "t3.micro")

Reliability & Maintainability

  • Naming conventions: Include each.key in resource names for uniqueness.
    hcl name = "sg-${var.env}-${each.key}"
  • Tagging: Dynamically tag resources with for_each.
    hcl tags = merge(var.tags, { Name = "resource-${each.key}" })
  • Idempotency: Use for_each with maps (not lists) to avoid resource recreation when order changes.

Observability

  • CloudWatch alarms: Dynamically create alarms for resources.
    hcl dynamic "alarm" {
    for_each = var.monitored_resources
    content {
    alarm_name = "alarm-${alarm.key}"
    metric_name = alarm.value.metric
    } }
  • Logging: Enable access logs for ALBs/S3 buckets conditionally.
    hcl enable_access_logs = var.env == "prod" ? true : false


5. ⚠️ Common Mistakes & Traps

Mistake Symptom Fix/Prevention
Using count for dynamic blocks Terraform recreates resources if list order changes. Use for_each with maps or sets instead.
Forgetting each.key in resource names Duplicate resource names (e.g., aws_s3_bucket.example). Always include each.key in names (e.g., name = "bucket-${each.key}").
Hardcoding environment names Can’t reuse modules across environments. Use var.env or Terraform workspaces.
Not using try() for optional vars Terraform fails if a variable is undefined. Wrap optional variables in try() (e.g., try(var.waf_rules, [])).
Overusing dynamic blocks Code becomes unreadable. Limit dynamic blocks to 3+ repeated blocks. For 1-2, hardcode.


6. ? Exam/Certification Focus


Typical Question Patterns

  1. Dynamic blocks:
  2. "How would you create 10 security group rules from a list variable?"
    Answer: Use dynamic "ingress" with for_each = var.rules.
  3. Conditional expressions:
  4. "How would you create a NAT Gateway only in prod?"
    Answer: count = var.env == "prod" ? 1 : 0.
  5. for_each vs count:
  6. "When would you use for_each instead of count?"
    Answer: When you need to reference individual resources later (e.g., aws_instance.web["app1"].id).

Key ⚠️ Trap Distinctions

  • count vs for_each:
  • count creates a list of resources (indexed by count.index).
  • for_each creates a map of resources (keyed by each.key).
  • Exam trap: If the question mentions "recreating resources when order changes," the answer is for_each.

  • Dynamic blocks vs for_each:

  • Dynamic blocks generate nested blocks (e.g., ingress rules).
  • for_each generates top-level resources (e.g., aws_instance).
  • Exam trap: If the question asks about "repeated nested configurations," the answer is dynamic blocks.

Scenario-Based Question

"You need to deploy an ALB with WAF rules only in prod. The WAF rules are defined in a list variable. How would you implement this?" Answer: 1. Use count = var.env == "prod" ? 1 : 0 for the WAF ACL.
2. Use dynamic "rule" with for_each = var.waf_rules to generate rules.


7. ? Hands-On Challenge

Challenge: Write a Terraform module that creates: - 1 EC2 instance in dev.
- 3 EC2 instances in prod (with unique names).
- A security group with dynamic ingress rules (ports 22, 80, 443).

Solution:


variable "env" {
  type = string
}

variable "ingress_ports" {
  type    = list(number)
  default = [22, 80, 443]
}

resource "aws_instance" "example" {
  for_each = var.env == "prod" ? { for i in range(3) : "instance-${i}" => i } : { "instance-0" = 0 }

  ami           = "ami-0c55b159cbfafe1f0"  # Amazon Linux 2
  instance_type = "t3.micro"
  tags = {
Name = each.key } } resource "aws_security_group" "example" { name = "sg-${var.env}" dynamic "ingress" {
for_each = var.ingress_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
} } }

Why it works: - for_each creates 1 instance in dev (instance-0) and 3 in prod (instance-0, instance-1, instance-2).
- Dynamic ingress block generates rules for all ports in var.ingress_ports.


8. ? Rapid-Reference Crib Sheet

Concept Syntax Example
Dynamic block dynamic "<BLOCK>" { for_each = <COLLECTION> content { ... } } dynamic "ingress" { for_each = var.ports content { from_port = each.value } }
Conditional expression condition ? true_val : false_val count = var.env == "prod" ? 1 : 0
for_each for_each = <MAP_OR_SET> for_each = var.instances
each.key / each.value Reference current item in for_each name = "resource-${each.key}"
try() try(<EXPRESSION>, <DEFAULT>) try(var.port, 80)
Splat expression <RESOURCE>[*].<ATTRIBUTE> aws_instance.web[*].private_ip
⚠️ count trap Avoid count for dynamic blocks (use for_each). count = length(var.ports) → ✅ for_each = toset(var.ports)
⚠️ Default WAF action Default WAF action is ALLOW. Override with block for security. default_action { block {} }


9. ? Where to Go Next

  1. Terraform Dynamic Blocks Docs
  2. Terraform Conditional Expressions
  3. AWS WAFv2 Terraform Module
  4. Terraform Best Practices (Gruntwork)


ADVERTISEMENT