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
- Complex Types: Use lists, maps, objects, and tuples for structured data
- Validation: Implement validation rules to catch errors early
- Sensitive Data: Properly handle sensitive variables and outputs
- Optional Attributes: Use optional and nullable for flexible configurations
- Type Safety: Leverage Terraform's type system for robust configurations
- 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