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:
Always specify types
Add descriptions
Use validation for critical values
Provide sensible defaults
Use complex types when appropriate
Document validation rules
❌ DON’T:
Don’t use
anytype unless necessaryDon’t skip validation for user input
Don’t make everything a variable
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!



