AskLearn
Loading...
← Back to Terraform Course
IntermediateFundamentals

Define Input Variables

Parameterize configurations

Tutorial 6: Define Input Variables

Learning Objectives

  • Understand the purpose and benefits of input variables
  • Learn different variable types and their usage
  • Practice defining and using variables in Terraform configurations
  • Implement variable validation and best practices

Why Use Variables?

Problems with Hardcoded Values

# Hardcoded configuration - NOT recommended
resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1d0"  # Hardcoded
  instance_type = "t2.micro"              # Hardcoded
  
  tags = {
    Name = "web-server-production"        # Hardcoded
  }
}

Issues:

  • Configuration not reusable across environments
  • Hard to maintain and update
  • No flexibility for different use cases
  • Risk of errors when copying configurations

Benefits of Variables

  • Reusability: Same configuration for multiple environments
  • Flexibility: Easy to customize deployments
  • Maintainability: Single place to change values
  • Validation: Ensure inputs meet requirements
  • Documentation: Self-documenting configurations

Variable Declaration Syntax

Basic Variable Block

variable "variable_name" {
  description = "A description of the variable"
  type        = string
  default     = "default_value"
}

Variable Components

  • Name: Identifier for the variable
  • Description: Human-readable explanation
  • Type: Data type constraint
  • Default: Optional default value
  • Validation: Optional validation rules
  • Sensitive: Mark as sensitive for security

Variable Types

String Variables

variable "region" {
  description = "AWS region for resources"
  type        = string
  default     = "us-west-2"
}

variable "environment" {
  description = "Environment name"
  type        = string
  # No default - must be provided
}

Number Variables

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

variable "disk_size" {
  description = "Disk size in GB"
  type        = number
  default     = 20
}

Boolean Variables

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

variable "create_load_balancer" {
  description = "Whether to create a load balancer"
  type        = bool
  default     = false
}

List Variables

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

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

Map Variables

variable "instance_types" {
  description = "Instance types by environment"
  type        = map(string)
  default = {
    dev  = "t2.micro"
    prod = "t3.medium"
  }
}

variable "common_tags" {
  description = "Common tags for all resources"
  type        = map(string)
  default = {
    Project   = "terraform-tutorial"
    ManagedBy = "Terraform"
  }
}

Object Variables

variable "vpc_config" {
  description = "VPC configuration object"
  type = object({
    cidr_block           = string
    enable_dns_hostnames = bool
    enable_dns_support   = bool
  })
  default = {
    cidr_block           = "10.0.0.0/16"
    enable_dns_hostnames = true
    enable_dns_support   = true
  }
}

variable "database_config" {
  description = "Database configuration"
  type = object({
    engine         = string
    engine_version = string
    instance_class = string
    allocated_storage = number
  })
}

Using Variables in Configuration

Variable Reference Syntax

# Reference a variable using var.variable_name
resource "aws_instance" "web" {
  ami           = var.ami_id
  instance_type = var.instance_type
  count         = var.instance_count
  
  tags = var.common_tags
}

Complete Example with Variables

variables.tf

variable "region" {
  description = "AWS region"
  type        = string
  default     = "us-west-2"
}

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

variable "project_name" {
  description = "Name of the project"
  type        = string
  default     = "terraform-tutorial"
}

variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t2.micro"
}

variable "create_load_balancer" {
  description = "Whether to create a load balancer"
  type        = bool
  default     = false
}

variable "allowed_cidr_blocks" {
  description = "CIDR blocks allowed to access the instance"
  type        = list(string)
  default     = ["0.0.0.0/0"]
}

variable "tags" {
  description = "Additional tags"
  type        = map(string)
  default     = {}
}

main.tf (using variables)

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

provider "aws" {
  region = var.region
}

# Local values combining variables
locals {
  common_tags = merge(
    {
      Environment = var.environment
      Project     = var.project_name
      ManagedBy   = "Terraform"
    },
    var.tags
  )
  
  name_prefix = "${var.project_name}-${var.environment}"
}

# Data source using variable
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]
  
  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }
}

# VPC using variables
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true
  
  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-vpc"
  })
}

# Security group using variables
resource "aws_security_group" "web" {
  name        = "${local.name_prefix}-web-sg"
  description = "Security group for ${var.environment} web servers"
  vpc_id      = aws_vpc.main.id
  
  dynamic "ingress" {
    for_each = [80, 443, 22]
    content {
      from_port   = ingress.value
      to_port     = ingress.value
      protocol    = "tcp"
      cidr_blocks = var.allowed_cidr_blocks
    }
  }
  
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  
  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-web-sg"
  })
}

# EC2 instance using variables
resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.instance_type
  
  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-web-server"
  })
}

Variable Validation

Basic Validation

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

Advanced Validation

variable "vpc_cidr" {
  description = "CIDR block for VPC"
  type        = string
  
  validation {
    condition     = can(cidrhost(var.vpc_cidr, 0))
    error_message = "VPC CIDR must be a valid IPv4 CIDR block."
  }
  
  validation {
    condition     = split("/", var.vpc_cidr)[1] <= 24
    error_message = "VPC CIDR block must be /24 or larger."
  }
}

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."
  }
}

variable "tags" {
  description = "Resource tags"
  type        = map(string)
  
  validation {
    condition     = contains(keys(var.tags), "Environment")
    error_message = "Tags must include an Environment key."
  }
}

Sensitive Variables

Marking Variables as Sensitive

variable "database_password" {
  description = "Password for the database"
  type        = string
  sensitive   = true
}

variable "api_key" {
  description = "API key for external service"
  type        = string
  sensitive   = true
}

Using Sensitive Variables

resource "aws_db_instance" "main" {
  # ... other configuration
  password = var.database_password  # Will be hidden in logs
}

Providing Variable Values

Method 1: terraform.tfvars File

# terraform.tfvars
region      = "us-east-1"
environment = "prod"
instance_type = "t3.medium"
create_load_balancer = true

tags = {
  Owner   = "DevOps Team"
  Purpose = "Web Application"
}

Method 2: Environment-Specific Files

# dev.tfvars
environment = "dev"
instance_type = "t2.micro"
create_load_balancer = false

# prod.tfvars
environment = "prod"
instance_type = "t3.medium"
create_load_balancer = true

Usage:

terraform apply -var-file="dev.tfvars"
terraform apply -var-file="prod.tfvars"

Method 3: Command Line

terraform apply -var="environment=dev" -var="instance_type=t2.micro"

Method 4: Environment Variables

export TF_VAR_environment="dev"
export TF_VAR_instance_type="t2.micro"
terraform apply

Variable Precedence (Highest to Lowest)

  1. Command line -var flags
  2. *.auto.tfvars files (alphabetical order)
  3. terraform.tfvars file
  4. *.auto.tfvars.json files (alphabetical order)
  5. terraform.tfvars.json file
  6. Environment variables (TF_VAR_name)
  7. Variable defaults

Complex Variable Examples

Nested Object Variable

variable "web_server_config" {
  description = "Web server configuration"
  type = object({
    instance_type = string
    count         = number
    monitoring = object({
      enabled           = bool
      detailed_monitoring = bool
    })
    storage = object({
      size = number
      type = string
      encrypted = bool
    })
  })
  
  default = {
    instance_type = "t2.micro"
    count         = 1
    monitoring = {
      enabled           = true
      detailed_monitoring = false
    }
    storage = {
      size      = 20
      type      = "gp3"
      encrypted = true
    }
  }
}

Using Complex Variables

resource "aws_instance" "web" {
  count         = var.web_server_config.count
  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.web_server_config.instance_type
  monitoring    = var.web_server_config.monitoring.enabled
  
  root_block_device {
    volume_size = var.web_server_config.storage.size
    volume_type = var.web_server_config.storage.type
    encrypted   = var.web_server_config.storage.encrypted
  }
}

Variable with Dynamic Types

variable "instance_configs" {
  description = "Map of instance configurations"
  type = map(object({
    instance_type = string
    ami_id        = string
    subnet_id     = string
  }))
  
  default = {
    web = {
      instance_type = "t2.micro"
      ami_id        = "ami-0c55b159cbfafe1d0"
      subnet_id     = "subnet-12345"
    }
    db = {
      instance_type = "t3.medium"
      ami_id        = "ami-0c55b159cbfafe1d0"
      subnet_id     = "subnet-67890"
    }
  }
}

Local Values vs Variables

When to Use Local Values

# variables.tf
variable "environment" {
  type = string
}

variable "project_name" {
  type = string
}

# main.tf
locals {
  # Computed values
  name_prefix = "${var.project_name}-${var.environment}"
  
  # Complex expressions
  is_production = var.environment == "prod"
  
  # Merged configurations
  common_tags = {
    Environment = var.environment
    Project     = var.project_name
    ManagedBy   = "Terraform"
    CreatedAt   = timestamp()
  }
}

Best Practices

1. Organization

# Recommended file structure
ā”œā”€ā”€ variables.tf      # All variable declarations
ā”œā”€ā”€ main.tf          # Main resources
ā”œā”€ā”€ outputs.tf       # Output declarations
ā”œā”€ā”€ terraform.tfvars # Default values
ā”œā”€ā”€ dev.tfvars       # Environment-specific values
└── prod.tfvars      # Environment-specific values

2. Naming Conventions

# Good variable names
variable "vpc_cidr_block" { }      # Clear and specific
variable "enable_monitoring" { }    # Boolean prefix
variable "instance_count" { }       # Descriptive

# Avoid
variable "cidr" { }                # Too generic
variable "monitoring" { }          # Unclear type
variable "count" { }               # Conflicts with built-in

3. Documentation

variable "instance_type" {
  description = <<-EOF
    EC2 instance type for web servers.
    Recommended: t2.micro for dev, t3.medium for prod.
    See: https://aws.amazon.com/ec2/instance-types/
  EOF
  type        = string
  default     = "t2.micro"
}

4. Validation Best Practices

variable "environment" {
  description = "Environment name"
  type        = string
  
  validation {
    condition = contains(["dev", "staging", "prod"], var.environment)
    error_message = <<-EOF
      Environment must be one of: dev, staging, prod.
      Current value: ${var.environment}
    EOF
  }
}

5. Default Values

# Provide sensible defaults for development
variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t2.micro"  # Safe, cheap default
}

# No default for required environment-specific values
variable "database_password" {
  description = "Database password"
  type        = string
  sensitive   = true
  # No default - force explicit setting
}

Testing Variable Configurations

Validate Configuration

terraform validate

Test with Different Values

# Test development configuration
terraform plan -var-file="dev.tfvars"

# Test production configuration
terraform plan -var-file="prod.tfvars"

# Test edge cases
terraform plan -var="instance_count=0"

Variable Testing Script

#!/bin/bash
# test-variables.sh

echo "Testing variable configurations..."

# Test development
echo "Testing development configuration..."
terraform plan -var-file="dev.tfvars" -out=dev.plan

# Test production
echo "Testing production configuration..."
terraform plan -var-file="prod.tfvars" -out=prod.plan

# Test with minimum values
echo "Testing minimum configuration..."
terraform plan \
  -var="environment=dev" \
  -var="instance_count=0" \
  -out=min.plan

echo "All tests completed. Review plan files."

Key Takeaways

  • Variables make configurations reusable and flexible
  • Use appropriate variable types for data validation
  • Implement validation rules to catch errors early
  • Mark sensitive variables appropriately
  • Use multiple methods to provide variable values
  • Follow naming conventions and organization best practices
  • Document variables thoroughly with descriptions
  • Test configurations with different variable values

Next Steps

  1. Complete Tutorial 7: Query Data Sources
  2. Learn about output values and their usage
  3. Explore local values for computed expressions
  4. Practice with complex variable structures

Additional Resources