Skip to main content

Command Palette

Search for a command to run...

Day 13: Conditional Expressions & Logic

Updated
13 min read
S

I'm a cloud-native enthusiast and tech blogger, sharing insights on Kubernetes, AWS, CI/CD, and Linux across my blog and Facebook page. Passionate about modern infrastructure and microservices, I aim to help others understand and leverage cloud-native technologies for scalable, efficient solutions.

Welcome to Day 13 - Today’s topic is a big one — Conditional Logic in Terraform.
By the end of this lesson, you’ll be able to make your Terraform configurations smart, flexible, and environment-aware—meaning your infrastructure can automatically adjust its setup for dev, staging, or production environments.

Think of it like this:

Instead of you manually changing settings for each environment, Terraform can make decisions for you automatically using conditions.


🎯 Today’s Goals

Here’s what you’ll learn step by step:

  • ✅ Understand conditional expressions (ternary operator)

  • ✅ Learn logical operators (AND, OR, NOT)

  • ✅ Write complex conditional logic

  • ✅ Use conditions to create or skip resources

  • ✅ Build environment-aware infrastructure

Let’s dive in! 🚀


🔀 Conditional Expressions

A conditional expression in Terraform works just like an if-else statement in programming.

Basic syntax:

condition ? true_value : false_value

It means:

“If the condition is true, use the first value; otherwise, use the second.”

Let’s see this in practice:

Example

variable "environment" {
  type = string
}

locals {
  # If the environment is prod, use a larger instance
  instance_type = var.environment == "prod" ? "t3.large" : "t2.small"

  # If prod, enable monitoring; otherwise disable it
  monitoring = var.environment == "prod" ? true : false

  # Nested conditions for different environments
  instance_count = (
    var.environment == "prod" ? 5 :
    var.environment == "staging" ? 3 :
    1  # default for dev
  )
}

💡 Explanation:

  • The ? and : symbols work like if ... else logic.

  • Terraform checks your environment (like prod, staging, or dev) and decides values automatically.

  • You can even nest multiple conditions for more flexibility.


Conditional Values

Terraform conditionals can be used with booleans, numbers, strings, and lists.

✅ Example with booleans

locals {
  is_production   = var.environment == "prod"
  backup_enabled  = local.is_production ? true : false
}

Explanation:
If environment is prod, backup_enabled becomes true. Otherwise, it’s false.


🔢 Example with numbers

locals {
  min_instances = var.high_availability ? 3 : 1
  max_instances = var.high_availability ? 10 : 3
}

If high availability is enabled, Terraform will deploy more instances automatically.


🧵 Example with strings

locals {
  db_instance_class = var.environment == "prod" ? "db.r5.xlarge" : "db.t3.small"
  dns_zone_name     = var.environment == "prod" ? "example.com" : "${var.environment}.example.com"
}

So in staging, your DNS could become staging.example.com.


📋 Example with lists

locals {
  availability_zones = var.multi_az ? ["us-east-1a", "us-east-1b", "us-east-1c"] : ["us-east-1a"]
}

Terraform chooses how many availability zones to use based on multi_az.


🔢 Comparison Operators

Comparison operators let you compare values just like in regular programming.

OperatorMeaningExample
==Equal tovar.env == "prod"
!=Not equalvar.env != "dev"
>Greater thanvar.count > 5
>=Greater than or equalvar.count >= 5
<Less thanvar.count < 5
<=Less than or equalvar.count <= 5

Example:

locals {
  needs_scaling = var.user_count > 1000 ? true : false
  is_free_tier  = var.instance_count <= 1 ? true : false
  is_valid_port = var.port >= 1 && var.port <= 65535 ? true : false
}

Terraform can check if a number is within a valid range or if it meets a threshold.


🔗 Logical Operators

Terraform supports three main logical operators — AND, OR, and NOT.


AND Operator (&&)

This means both conditions must be true.

locals {
  enable_backup = var.environment == "prod" && var.disk_size > 100
  create_lb     = var.environment == "prod" && var.instance_count > 1
}

✅ Terraform enables backups only if it’s production and disk size is greater than 100 GB.


OR Operator (||)

This means at least one condition must be true.

locals {
  monitoring = var.environment == "prod" || var.environment == "staging"
  create_nat = var.environment == "prod" || var.high_availability == true
}

✅ Monitoring is enabled for both prod and staging environments.


NOT Operator (!)

This reverses the boolean value.

locals {
  is_development = !local.is_production
  skip_backup = !(var.environment == "prod")
}

✅ If the environment is not production, Terraform skips creating backups.


Combining Operators

You can mix and match these operators to create advanced conditions.

locals {
  create_multi_az_db = (
    (var.environment == "prod" || var.environment == "staging") &&
    var.multi_az == true
  )
}

✅ This creates a multi-AZ database only if the environment is prod or staging and multi-AZ is enabled.


🎭 Conditional Resource Creation

One of the most powerful uses of conditionals is deciding whether Terraform should create a resource.


Example 1: Create or Skip a Resource

resource "aws_instance" "bastion" {
  count = var.environment == "prod" ? 1 : 0
  ami           = data.aws_ami.amazon_linux.id
  instance_type = "t2.micro"
}

✅ Terraform creates a bastion host only in production.
If it’s dev or staging, it skips it entirely!


Example 2: Conditional Resource Attributes

resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.environment == "prod" ? "t3.large" : "t2.micro"

  monitoring = var.environment == "prod" ? true : false
  subnet_id  = var.environment == "prod" ? aws_subnet.private[0].id : aws_subnet.public[0].id
}

✅ The same resource can adapt its configuration depending on the environment.


Example 3: Conditional Blocks with dynamic

Sometimes you need to include or exclude entire blocks, such as security rules.

resource "aws_security_group" "web" {
  dynamic "ingress" {
    for_each = var.environment == "dev" ? [1] : []
    content {
      from_port   = 22
      to_port     = 22
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
  }
}

✅ SSH access is only added in the dev environment.


🧮 Advanced Conditional Patterns

Terraform also provides functions like contains() and lookup() to simplify logic.

Multi-Way Conditions

You can chain conditions for multiple environments:

locals {
  instance_type = (
    var.environment == "prod" ? "t3.2xlarge" :
    var.environment == "staging" ? "t3.large" :
    var.environment == "dev" ? "t2.micro" :
    "t2.nano"
  )
}

Conditional with contains()

locals {
  is_valid_env   = contains(["dev", "staging", "prod"], var.environment)
  enable_feature = contains(["prod", "staging"], var.environment) ? true : false
}

✅ Checks if a value exists inside a list.


Conditional with lookup()

variable "instance_type_map" {
  type = map(string)
  default = {
    dev     = "t2.micro"
    staging = "t2.small"
    prod    = "t3.large"
  }
}

locals {
  instance_type = lookup(var.instance_type_map, var.environment, "t2.nano")
}

✅ If your environment isn’t found in the map, Terraform uses the fallback (t2.nano).


Null Conditionals

variable "custom_ami" {
  type    = string
  default = null
}

resource "aws_instance" "web" {
  ami = var.custom_ami != null ? var.custom_ami : data.aws_ami.amazon_linux.id
}

✅ If you don’t specify a custom AMI, Terraform will use the default Amazon Linux image.

🧪 Hands-On Lab: Conditional Infrastructure

Let’s build an environment-aware infrastructure!

🧱 Step 1: Create a New Project Folder

Open your terminal and create a new folder for this lab:

mkdir terraform-conditionals-lab
cd terraform-conditionals-lab

This will be your working directory for today’s exercise.


🧾 Step 2: Create variables.tf

This file defines all the inputs your Terraform configuration will use.
You’ll define variables like environment, monitoring, backup, etc.

# variables.tf

variable "aws_region" {
  type    = string
  default = "us-east-1"
}

variable "project_name" {
  type    = string
  default = "conditional-demo"
}

variable "environment" {
  description = "Environment (dev, staging, prod)"
  type        = string

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

variable "enable_monitoring" {
  description = "Override monitoring setting"
  type        = bool
  default     = null
}

variable "enable_backup" {
  description = "Override backup setting"
  type        = bool
  default     = null
}

variable "multi_az" {
  description = "Deploy across multiple availability zones"
  type        = bool
  default     = false
}

variable "high_availability" {
  description = "Enable high availability features"
  type        = bool
  default     = false
}

variable "compliance_mode" {
  description = "Enable compliance features (encryption, logging, etc.)"
  type        = bool
  default     = false
}

🧠 Explanation:

  • These variables make your configuration flexible.

  • environment can only be one of dev, staging, or prod.

  • The null defaults for enable_backup and enable_monitoring let Terraform decide automatically based on the environment.


🧮 Step 3: Create locals.tf

This is where the conditional logic lives.
You’ll use locals to calculate derived values from variables (like how many instances to create or which subnet to use).

# locals.tf

locals {
  # Environment checks
  is_production = var.environment == "prod"
  is_staging    = var.environment == "staging"
  is_dev        = var.environment == "dev"

  # Auto-enable features
  monitoring_enabled = var.enable_monitoring != null ? var.enable_monitoring : local.is_production || local.is_staging
  backup_enabled     = var.enable_backup != null ? var.enable_backup : local.is_production

  # Instance configuration
  instance_config = {
    dev = {
      type  = "t2.micro"
      count = 1
    }
    staging = {
      type  = "t2.small"
      count = 2
    }
    prod = {
      type  = var.high_availability ? "t3.large" : "t3.medium"
      count = var.high_availability ? 3 : 2
    }
  }

  instance_type  = local.instance_config[var.environment].type
  instance_count = local.instance_config[var.environment].count

  # Network configuration
  create_nat_gateway = local.is_production || var.multi_az
  az_count          = var.multi_az ? 3 : (local.is_production ? 2 : 1)

  # Security and compliance
  enable_encryption = local.is_production || var.compliance_mode
  enable_flow_logs  = local.is_production || var.compliance_mode

  allowed_ssh_cidrs = local.is_dev ? ["0.0.0.0/0"] : ["10.0.0.0/8"]

  # Backup retention period
  backup_retention_days = (
    local.is_production ? 30 :
    local.is_staging ? 14 :
    7
  )

  # Common tags
  common_tags = merge(
    {
      Project     = var.project_name
      Environment = var.environment
      ManagedBy   = "Terraform"
    },
    local.is_production ? { Backup = "Required", Monitoring = "Enhanced" } : {},
    var.compliance_mode ? { Compliance = "Enabled" } : {}
  )

  # Feature flags summary
  features = {
    monitoring        = local.monitoring_enabled
    backup            = local.backup_enabled
    nat_gateway       = local.create_nat_gateway
    multi_az          = var.multi_az
    encryption        = local.enable_encryption
    flow_logs         = local.enable_flow_logs
    high_availability = var.high_availability
  }
}

💡 Explanation:

  • Terraform now knows which settings apply to each environment automatically.

  • For example:

    • In dev, only 1 small instance, no NAT, no backup.

    • In prod, multiple instances, NAT gateways, encryption, and longer backups.


☁️ Step 4: Create main.tf

This file defines your actual AWS resources using the conditionals you built.

# main.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = local.common_tags
  }
}

data "aws_availability_zones" "available" {
  state = "available"
}

# 1️⃣ VPC
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = { Name = "${var.project_name}-${var.environment}-vpc" }
}

# 2️⃣ Internet Gateway
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
  tags = { Name = "${var.project_name}-${var.environment}-igw" }
}

# 3️⃣ Public Subnets
resource "aws_subnet" "public" {
  count = local.az_count
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.${count.index + 1}.0/24"
  availability_zone = data.aws_availability_zones.available.names[count.index]
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.project_name}-${var.environment}-public-${count.index + 1}"
    Type = "Public"
  }
}

# 4️⃣ Private Subnets (only for prod or multi-AZ)
resource "aws_subnet" "private" {
  count = local.is_production || var.multi_az ? local.az_count : 0
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.${count.index + 10}.0/24"
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = {
    Name = "${var.project_name}-${var.environment}-private-${count.index + 1}"
    Type = "Private"
  }
}

# NAT Gateway (conditional)
resource "aws_nat_gateway" "main" {
  count = local.create_nat_gateway ? local.az_count : 0

  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id

  tags = {
    Name = "${var.project_name}-${var.environment}-nat-${count.index + 1}"
  }
}

# Security Group with conditional rules
resource "aws_security_group" "web" {
  name        = "${var.project_name}-${var.environment}-web-sg"
  description = "Security group for web servers"
  vpc_id      = aws_vpc.main.id

  # HTTP - always allowed
  ingress {
    description = "HTTP"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  # HTTPS - always allowed
  ingress {
    description = "HTTPS"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  # SSH - conditional based on environment
  dynamic "ingress" {
    for_each = local.is_dev || local.is_staging ? [1] : []
    content {
      description = "SSH (Dev/Staging only)"
      from_port   = 22
      to_port     = 22
      protocol    = "tcp"
      cidr_blocks = local.allowed_ssh_cidrs
    }
  }

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

  tags = {
    Name = "${var.project_name}-${var.environment}-web-sg"
  }
}

# AMI Data Source
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }
}

# EC2 Instances
resource "aws_instance" "web" {
  count = local.instance_count

  ami           = data.aws_ami.amazon_linux.id
  instance_type = local.instance_type

  # Use private subnet in prod with NAT, public in dev/staging
  subnet_id = (local.is_production && local.create_nat_gateway) ? aws_subnet.private[count.index % length(aws_subnet.private)].id : aws_subnet.public[count.index % length(aws_subnet.public)].id

  vpc_security_group_ids = [aws_security_group.web.id]

  # Conditional monitoring
  monitoring = local.monitoring_enabled

  # Conditional EBS encryption
  root_block_device {
    encrypted   = local.enable_encryption
    volume_type = local.is_production ? "gp3" : "gp2"
    volume_size = local.is_production ? 30 : 20
  }

  user_data = <<-EOF
              #!/bin/bash
              yum update -y
              yum install -y httpd
              systemctl start httpd
              systemctl enable httpd
              echo "<h1>${var.environment} - Instance ${count.index + 1}</h1>" > /var/www/html/index.html
              echo "<p>Type: ${local.instance_type}</p>" >> /var/www/html/index.html
              echo "<p>Monitoring: ${local.monitoring_enabled}</p>" >> /var/www/html/index.html
              EOF

  tags = {
    Name  = "${var.project_name}-${var.environment}-web-${count.index + 1}"
    Tier  = "Web"
    Index = count.index + 1
  }
}

# VPC Flow Logs (conditional)
resource "aws_flow_log" "main" {
  count = local.enable_flow_logs ? 1 : 0

  iam_role_arn    = aws_iam_role.flow_logs[0].arn
  log_destination = aws_cloudwatch_log_group.flow_logs[0].arn
  traffic_type    = "ALL"
  vpc_id          = aws_vpc.main.id

  tags = {
    Name = "${var.project_name}-${var.environment}-flow-logs"
  }
}

# CloudWatch Log Group for Flow Logs (conditional)
resource "aws_cloudwatch_log_group" "flow_logs" {
  count = local.enable_flow_logs ? 1 : 0

  name              = "/aws/vpc/${var.project_name}-${var.environment}"
  retention_in_days = local.backup_retention_days

  tags = {
    Name = "${var.project_name}-${var.environment}-flow-logs"
  }
}

# IAM Role for Flow Logs (conditional)
resource "aws_iam_role" "flow_logs" {
  count = local.enable_flow_logs ? 1 : 0

  name = "${var.project_name}-${var.environment}-flow-logs-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "vpc-flow-logs.amazonaws.com"
      }
    }]
  })
}

resource "aws_iam_role_policy" "flow_logs" {
  count = local.enable_flow_logs ? 1 : 0

  name = "flow-logs-policy"
  role = aws_iam_role.flow_logs[0].id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents",
        "logs:DescribeLogGroups",
        "logs:DescribeLogStreams"
      ]
      Effect   = "Allow"
      Resource = "*"
    }]
  })
}

💡 Explanation:

  • Terraform uses the local values to decide how many of each resource to create.

  • For example, NAT gateways only exist in prod or when multi_az is true.

  • SSH rules are added only in dev or staging for safety.


📤 Step 5: Create outputs.tf

Finally, you’ll add output blocks to display summary information after terraform apply.

# outputs.tf

output "environment_info" {
  description = "Environment configuration summary"
  value = {
    environment       = var.environment
    is_production     = local.is_production
    is_staging        = local.is_staging
    is_dev            = local.is_dev
    features_enabled  = local.features
  }
}

output "infrastructure_details" {
  description = "Infrastructure configuration"
  value = {
    instance_type       = local.instance_type
    instance_count      = local.instance_count
    availability_zones  = local.az_count
    monitoring          = local.monitoring_enabled
    backup_enabled      = local.backup_enabled
    backup_retention    = local.backup_retention_days
    nat_gateway_count   = local.create_nat_gateway ? local.az_count : 0
  }
}

When you run:

terraform apply -var="environment=staging"

Terraform will automatically:

  • Use smaller instances

  • Skip private subnets

  • Enable monitoring

  • Set backup retention to 14 days
    All without changing a single line of code.


🧠 Summary of the Lab

EnvironmentInstance TypeNAT GatewayBackupMonitoring
Devt2.micro
Stagingt2.small
Prodt3.medium/large

You’ve just built a dynamic, environment-aware Terraform project!

Now you can:

  • Use the same code for dev, staging, and prod

  • Let Terraform make smart choices based on variables

  • Simplify your infrastructure management

Step 6: Test Different Environments

# Initialize
terraform init
# Test Development
terraform plan -var="environment=dev"
# Test Staging
terraform plan -var="environment=staging"
# Test Production
terraform plan -var="environment=prod"
# Test Production with High Availability
terraform plan -var="environment=prod" -var="high_availability=true"
# Apply development
terraform apply -var="environment=dev" -auto-approve
# View outputs
terraform output
# Clean up
terraform destroy -var="environment=dev" -auto-approve

📝 Summary

Today you learned:

  • ✅ Conditional expressions (ternary operator)

  • ✅ Comparison and logical operators

  • ✅ Combining conditions with AND, OR, NOT

  • ✅ Conditional resource creation

  • ✅ Multi-way conditionals

  • ✅ Building environment-aware infrastructure

🎉 Week 2 Complete!

Congratulations on completing Week 2! You’ve learned:

  • Terraform CLI mastery

  • Advanced variable types and validation

  • Output and local values

  • Collection manipulation

  • Count and for_each

  • Conditional logic

🚀 Week 3 Preview

Day 15: Built-in Functions in Terraform

Next week we’ll:

  • Explore Terraform’s built-in functions

  • Learn dynamic blocks

  • Create reusable modules

  • Work with module sources

  • Configure remote state


← Day 12: Count & For_Each | Day 15: Built-in Functions →


Remember: Conditionals make your infrastructure smart and adaptable!

🎊 Enjoy your Sunday! See you Monday for Week 3!

T

Thank you! I see there is minor error.

Add the following EIP resource to your main.tf file, preferably before the NAT Gateway resource:

Elastic IPs for NAT Gateways (conditional)

resource "aws_eip" "nat" { count = local.create_nat_gateway ? local.az_count : 0 domain = "vpc"

tags = { Name = "${var.project_name}-${var.environment}-nat-eip-${count.index + 1}" } }

1
S

Yes, you’re correct. I don’t know how I missed the EIP block. I really appreciate you pointing that out! 🫡

More from this blog

S

StackOps - Diary

33 posts

Welcome to the StackOps - Diary. We’re dedicated to empowering the tech community. We delve into cloud-native and microservices technologies, sharing knowledge to build modern, scalable solutions.