Tutorial 23: Conditional Expressions and Dynamic Blocks
Learning Objectives
By the end of this tutorial, you will be able to:
- Use conditional expressions for dynamic resource configuration
- Implement dynamic blocks for flexible resource structures
- Apply conditional logic in variable validation and outputs
- Create adaptive configurations based on environment and feature flags
- Design maintainable conditional infrastructure patterns
Prerequisites
- Understanding of HCL expressions and Terraform functions
- Completed Tutorial 22: Built-in Functions and Expressions
- Knowledge of Terraform resource configuration
Introduction
Conditional expressions and dynamic blocks enable you to create flexible, adaptive Terraform configurations that respond to different environments, feature flags, and runtime conditions. These features allow your infrastructure code to be more maintainable and reusable across various scenarios.
Conditional Expressions
Basic Conditional Syntax
# Basic ternary operator: condition ? true_value : false_value
# variables.tf
variable "environment" {
description = "Environment name"
type = string
default = "dev"
}
variable "enable_monitoring" {
description = "Enable monitoring features"
type = bool
default = null # null means auto-determine based on environment
}
variable "instance_count" {
description = "Number of instances"
type = number
default = null
}
# main.tf
locals {
# Simple conditionals
is_production = var.environment == "prod"
is_development = var.environment == "dev"
# Conditional values with defaults
monitoring_enabled = var.enable_monitoring != null ? var.enable_monitoring : local.is_production
# Multi-condition expressions
instance_type = (
var.environment == "prod" ? "t3.large" :
var.environment == "staging" ? "t3.medium" :
"t3.micro"
)
# Conditional instance count
default_instance_count = (
local.is_production ? 3 :
var.environment == "staging" ? 2 : 1
)
final_instance_count = var.instance_count != null ? var.instance_count : local.default_instance_count
# Conditional resource naming
name_prefix = local.is_production ? var.project_name : "${var.project_name}-${var.environment}"
# Conditional tags
environment_tags = local.is_production ? {
Environment = "production"
Critical = "true"
Backup = "enabled"
} : {
Environment = var.environment
Critical = "false"
Backup = "disabled"
}
}
# Conditional resource creation
resource "aws_instance" "app" {
count = local.final_instance_count
ami = data.aws_ami.ubuntu.id
instance_type = local.instance_type
monitoring = local.monitoring_enabled
tags = merge({
Name = "${local.name_prefix}-app-${count.index + 1}"
}, local.environment_tags)
}
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}
}
Complex Conditional Logic
# variables.tf
variable "features" {
description = "Feature flags"
type = object({
enable_ssl = bool
enable_cdn = bool
enable_waf = bool
enable_logging = bool
enable_backup = bool
})
default = {
enable_ssl = false
enable_cdn = false
enable_waf = false
enable_logging = false
enable_backup = false
}
}
variable "security_level" {
description = "Security level: basic, standard, or enhanced"
type = string
default = "basic"
validation {
condition = contains(["basic", "standard", "enhanced"], var.security_level)
error_message = "Security level must be basic, standard, or enhanced."
}
}
# main.tf
locals {
# Conditional feature enabling based on environment and security level
features = {
ssl_enabled = (
var.features.enable_ssl ||
local.is_production ||
contains(["standard", "enhanced"], var.security_level)
)
cdn_enabled = (
var.features.enable_cdn ||
(local.is_production && var.security_level == "enhanced")
)
waf_enabled = (
var.features.enable_waf ||
var.security_level == "enhanced"
)
logging_enabled = (
var.features.enable_logging ||
!local.is_development
)
backup_enabled = (
var.features.enable_backup ||
var.environment != "dev"
)
}
# Conditional SSL certificate configuration
ssl_config = local.features.ssl_enabled ? {
certificate_arn = var.existing_cert_arn != null ? var.existing_cert_arn : aws_acm_certificate.app[0].arn
ssl_policy = var.security_level == "enhanced" ? "ELBSecurityPolicy-TLS-1-2-2017-01" : "ELBSecurityPolicy-2016-08"
redirect_http = true
} : {
certificate_arn = null
ssl_policy = null
redirect_http = false
}
# Conditional backup configuration
backup_config = local.features.backup_enabled ? {
retention_days = (
local.is_production ? 30 :
var.environment == "staging" ? 7 : 1
)
backup_window = local.is_production ? "03:00-04:00" : "02:00-03:00"
maintenance_window = local.is_production ? "sun:04:00-sun:05:00" : "sat:03:00-sat:04:00"
} : null
# Conditional monitoring configuration
monitoring_config = local.features.logging_enabled ? {
log_retention_days = (
local.is_production ? 90 :
var.environment == "staging" ? 30 : 7
)
detailed_monitoring = local.is_production || var.security_level == "enhanced"
alarm_threshold = local.is_production ? 80 : 90
} : null
}
# Conditional SSL certificate
resource "aws_acm_certificate" "app" {
count = local.features.ssl_enabled && var.existing_cert_arn == null ? 1 : 0
domain_name = var.domain_name
subject_alternative_names = ["*.${var.domain_name}"]
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
tags = {
Name = "${local.name_prefix}-certificate"
}
}
# Conditional CloudFront distribution
resource "aws_cloudfront_distribution" "app" {
count = local.features.cdn_enabled ? 1 : 0
origin {
domain_name = aws_lb.app.dns_name
origin_id = "ALB-${local.name_prefix}"
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = local.features.ssl_enabled ? "https-only" : "http-only"
origin_ssl_protocols = local.features.ssl_enabled ? ["TLSv1.2"] : ["TLSv1"]
}
}
enabled = true
default_cache_behavior {
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "ALB-${local.name_prefix}"
compress = true
viewer_protocol_policy = local.features.ssl_enabled ? "redirect-to-https" : "allow-all"
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
acm_certificate_arn = local.features.ssl_enabled ? local.ssl_config.certificate_arn : null
cloudfront_default_certificate = !local.features.ssl_enabled
ssl_support_method = local.features.ssl_enabled ? "sni-only" : null
}
web_acl_id = local.features.waf_enabled ? aws_wafv2_web_acl.app[0].arn : null
tags = {
Name = "${local.name_prefix}-cdn"
}
}
# Conditional WAF
resource "aws_wafv2_web_acl" "app" {
count = local.features.waf_enabled ? 1 : 0
name = "${local.name_prefix}-waf"
scope = "CLOUDFRONT"
default_action {
allow {}
}
rule {
name = "RateLimitRule"
priority = 1
action {
block {}
}
statement {
rate_based_statement {
limit = var.security_level == "enhanced" ? 2000 : 10000
aggregate_key_type = "IP"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${local.name_prefix}-RateLimit"
sampled_requests_enabled = true
}
}
tags = {
Name = "${local.name_prefix}-waf"
}
}
Dynamic Blocks
Basic Dynamic Block Usage
# variables.tf
variable "ingress_rules" {
description = "List of ingress rules"
type = list(object({
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
description = string
}))
default = [
{
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTP"
},
{
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTPS"
}
]
}
variable "egress_rules" {
description = "List of egress rules"
type = list(object({
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
description = string
}))
default = [
{
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
description = "All outbound traffic"
}
]
}
# main.tf
resource "aws_security_group" "app" {
name_prefix = "${local.name_prefix}-"
vpc_id = aws_vpc.main.id
description = "Security group for ${local.name_prefix}"
# Dynamic ingress rules
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
description = ingress.value.description
}
}
# Dynamic egress rules
dynamic "egress" {
for_each = var.egress_rules
content {
from_port = egress.value.from_port
to_port = egress.value.to_port
protocol = egress.value.protocol
cidr_blocks = egress.value.cidr_blocks
description = egress.value.description
}
}
tags = {
Name = "${local.name_prefix}-security-group"
}
}
Conditional Dynamic Blocks
# variables.tf
variable "load_balancer_config" {
description = "Load balancer configuration"
type = object({
enable_access_logs = bool
enable_waf = bool
ssl_certificates = list(string)
custom_headers = map(string)
})
default = {
enable_access_logs = false
enable_waf = false
ssl_certificates = []
custom_headers = {}
}
}
variable "auto_scaling_policies" {
description = "Auto scaling policies"
type = list(object({
name = string
policy_type = string
adjustment_type = string
scaling_adjustment = number
cooldown = number
}))
default = []
}
# main.tf
resource "aws_lb" "app" {
name = "${local.name_prefix}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.app.id]
subnets = aws_subnet.public[*].id
enable_deletion_protection = local.is_production
# Conditional access logs
dynamic "access_logs" {
for_each = var.load_balancer_config.enable_access_logs ? [1] : []
content {
bucket = aws_s3_bucket.lb_logs[0].bucket
prefix = local.name_prefix
enabled = true
}
}
tags = {
Name = "${local.name_prefix}-load-balancer"
}
}
# Conditional S3 bucket for access logs
resource "aws_s3_bucket" "lb_logs" {
count = var.load_balancer_config.enable_access_logs ? 1 : 0
bucket = "${local.name_prefix}-lb-logs-${random_id.bucket_suffix.hex}"
force_destroy = !local.is_production
tags = {
Name = "${local.name_prefix}-lb-logs"
Purpose = "load-balancer-logs"
}
}
# Load balancer listener with conditional SSL
resource "aws_lb_listener" "app" {
load_balancer_arn = aws_lb.app.arn
port = local.features.ssl_enabled ? "443" : "80"
protocol = local.features.ssl_enabled ? "HTTPS" : "HTTP"
ssl_policy = local.features.ssl_enabled ? local.ssl_config.ssl_policy : null
certificate_arn = local.features.ssl_enabled ? local.ssl_config.certificate_arn : null
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
# Dynamic additional certificates
dynamic "certificate" {
for_each = local.features.ssl_enabled ? var.load_balancer_config.ssl_certificates : []
content {
certificate_arn = certificate.value
}
}
}
# Auto scaling group with dynamic policies
resource "aws_autoscaling_group" "app" {
name = "${local.name_prefix}-asg"
vpc_zone_identifier = aws_subnet.private[*].id
target_group_arns = [aws_lb_target_group.app.arn]
health_check_type = "ELB"
min_size = local.final_instance_count
max_size = local.final_instance_count * 3
desired_capacity = local.final_instance_count
launch_template {
id = aws_launch_template.app.id
version = "$Latest"
}
# Dynamic tags
dynamic "tag" {
for_each = merge({
Name = "${local.name_prefix}-asg"
}, local.environment_tags)
content {
key = tag.key
value = tag.value
propagate_at_launch = true
}
}
}
# Dynamic auto scaling policies
resource "aws_autoscaling_policy" "app" {
for_each = {
for policy in var.auto_scaling_policies :
policy.name => policy
}
name = "${local.name_prefix}-${each.key}"
scaling_adjustment = each.value.scaling_adjustment
adjustment_type = each.value.adjustment_type
cooldown = each.value.cooldown
autoscaling_group_name = aws_autoscaling_group.app.name
policy_type = each.value.policy_type
}
resource "random_id" "bucket_suffix" {
byte_length = 4
}
Advanced Dynamic Block Patterns
# variables.tf
variable "applications" {
description = "Application configurations"
type = map(object({
port = number
protocol = string
health_path = string
environments = map(object({
replicas = number
cpu_request = string
memory_request = string
env_vars = map(string)
volumes = list(object({
name = string
mount_path = string
size = string
type = string
}))
init_containers = list(object({
name = string
image = string
command = list(string)
env = map(string)
}))
}))
}))
default = {
frontend = {
port = 80
protocol = "HTTP"
health_path = "/health"
environments = {
dev = {
replicas = 1
cpu_request = "100m"
memory_request = "128Mi"
env_vars = {
NODE_ENV = "development"
LOG_LEVEL = "debug"
}
volumes = [
{
name = "logs"
mount_path = "/var/log"
size = "1Gi"
type = "emptyDir"
}
]
init_containers = [
{
name = "db-migrate"
image = "migrate/migrate"
command = ["migrate", "-path", "/migrations", "up"]
env = {
DB_URL = "postgres://localhost/app"
}
}
]
}
}
}
}
}
# main.tf
locals {
# Flatten applications and environments for iteration
app_deployments = flatten([
for app_name, app_config in var.applications : [
for env_name, env_config in app_config.environments : {
app_name = app_name
environment = env_name
key = "${app_name}-${env_name}"
app_config = app_config
env_config = env_config
}
]
])
}
# Kubernetes deployment with complex dynamic blocks
resource "kubernetes_deployment" "apps" {
for_each = {
for deployment in local.app_deployments :
deployment.key => deployment
}
metadata {
name = each.value.key
namespace = each.value.environment
labels = {
app = each.value.app_name
environment = each.value.environment
}
}
spec {
replicas = each.value.env_config.replicas
selector {
match_labels = {
app = each.value.app_name
environment = each.value.environment
}
}
template {
metadata {
labels = {
app = each.value.app_name
environment = each.value.environment
}
}
spec {
# Dynamic init containers
dynamic "init_container" {
for_each = each.value.env_config.init_containers
content {
name = init_container.value.name
image = init_container.value.image
command = init_container.value.command
# Dynamic environment variables for init containers
dynamic "env" {
for_each = init_container.value.env
content {
name = env.key
value = env.value
}
}
}
}
container {
name = each.value.app_name
image = "${each.value.app_name}:latest"
port {
container_port = each.value.app_config.port
}
resources {
requests = {
cpu = each.value.env_config.cpu_request
memory = each.value.env_config.memory_request
}
}
# Dynamic environment variables
dynamic "env" {
for_each = each.value.env_config.env_vars
content {
name = env.key
value = env.value
}
}
# Dynamic volume mounts
dynamic "volume_mount" {
for_each = each.value.env_config.volumes
content {
name = volume_mount.value.name
mount_path = volume_mount.value.mount_path
}
}
liveness_probe {
http_get {
path = each.value.app_config.health_path
port = each.value.app_config.port
}
initial_delay_seconds = 30
period_seconds = 10
}
}
# Dynamic volumes
dynamic "volume" {
for_each = each.value.env_config.volumes
content {
name = volume.value.name
# Conditional volume types
dynamic "empty_dir" {
for_each = volume.value.type == "emptyDir" ? [1] : []
content {
size_limit = volume.value.size
}
}
dynamic "persistent_volume_claim" {
for_each = volume.value.type == "pvc" ? [1] : []
content {
claim_name = "${each.value.key}-${volume.value.name}-pvc"
}
}
}
}
}
}
}
}
# Conditional PVCs for persistent volumes
resource "kubernetes_persistent_volume_claim" "app_volumes" {
for_each = {
for volume in flatten([
for deployment in local.app_deployments : [
for vol in deployment.env_config.volumes : {
key = "${deployment.key}-${vol.name}-pvc"
volume = vol
deployment = deployment
}
if vol.type == "pvc"
]
]) : volume.key => volume
}
metadata {
name = each.key
namespace = each.value.deployment.environment
}
spec {
access_modes = ["ReadWriteOnce"]
resources {
requests = {
storage = each.value.volume.size
}
}
}
}
Conditional Module Usage
# modules/database/main.tf
variable "create_read_replica" {
description = "Whether to create read replica"
type = bool
default = false
}
variable "backup_config" {
description = "Backup configuration"
type = object({
enabled = bool
retention_days = number
backup_window = string
maintenance_window = string
})
default = {
enabled = false
retention_days = 0
backup_window = ""
maintenance_window = ""
}
}
resource "aws_db_instance" "main" {
identifier = var.db_identifier
engine = var.engine
engine_version = var.engine_version
instance_class = var.instance_class
allocated_storage = var.allocated_storage
storage_encrypted = var.storage_encrypted
db_name = var.db_name
username = var.username
password = var.password
# Conditional backup configuration
backup_retention_period = var.backup_config.enabled ? var.backup_config.retention_days : 0
backup_window = var.backup_config.enabled ? var.backup_config.backup_window : null
maintenance_window = var.backup_config.enabled ? var.backup_config.maintenance_window : null
skip_final_snapshot = !var.backup_config.enabled
final_snapshot_identifier = var.backup_config.enabled ? "${var.db_identifier}-final-snapshot" : null
tags = var.tags
}
resource "aws_db_instance" "read_replica" {
count = var.create_read_replica ? 1 : 0
identifier = "${var.db_identifier}-replica"
replicate_source_db = aws_db_instance.main.identifier
instance_class = var.replica_instance_class
publicly_accessible = false
tags = merge(var.tags, {
Role = "read-replica"
})
}
# Root main.tf using conditional modules
module "database" {
source = "./modules/database"
db_identifier = "${var.project_name}-${var.environment}"
engine = "mysql"
engine_version = "8.0"
instance_class = local.is_production ? "db.t3.medium" : "db.t3.micro"
allocated_storage = local.is_production ? 100 : 20
storage_encrypted = local.is_production
db_name = var.db_name
username = var.db_username
password = var.db_password
# Conditional read replica
create_read_replica = local.is_production
replica_instance_class = "db.t3.small"
# Conditional backup configuration
backup_config = local.features.backup_enabled ? {
enabled = true
retention_days = local.backup_config.retention_days
backup_window = local.backup_config.backup_window
maintenance_window = local.backup_config.maintenance_window
} : {
enabled = false
retention_days = 0
backup_window = ""
maintenance_window = ""
}
tags = local.environment_tags
}
# Conditional monitoring module
module "monitoring" {
count = local.features.logging_enabled ? 1 : 0
source = "./modules/monitoring"
project_name = var.project_name
environment = var.environment
# Pass resources to monitor
database_id = module.database.db_instance_id
load_balancer_arn = aws_lb.app.arn
auto_scaling_group_name = aws_autoscaling_group.app.name
# Monitoring configuration
log_retention_days = local.monitoring_config.log_retention_days
alarm_threshold = local.monitoring_config.alarm_threshold
tags = local.environment_tags
}
Best Practices
1. Readable Conditional Logic
# Good: Clear, readable conditions
locals {
is_production = var.environment == "prod"
is_high_security = contains(["enhanced", "strict"], var.security_level)
enable_ssl = (
var.force_ssl ||
local.is_production ||
local.is_high_security
)
}
# Avoid: Complex nested ternary operators
locals {
# Don't do this
instance_type = var.environment == "prod" ? "t3.large" : var.environment == "staging" ? "t3.medium" : var.environment == "dev" ? "t3.micro" : "t3.nano"
# Do this instead
instance_type = (
var.environment == "prod" ? "t3.large" :
var.environment == "staging" ? "t3.medium" :
var.environment == "dev" ? "t3.micro" :
"t3.nano" # default
)
}
2. Consistent Conditional Patterns
# Establish consistent patterns for feature flags
locals {
features = {
ssl_enabled = var.enable_ssl || local.is_production
backup_enabled = var.enable_backup || var.environment != "dev"
monitoring_enabled = var.enable_monitoring || !local.is_development
}
# Use the same pattern for all conditional configurations
ssl_config = local.features.ssl_enabled ? var.ssl_config : null
backup_config = local.features.backup_enabled ? var.backup_config : null
monitoring_config = local.features.monitoring_enabled ? var.monitoring_config : null
}
3. Dynamic Block Organization
# Organize dynamic blocks logically
resource "aws_security_group" "app" {
name_prefix = "${var.project_name}-"
vpc_id = var.vpc_id
# Group related dynamic blocks together
# Application-specific ingress rules
dynamic "ingress" {
for_each = var.application_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "Application port ${ingress.value}"
}
}
# Management ingress rules
dynamic "ingress" {
for_each = local.is_production ? [] : var.management_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = var.management_cidrs
description = "Management port ${ingress.value}"
}
}
}
Key Takeaways
- Conditional Logic: Use ternary operators and conditional expressions for dynamic configurations
- Dynamic Blocks: Implement dynamic blocks for flexible resource structures
- Feature Flags: Create consistent patterns for feature enablement across environments
- Readability: Keep conditional logic clear and well-documented
- Consistency: Establish patterns and stick to them throughout your configurations
- Environment Awareness: Design configurations that adapt to different environments
Next Steps
- Tutorial 24: Learn about resource targeting and partial application
- Practice implementing complex conditional logic patterns
- Experiment with dynamic blocks for various resource types
- Review the Terraform Expressions Documentation