AskLearn
Loading...
← Back to Terraform Course
AdvancedConfiguration

Advanced Variable Types

Complex variable patterns

Tutorial 16: Advanced Variable Types and Validation

Learning Objectives

By the end of this tutorial, you will be able to:

  • Use complex variable types (list, map, object, tuple)
  • Implement variable validation rules
  • Handle sensitive variables securely
  • Use nullable and optional object attributes
  • Apply variable type constraints and defaults

Prerequisites

  • Completed Tutorial 6: Define Input Variables
  • Basic understanding of Terraform configuration syntax
  • Familiarity with HCL data types

Introduction

Terraform supports sophisticated variable types and validation mechanisms that enable you to create robust, self-documenting configurations. This tutorial explores advanced variable features that help prevent configuration errors and improve code quality.

Complex Variable Types

List Variables

Lists contain ordered sequences of values of the same type.

# variables.tf
variable "availability_zones" {
  description = "List of availability zones"
  type        = list(string)
  default     = ["us-west-2a", "us-west-2b", "us-west-2c"]
}

variable "instance_counts" {
  description = "Number of instances per environment"
  type        = list(number)
  default     = [1, 2, 3]
}

Map Variables

Maps contain key-value pairs where all values have the same type.

# variables.tf
variable "instance_types" {
  description = "Instance types for different environments"
  type        = map(string)
  default = {
    dev     = "t3.micro"
    staging = "t3.small"
    prod    = "t3.medium"
  }
}

variable "tags" {
  description = "Common tags for all resources"
  type        = map(string)
  default = {
    Project     = "MyApp"
    Environment = "development"
    Owner       = "DevOps Team"
  }
}

Object Variables

Objects have a fixed schema with named attributes of potentially different types.

# variables.tf
variable "database_config" {
  description = "Database configuration object"
  type = object({
    engine         = string
    engine_version = string
    instance_class = string
    allocated_storage = number
    backup_retention = number
    multi_az         = bool
  })
  default = {
    engine         = "mysql"
    engine_version = "8.0"
    instance_class = "db.t3.micro"
    allocated_storage = 20
    backup_retention = 7
    multi_az         = false
  }
}

variable "vpc_config" {
  description = "VPC configuration with nested objects"
  type = object({
    cidr_block = string
    subnets = list(object({
      cidr_block        = string
      availability_zone = string
      public           = bool
    }))
  })
}

Tuple Variables

Tuples are like lists but can contain values of different types.

# variables.tf
variable "server_config" {
  description = "Server configuration tuple"
  type        = tuple([string, number, bool])
  default     = ["web-server", 80, true]
}

Variable Validation

Basic Validation Rules

# variables.tf
variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t3.micro"
  
  validation {
    condition     = contains(["t3.micro", "t3.small", "t3.medium"], var.instance_type)
    error_message = "Instance type must be t3.micro, t3.small, or t3.medium."
  }
}

variable "environment" {
  description = "Environment name"
  type        = string
  
  validation {
    condition     = can(regex("^(dev|staging|prod)$", var.environment))
    error_message = "Environment must be dev, staging, or prod."
  }
}

Advanced Validation

# variables.tf
variable "cidr_block" {
  description = "VPC CIDR block"
  type        = string
  
  validation {
    condition     = can(cidrhost(var.cidr_block, 0))
    error_message = "CIDR block must be a valid IPv4 CIDR."
  }
}

variable "port_range" {
  description = "Port range for security group"
  type        = number
  
  validation {
    condition     = var.port_range >= 1 && var.port_range <= 65535
    error_message = "Port must be between 1 and 65535."
  }
}

variable "tags" {
  description = "Resource tags"
  type        = map(string)
  
  validation {
    condition     = alltrue([for v in values(var.tags) : length(v) > 0])
    error_message = "All tag values must be non-empty strings."
  }
  
  validation {
    condition     = contains(keys(var.tags), "Environment")
    error_message = "Tags must include an Environment key."
  }
}

Sensitive Variables

Marking Variables as Sensitive

# variables.tf
variable "database_password" {
  description = "Database master password"
  type        = string
  sensitive   = true
}

variable "api_keys" {
  description = "API keys for external services"
  type        = map(string)
  sensitive   = true
}

Using Sensitive Values

# main.tf
resource "aws_db_instance" "example" {
  allocated_storage    = 20
  storage_type         = "gp2"
  engine              = "mysql"
  engine_version      = "8.0"
  instance_class      = "db.t3.micro"
  identifier          = "mydb"
  username            = "admin"
  password            = var.database_password
  skip_final_snapshot = true
}

# outputs.tf
output "database_endpoint" {
  description = "Database endpoint"
  value       = aws_db_instance.example.endpoint
}

# Don't output sensitive values directly
output "database_password_length" {
  description = "Length of database password"
  value       = length(var.database_password)
  sensitive   = true
}

Nullable and Optional Attributes

Nullable Variables

# variables.tf
variable "backup_retention_period" {
  description = "Backup retention period in days (null for no backups)"
  type        = number
  default     = null
  nullable    = true
  
  validation {
    condition     = var.backup_retention_period == null || (var.backup_retention_period >= 1 && var.backup_retention_period <= 35)
    error_message = "Backup retention period must be between 1 and 35 days, or null."
  }
}

Optional Object Attributes

# variables.tf
variable "server_config" {
  description = "Server configuration with optional attributes"
  type = object({
    name               = string
    instance_type     = string
    monitoring        = optional(bool, false)
    backup_enabled    = optional(bool, true)
    security_groups   = optional(list(string), [])
    tags              = optional(map(string), {})
  })
}

Practical Example: Multi-Tier Application

Let's create a complete example with advanced variable types:

# variables.tf
variable "application_config" {
  description = "Multi-tier application configuration"
  type = object({
    name        = string
    environment = string
    
    vpc = object({
      cidr_block           = string
      enable_dns_hostnames = optional(bool, true)
      enable_dns_support   = optional(bool, true)
    })
    
    web_tier = object({
      instance_type    = string
      min_size        = number
      max_size        = number
      desired_capacity = number
      subnets         = list(string)
    })
    
    app_tier = object({
      instance_type    = string
      min_size        = number
      max_size        = number
      desired_capacity = number
      subnets         = list(string)
    })
    
    database = object({
      engine            = string
      engine_version    = string
      instance_class    = string
      allocated_storage = number
      multi_az         = optional(bool, false)
      backup_retention = optional(number, 7)
      subnets          = list(string)
    })
    
    monitoring = optional(object({
      enabled               = bool
      detailed_monitoring   = optional(bool, false)
      log_retention_days   = optional(number, 30)
    }), {
      enabled = false
    })
  })
  
  validation {
    condition     = can(regex("^[a-z][a-z0-9-]*[a-z0-9]$", var.application_config.name))
    error_message = "Application name must start with a letter, contain only lowercase letters, numbers, and hyphens, and end with a letter or number."
  }
  
  validation {
    condition     = contains(["dev", "staging", "prod"], var.application_config.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
  
  validation {
    condition     = var.application_config.web_tier.min_size <= var.application_config.web_tier.max_size
    error_message = "Web tier min_size must be less than or equal to max_size."
  }
  
  validation {
    condition     = var.application_config.database.allocated_storage >= 20
    error_message = "Database allocated storage must be at least 20 GB."
  }
}

variable "common_tags" {
  description = "Common tags applied to all resources"
  type        = map(string)
  default     = {}
  
  validation {
    condition     = alltrue([for k, v in var.common_tags : can(regex("^[A-Za-z][A-Za-z0-9_-]*$", k))])
    error_message = "Tag keys must start with a letter and contain only alphanumeric characters, underscores, and hyphens."
  }
}

variable "allowed_cidr_blocks" {
  description = "CIDR blocks allowed to access the application"
  type        = list(string)
  default     = ["0.0.0.0/0"]
  
  validation {
    condition     = alltrue([for cidr in var.allowed_cidr_blocks : can(cidrhost(cidr, 0))])
    error_message = "All CIDR blocks must be valid IPv4 CIDRs."
  }
}
# terraform.tfvars.example
application_config = {
  name        = "myapp"
  environment = "dev"
  
  vpc = {
    cidr_block = "10.0.0.0/16"
  }
  
  web_tier = {
    instance_type    = "t3.micro"
    min_size        = 1
    max_size        = 3
    desired_capacity = 2
    subnets         = ["10.0.1.0/24", "10.0.2.0/24"]
  }
  
  app_tier = {
    instance_type    = "t3.small"
    min_size        = 1
    max_size        = 5
    desired_capacity = 2
    subnets         = ["10.0.11.0/24", "10.0.12.0/24"]
  }
  
  database = {
    engine            = "mysql"
    engine_version    = "8.0"
    instance_class    = "db.t3.micro"
    allocated_storage = 20
    subnets          = ["10.0.21.0/24", "10.0.22.0/24"]
  }
  
  monitoring = {
    enabled = true
    detailed_monitoring = true
    log_retention_days = 14
  }
}

common_tags = {
  Project     = "MyApplication"
  Owner       = "DevOps Team"
  Environment = "development"
  CostCenter  = "Engineering"
}

allowed_cidr_blocks = [
  "10.0.0.0/8",
  "172.16.0.0/12"
]

Using Variables in Configuration

# main.tf
locals {
  app_name = var.application_config.name
  env      = var.application_config.environment
  
  # Merge common tags with resource-specific tags
  common_tags = merge(var.common_tags, {
    Name        = "${local.app_name}-${local.env}"
    Environment = local.env
  })
}

# VPC
resource "aws_vpc" "main" {
  cidr_block           = var.application_config.vpc.cidr_block
  enable_dns_hostnames = var.application_config.vpc.enable_dns_hostnames
  enable_dns_support   = var.application_config.vpc.enable_dns_support
  
  tags = merge(local.common_tags, {
    Name = "${local.app_name}-${local.env}-vpc"
    Type = "networking"
  })
}

# Database subnet group
resource "aws_db_subnet_group" "main" {
  name       = "${local.app_name}-${local.env}-db-subnet-group"
  subnet_ids = [for subnet in var.application_config.database.subnets : aws_subnet.database[subnet].id]
  
  tags = merge(local.common_tags, {
    Name = "${local.app_name}-${local.env}-db-subnet-group"
    Type = "database"
  })
}

# RDS instance
resource "aws_db_instance" "main" {
  identifier             = "${local.app_name}-${local.env}-db"
  engine                = var.application_config.database.engine
  engine_version        = var.application_config.database.engine_version
  instance_class        = var.application_config.database.instance_class
  allocated_storage     = var.application_config.database.allocated_storage
  
  db_subnet_group_name  = aws_db_subnet_group.main.name
  multi_az             = var.application_config.database.multi_az
  backup_retention_period = var.application_config.database.backup_retention
  
  skip_final_snapshot = local.env == "dev"
  
  tags = merge(local.common_tags, {
    Name = "${local.app_name}-${local.env}-database"
    Type = "database"
  })
}

Variable Validation Testing

Create a test file to validate your variable configurations:

# test/variables_test.tf
variable "test_cases" {
  description = "Test cases for variable validation"
  type = map(object({
    application_config = object({
      name        = string
      environment = string
      vpc = object({
        cidr_block = string
      })
      web_tier = object({
        instance_type    = string
        min_size        = number
        max_size        = number
        desired_capacity = number
        subnets         = list(string)
      })
      app_tier = object({
        instance_type    = string
        min_size        = number
        max_size        = number
        desired_capacity = number
        subnets         = list(string)
      })
      database = object({
        engine            = string
        engine_version    = string
        instance_class    = string
        allocated_storage = number
        subnets          = list(string)
      })
    })
    should_pass = bool
  }))
  
  default = {
    valid_config = {
      application_config = {
        name        = "myapp"
        environment = "dev"
        vpc = {
          cidr_block = "10.0.0.0/16"
        }
        web_tier = {
          instance_type    = "t3.micro"
          min_size        = 1
          max_size        = 3
          desired_capacity = 2
          subnets         = ["10.0.1.0/24"]
        }
        app_tier = {
          instance_type    = "t3.small"
          min_size        = 1
          max_size        = 5
          desired_capacity = 2
          subnets         = ["10.0.11.0/24"]
        }
        database = {
          engine            = "mysql"
          engine_version    = "8.0"
          instance_class    = "db.t3.micro"
          allocated_storage = 20
          subnets          = ["10.0.21.0/24"]
        }
      }
      should_pass = true
    }
  }
}

Best Practices

1. Variable Organization

  • Group related variables in objects
  • Use descriptive names and documentation
  • Provide sensible defaults where appropriate

2. Validation Rules

  • Validate critical configuration values
  • Provide clear error messages
  • Use built-in functions for complex validation

3. Type Safety

  • Use specific types instead of any
  • Leverage optional attributes for flexibility
  • Consider nullable for truly optional values

4. Security

  • Mark sensitive variables appropriately
  • Never output sensitive values directly
  • Use separate files for sensitive defaults

5. Documentation

  • Document complex variable structures
  • Provide examples in comments
  • Use validation error messages as documentation

Common Pitfalls

1. Over-Complex Variables

# Avoid overly nested structures
variable "bad_example" {
  type = object({
    level1 = object({
      level2 = object({
        level3 = object({
          level4 = string
        })
      })
    })
  })
}

# Better: Keep structures flat and logical
variable "good_example" {
  type = object({
    database_config = object({
      engine   = string
      version  = string
      size     = string
    })
    network_config = object({
      vpc_cidr = string
      subnets  = list(string)
    })
  })
}

2. Missing Validation

# Add validation for important values
variable "environment" {
  type = string
  
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

3. Insecure Defaults

# Avoid insecure defaults
variable "public_access" {
  type    = bool
  default = false  # Secure by default
}

Key Takeaways

  1. Complex Types: Use lists, maps, objects, and tuples for structured data
  2. Validation: Implement validation rules to catch errors early
  3. Sensitive Data: Properly handle sensitive variables and outputs
  4. Optional Attributes: Use optional and nullable for flexible configurations
  5. Type Safety: Leverage Terraform's type system for robust configurations
  6. Documentation: Well-documented variables improve maintainability

Next Steps

  • Tutorial 17: Learn about local values and computed expressions
  • Practice creating complex variable structures for your use cases
  • Experiment with different validation patterns
  • Review the Terraform Variable Documentation

Additional Resources