Skip to main content

Chapter 3: Terraform Variables and Locals

Hardcoding values works for demos, not production. Variables make your code flexible. Locals make it DRY (Don’t Repeat Yourself).

Basic Variable Types

Terraform has three basic types: string, number, and bool.

variable "name" {
type = string
description = "User name"
default = "World"
}
variable "counts" {
type = number
default = 5
}
variable "enabled" {
type = bool
default = true
}

Use them:

resource "local_file" "example" {
content = "Hello, ${var.name}! Count: ${var.counts}, Enabled: ${var.enabled}"
filename = "output.txt"
}
🚧
You cannot use reserved words like count as variable name.

Change values:

terraform apply -var="name=Alice" -var="counts=10"
Apply Variable and other values in Terraform.
Apply Variable

Always add description. Future you will thank you.

Advanced Variable Types

Real infrastructure needs complex data structures.

Lists

Ordered collections of values:

variable "availability_zones" {
	type = list(string)
	default = ["us-west-2a", "us-west-2b", "us-west-2c"]
}

Access elements:

locals {
	first_az = var.availability_zones[0] # "us-west-2a"
	all_zones = join(", ", var.availability_zones)
}

Use in resources:

resource "aws_subnet" "public" {
	count = length(var.availability_zones)
	availability_zone = var.availability_zones[count.index]
	# ... other config
}

Maps

Key-value pairs:

variable "instance_types" {
	type = map(string)
	default = {
		dev = "t2.micro"
		prod = "t2.large"
	}
}

Access values:

resource "aws_instance" "app" {
instance_type = var.instance_types["prod"]
# Or with lookup function
instance_type = lookup(var.instance_types, var.environment, "t2.micro")
}

Objects

Structured data with different types:

variable "database_config" {
	type = object({
	instance_class = string
	allocated_storage = number
	multi_az = bool
	backup_retention = number
	})
	default = {
	instance_class = "db.t3.micro"
	allocated_storage = 20
	multi_az = false
	backup_retention = 7
	}
}

Use in resources:

resource "aws_db_instance" "main" {
	instance_class = var.database_config.instance_class
	allocated_storage = var.database_config.allocated_storage
	multi_az = var.database_config.multi_az
	backup_retention_period = var.database_config.backup_retention
}

List of Objects

The power combo - multiple structured items:

variable "servers" {
	type = map(object({
		size = string
		disk = number
	}))
	default = {
		web-1 = { size = "t2.micro", disk = 20 }
		web-2 = { size = "t2.small", disk = 30 }
	}
}
resource "aws_instance" "servers" {
	for_each = var.servers
	instance_type = each.value.size

	tags = {
		Name = each.key
	}
	root_block_device {
		volume_size = each.value.disk
	}
}

Sets and Tuples

Set - Like list but unordered and unique:

variable "allowed_ips" {
	type = set(string)
	default = ["10.0.0.1", "10.0.0.2"]
}

Tuple - Fixed-length list with specific types:

variable "server_config" {
	type = tuple([string, number, bool])
	default = ["t2.micro", 20, true]
}

Rarely used. Stick with lists and maps for most cases.

Variable Validation

Add rules to validate input:

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

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

variable "instance_count" {
	type = number
	default = 1

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

Catches errors before Terraform runs.

Validation Check Terraform
Validation Check

Sensitive Variables

Mark secrets as sensitive:

variable "db_password" {
	type = string
	sensitive = true
}

Won’t appear in logs or plan output. Still stored in state though (encrypt your state!).

Variable Precedence

Multiple ways to set variables. Terraform picks in this order (highest to lowest):

  1. Command line: -var="key=value"
  2. *.auto.tfvars files (alphabetical order)
  3. terraform.tfvars file
  4. Environment variables: TF_VAR_name
  5. Default value in variable block

Setting Variables with Files

Create terraform.tfvars:

environment = "prod"
instance_type = "t2.large"
database_config = {
instance_class = "db.t3.large"
allocated_storage = 100
multi_az = true
backup_retention = 30
}

Run terraform apply - picks up values automatically

Or environment-specific files:

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

Locals: Computed Values

Variables are inputs. Locals are calculated values you use internally.

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

variable "environment" {
	type = string
	default = "dev"
}

locals {
	resource_prefix = "${var.project_name}-${var.environment}"

	common_tags = {
		Project = var.project_name
		Environment = var.environment
		ManagedBy = "Terraform"
	}

	is_production = var.environment == "prod"
	backup_count = local.is_production ? 3 : 1
}
resource "aws_s3_bucket" "data" {
	bucket = "${local.resource_prefix}-data"
	tags = local.common_tags
}

Use var. for variables, local. for locals.

Outputs

Display values after apply:

output "bucket_name" {
	description = "Name of the S3 bucket"
	value = aws_s3_bucket.data.id
}

output "is_production" {
	value = local.is_production
}

output "db_endpoint" {
	value = aws_db_instance.main.endpoint
	sensitive = true # Don't show in logs
}

View outputs:

terraform output
terraform output bucket_name

Real-World Example

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

variable "app_config" {
	type = object({
	instance_type = string
	min_size = number
	})
}

locals {
	common_tags = {
		Environment = var.environment
		ManagedBy = "Terraform"
	}

	# Override for production
	min_size = var.environment == "prod" ? 3 : var.app_config.min_size
}

resource "aws_autoscaling_group" "app" {
	name = "myapp-${var.environment}-asg"
	min_size = local.min_size
	desired_capacity = local.min_size
	tags = [
		for key, value in local.common_tags : {
		key = key
		value = value
		propagate_at_launch = true
		}
	]
}

Quick Reference

Basic types:

variable "name" { type = string }
variable "count" { type = number }
variable "enabled" { type = bool }

Complex types:

variable "zones" { type = list(string) }
variable "types" { type = map(string) }
variable "config" { type = object({ name = string, size = number }) }
variable "servers" { type = map(object({ size = string, disk = number })) }

Validation:

validation {
condition = contains(["dev", "prod"], var.env)
error_message = "Must be dev or prod."
}

Locals and Outputs:

locals { name = "${var.project}-${var.env}" }
output "result" { value = aws_instance.app.id, sensitive = true }

Variables make your code flexible. Complex types model real infrastructure. Locals keep things DRY. Outputs share information.

With variables and locals in your toolkit, you now know how to make your Terraform code flexible and maintainable. But where does Terraform store the information about what it created? And how does it connect to AWS, Azure, or other cloud providers? That’s what we’ll explore next with state management and providers.

Updated on Nov 28, 2025