Skip to main content

Command Palette

Search for a command to run...

Day 9: Input Variables - Types & Validation

Updated
10 min read
Day 9: Input Variables - Types & Validation

Welcome to Day 9! Today we’ll master Terraform input variables - from simple strings to complex nested objects. You’ll learn how to create type-safe, validated configurations that prevent errors before they happen.

🎯 Today’s Goals

  • Master all variable types in depth

  • Learn complex type constructors

  • Implement robust variable validation

  • Understand type constraints and conversion

  • Build production-ready variable configurations

📊 Variable Type System

Terraform has a rich type system:

Primitive Types          Complex Types
├── string              ├── list(type)
├── number              ├── set(type)
└── bool                ├── map(type)
                        ├── object({...})
                        └── tuple([...])

Special Type
└── any (accepts anything)

🔤 Primitive Types Deep Dive

String Type

variable "region" {
  description = "AWS region"
  type        = string
  default     = "us-east-1"
}

variable "project_name" {
  type    = string
  default = "myproject"
}

# Multi-line strings
variable "user_data" {
  type = string
  default = <<-EOT
    #!/bin/bash
    echo "Hello World"
    yum update -y
  EOT
}

Number Type

variable "instance_count" {
  description = "Number of instances to create"
  type        = number
  default     = 3
}

variable "max_size" {
  type    = number
  default = 10
}

# Decimals are supported
variable "cpu_credits" {
  type    = number
  default = 0.5
}

Bool Type

variable "enable_monitoring" {
  description = "Enable detailed monitoring"
  type        = bool
  default     = true
}

variable "is_production" {
  type    = bool
  default = false
}

# Usage in resources
resource "aws_instance" "web" {
  monitoring = var.enable_monitoring
}

📋 Collection Types

List Type

Ordered collection of values of the same type.

variable "availability_zones" {
  description = "List of availability zones"
  type        = list(string)
  default     = ["us-east-1a", "us-east-1b", "us-east-1c"]
}

variable "allowed_ports" {
  description = "List of allowed ports"
  type        = list(number)
  default     = [80, 443, 8080]
}

# Access elements
resource "aws_subnet" "example" {
  availability_zone = var.availability_zones[0]  # First element
}

# Iterate over list
resource "aws_subnet" "public" {
  count             = length(var.availability_zones)
  availability_zone = var.availability_zones[count.index]
}

Set Type

Unordered collection of unique values.

variable "security_group_ids" {
  description = "Set of security group IDs"
  type        = set(string)
  default     = ["sg-12345", "sg-67890"]
}

# Sets automatically remove duplicates
# ["a", "b", "a"] becomes ["a", "b"]

Map Type

Collection of key-value pairs.

variable "instance_types" {
  description = "Instance types per environment"
  type        = map(string)
  default = {
    dev     = "t2.micro"
    staging = "t2.small"
    prod    = "t3.large"
  }
}

variable "common_tags" {
  description = "Tags to apply to all resources"
  type        = map(string)
  default = {
    Project     = "MyApp"
    ManagedBy   = "Terraform"
    Environment = "Production"
  }
}

# Access map values
resource "aws_instance" "web" {
  instance_type = var.instance_types["prod"]
  tags          = var.common_tags
}

🏗️ Structural Types

Object Type

Structured type with named attributes of different types.

variable "vpc_config" {
  description = "VPC configuration"
  type = object({
    cidr_block           = string
    enable_dns_hostnames = bool
    enable_dns_support   = bool
    name                 = string
  })

  default = {
    cidr_block           = "10.0.0.0/16"
    enable_dns_hostnames = true
    enable_dns_support   = true
    name                 = "main-vpc"
  }
}

# Usage
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_config.cidr_block
  enable_dns_hostnames = var.vpc_config.enable_dns_hostnames
  enable_dns_support   = var.vpc_config.enable_dns_support

  tags = {
    Name = var.vpc_config.name
  }
}

Tuple Type

Sequence with fixed length and specific types for each position.

variable "network_config" {
  description = "Network configuration [cidr, az, public]"
  type        = tuple([string, string, bool])
  default     = ["10.0.1.0/24", "us-east-1a", true]
}

# Access by index
locals {
  cidr_block   = var.network_config[0]
  az           = var.network_config[1]
  is_public    = var.network_config[2]
}

🔐 Advanced Variable Validation

Basic Validation

variable "environment" {
  description = "Environment name"
  type        = string

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

Multiple Validation Rules

variable "instance_count" {
  description = "Number of instances (1-10)"
  type        = number

  validation {
    condition     = var.instance_count > 0
    error_message = "Instance count must be positive."
  }

  validation {
    condition     = var.instance_count <= 10
    error_message = "Instance count cannot exceed 10."
  }
}

Regex Validation

variable "bucket_name" {
  description = "S3 bucket name"
  type        = string

  validation {
    condition     = can(regex("^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$", var.bucket_name))
    error_message = "Bucket name must be 3-63 characters, lowercase alphanumeric and hyphens."
  }
}

variable "ip_address" {
  description = "IP address"
  type        = string

  validation {
    condition = can(regex(
      "^([0-9]{1,3}\\\\.){3}[0-9]{1,3}$",
      var.ip_address
    ))
    error_message = "Must be a valid IPv4 address."
  }
}

variable "email" {
  description = "Email address"
  type        = string

  validation {
    condition     = can(regex("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}$", var.email))
    error_message = "Must be a valid email address."
  }
}

Complex Validation Logic

variable "instance_config" {
  description = "Instance configuration"
  type = object({
    type  = string
    count = number
  })

  validation {
    condition     = contains(["t2.micro", "t2.small", "t3.medium"], var.instance_config.type)
    error_message = "Instance type must be t2.micro, t2.small, or t3.medium."
  }

  validation {
    condition     = var.instance_config.count >= 1 && var.instance_config.count <= 5
    error_message = "Instance count must be between 1 and 5."
  }
}

Validation Functions

variable "region" {
  type = string

  validation {
    # Check if region starts with valid prefix
    condition = anytrue([
      startswith(var.region, "us-"),
      startswith(var.region, "eu-"),
      startswith(var.region, "ap-"),
    ])
    error_message = "Region must start with us-, eu-, or ap-."
  }
}

variable "tags" {
  type = map(string)

  validation {
    # Ensure required tags exist
    condition     = alltrue([
      contains(keys(var.tags), "Environment"),
      contains(keys(var.tags), "Project"),
    ])
    error_message = "Tags must include Environment and Project."
  }
}

Map of Objects

variable "applications" {
  description = "Application configurations"
  type = map(object({
    instance_type = string
    instance_count = number
    subnets        = list(string)
    enable_monitoring = bool
  }))

  default = {
    web = {
      instance_type     = "t3.medium"
      instance_count    = 3
      subnets           = ["subnet-1", "subnet-2"]
      enable_monitoring = true
    }
    api = {
      instance_type     = "t3.large"
      instance_count    = 2
      subnets           = ["subnet-3", "subnet-4"]
      enable_monitoring = true
    }
    worker = {
      instance_type     = "t3.small"
      instance_count    = 5
      subnets           = ["subnet-5"]
      enable_monitoring = false
    }
  }
}

# Usage
resource "aws_instance" "apps" {
  for_each = var.applications

  instance_type = each.value.instance_type
  # ... use other values
}

🧪 Hands-On Lab: Advanced Variables

Let’s build a complete infrastructure with advanced variable types and validation!

Step 1: Create Project

mkdir terraform-advanced-variables
cd terraform-advanced-variables

Step 2: Create variables.tf

# variables.tf

variable "aws_region" {
  description = "AWS region for resources"
  type        = string
  default     = "us-east-1"

  validation {
    condition = can(regex("^(us|eu|ap|sa|ca|me|af)-(north|south|east|west|central|northeast|southeast)-[1-3]$", var.aws_region))
    error_message = "Must be a valid AWS region."
  }
}

variable "environment" {
  description = "Environment name"
  type        = string

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

variable "vpc_config" {
  description = "VPC configuration"
  type = object({
    cidr_block           = string
    enable_dns_hostnames = bool
    enable_dns_support   = bool
    enable_nat_gateway   = bool
  })

  validation {
    condition     = can(cidrhost(var.vpc_config.cidr_block, 0))
    error_message = "VPC CIDR block must be valid."
  }
}

variable "subnets" {
  description = "Subnet configurations"
  type = list(object({
    name              = string
    cidr_block        = string
    availability_zone = string
    type              = string  # "public" or "private"
  }))

  validation {
    condition = alltrue([
      for subnet in var.subnets :
      contains(["public", "private"], subnet.type)
    ])
    error_message = "Subnet type must be either 'public' or 'private'."
  }

  validation {
    condition = alltrue([
      for subnet in var.subnets :
      can(cidrhost(subnet.cidr_block, 0))
    ])
    error_message = "All subnet CIDR blocks must be valid."
  }
}

variable "security_groups" {
  description = "Security group configurations"
  type = map(object({
    description = string
    ingress_rules = list(object({
      from_port   = number
      to_port     = number
      protocol    = string
      cidr_blocks = list(string)
      description = string
    }))
  }))
}

variable "instance_configs" {
  description = "EC2 instance configurations"
  type = map(object({
    instance_type = string
    count         = number
    subnet_type   = string
    security_groups = list(string)
  }))

  validation {
    condition = alltrue([
      for key, config in var.instance_configs :
      config.count >= 0 && config.count <= 10
    ])
    error_message = "Instance count must be between 0 and 10."
  }
}

variable "tags" {
  description = "Common tags for all resources"
  type        = map(string)

  validation {
    condition = alltrue([
      contains(keys(var.tags), "Project"),
      contains(keys(var.tags), "Owner"),
    ])
    error_message = "Tags must include Project and Owner."
  }
}

variable "enable_backup" {
  description = "Enable automated backups"
  type        = bool
  default     = true
}

Step 3: Create terraform.tfvars

# terraform.tfvars

aws_region  = "us-east-1"
environment = "dev"

vpc_config = {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true
  enable_nat_gateway   = false
}

subnets = [
  {
    name              = "public-subnet-1"
    cidr_block        = "10.0.1.0/24"
    availability_zone = "us-east-1a"
    type              = "public"
  },
  {
    name              = "public-subnet-2"
    cidr_block        = "10.0.2.0/24"
    availability_zone = "us-east-1b"
    type              = "public"
  },
  {
    name              = "private-subnet-1"
    cidr_block        = "10.0.10.0/24"
    availability_zone = "us-east-1a"
    type              = "private"
  }
]

security_groups = {
  web = {
    description = "Security group for web servers"
    ingress_rules = [
      {
        from_port   = 80
        to_port     = 80
        protocol    = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
        description = "HTTP from anywhere"
      },
      {
        from_port   = 443
        to_port     = 443
        protocol    = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
        description = "HTTPS from anywhere"
      }
    ]
  }
  app = {
    description = "Security group for application servers"
    ingress_rules = [
      {
        from_port   = 8080
        to_port     = 8080
        protocol    = "tcp"
        cidr_blocks = ["10.0.0.0/16"]
        description = "App port from VPC"
      }
    ]
  }
}

instance_configs = {
  web = {
    instance_type   = "t2.micro"
    count           = 2
    subnet_type     = "public"
    security_groups = ["web"]
  }
  app = {
    instance_type   = "t2.small"
    count           = 2
    subnet_type     = "private"
    security_groups = ["app"]
  }
}

tags = {
  Project     = "AdvancedVariables"
  Owner       = "DevOps Team"
  Environment = "Development"
  ManagedBy   = "Terraform"
}

enable_backup = true

Step 4: Create main.tf

# main.tf

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

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = var.tags
  }
}

# VPC
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_config.cidr_block
  enable_dns_hostnames = var.vpc_config.enable_dns_hostnames
  enable_dns_support   = var.vpc_config.enable_dns_support

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

# Internet Gateway (for public subnets)
resource "aws_internet_gateway" "main" {
  count = length([for s in var.subnets : s if s.type == "public"]) > 0 ? 1 : 0

  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.environment}-igw"
  }
}

# Subnets
resource "aws_subnet" "subnets" {
  for_each = { for idx, subnet in var.subnets : subnet.name => subnet }

  vpc_id                  = aws_vpc.main.id
  cidr_block              = each.value.cidr_block
  availability_zone       = each.value.availability_zone
  map_public_ip_on_launch = each.value.type == "public"

  tags = {
    Name = each.value.name
    Type = each.value.type
  }
}

# Security Groups
resource "aws_security_group" "groups" {
  for_each = var.security_groups

  name        = "${var.environment}-${each.key}-sg"
  description = each.value.description
  vpc_id      = aws_vpc.main.id

  dynamic "ingress" {
    for_each = each.value.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
    }
  }

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

  tags = {
    Name = "${var.environment}-${each.key}-sg"
  }
}

Step 5: Test Validation

# Initialize
terraform init
# Test with invalid environment
terraform plan -var="environment=production"
# Error: Environment must be dev, staging, or prod.
# Test with invalid region
terraform plan -var="aws_region=invalid-region"
# Error: Must be a valid AWS region.
# Test with valid values
terraform plan

Step 6: Test Different Environments

Create prod.tfvars:

environment = "prod"

vpc_config = {
  cidr_block           = "172.16.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true
  enable_nat_gateway   = true
}

instance_configs = {
  web = {
    instance_type   = "t3.medium"
    count           = 4
    subnet_type     = "public"
    security_groups = ["web"]
  }
}
terraform plan -var-file="prod.tfvars"

Step 7: Apply and Clean Up

# Apply
terraform apply -auto-approve

# Destroy
terraform destroy -auto-approve

📝 Variable Best Practices

DO:

  1. Always specify types

  2. Add descriptions

  3. Use validation for critical values

  4. Provide sensible defaults

  5. Use complex types when appropriate

  6. Document validation rules

DON’T:

  1. Don’t use any type unless necessary

  2. Don’t skip validation for user input

  3. Don’t make everything a variable

  4. Don’t use overly complex nested types

📝 Summary

Today you learned:

  • ✅ All Terraform variable types

  • ✅ Complex type constructors (list, map, object, tuple)

  • ✅ Advanced validation techniques

  • ✅ Regex and custom validation

  • ✅ Nested and complex types

  • ✅ Type-safe configurations

🚀 Tomorrow’s Preview

Day 10: Output Values & Local Values

Tomorrow we’ll:

  • Master output values

  • Learn local values and when to use them

  • Understand output dependencies

  • Share data between configurations

  • Build modular outputs


← Day 8: Terraform CLI | Day 10: Outputs & Locals →


Remember: Type safety and validation prevent errors before they happen!

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.