Variables and Outputs in Terraform
Learn how to parameterize configurations with input variables and expose information with outputs
TLDR: Variables make configurations reusable - define them with type and default, set them via CLI, files, or environment variables. Outputs expose values after applying - like IP addresses or resource IDs. Use validation rules to catch errors early and sensitive flags to hide secrets.
Variables and outputs are how you make Terraform configurations flexible and informative. Variables let you customize behavior without changing code. Outputs extract information about created resources.
Input Variables
Variables parameterize your configuration so you can use it in different scenarios without modification.
Basic Variable Declaration
Declare variables with the variable block:
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "instance_count" {
description = "Number of instances to create"
type = number
default = 1
}
variable "enable_monitoring" {
description = "Enable detailed monitoring"
type = bool
default = false
}
Use variables in your configuration:
resource "aws_instance" "web" {
count = var.instance_count
ami = "ami-0c55b159cbfafe1f0"
instance_type = var.instance_type
monitoring = var.enable_monitoring
tags = {
Name = "web-${count.index}"
}
}
Variable Types
Terraform supports several types:
Primitive types:
variable "string_example" {
type = string
default = "hello"
}
variable "number_example" {
type = number
default = 42
}
variable "bool_example" {
type = bool
default = true
}
Collection types:
variable "availability_zones" {
type = list(string)
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
variable "instance_tags" {
type = map(string)
default = {
Environment = "development"
Project = "web-app"
}
}
variable "allowed_ports" {
type = set(number)
default = [80, 443, 8080]
}
Structural types:
variable "server_config" {
type = object({
instance_type = string
disk_size = number
enable_backup = bool
})
default = {
instance_type = "t3.micro"
disk_size = 20
enable_backup = false
}
}
variable "subnet_configs" {
type = list(object({
cidr_block = string
availability_zone = string
public = bool
}))
default = [
{
cidr_block = "10.0.1.0/24"
availability_zone = "us-east-1a"
public = true
},
{
cidr_block = "10.0.2.0/24"
availability_zone = "us-east-1b"
public = false
}
]
}
Using complex types:
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = var.server_config.instance_type
root_block_device {
volume_size = var.server_config.disk_size
}
tags = var.instance_tags
}
resource "aws_subnet" "subnets" {
for_each = { for idx, subnet in var.subnet_configs : idx => 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.public
}
Setting Variable Values
There are several ways to provide values:
Command line:
terraform apply -var="instance_type=t3.large" -var="instance_count=3"
Variable files:
Create terraform.tfvars:
instance_type = "t3.large"
instance_count = 3
enable_monitoring = true
instance_tags = {
Environment = "production"
Project = "web-app"
Team = "platform"
}
Terraform automatically loads terraform.tfvars and *.auto.tfvars files.
For environment-specific configs:
# dev.tfvars
instance_type = "t3.micro"
instance_count = 1
# prod.tfvars
instance_type = "t3.large"
instance_count = 5
Apply with:
terraform apply -var-file="dev.tfvars"
terraform apply -var-file="prod.tfvars"
Environment variables:
export TF_VAR_instance_type="t3.large"
export TF_VAR_instance_count=3
terraform apply
Terraform reads any environment variable starting with TF_VAR_.
Interactive input:
If a variable has no default and you don't provide a value, Terraform prompts:
$ terraform apply
var.instance_type
EC2 instance type
Enter a value: t3.micro
Variable Priority
When multiple sources provide values, Terraform uses this precedence (highest to lowest):
- Command-line flags (
-var) *.auto.tfvarsfiles (alphabetical order)terraform.tfvarsfile- Environment variables (
TF_VAR_*) - Default values in variable declarations
Variable Validation
Add validation rules to catch errors before infrastructure changes:
variable "instance_type" {
type = string
description = "EC2 instance type"
validation {
condition = can(regex("^t3\\.", var.instance_type))
error_message = "Instance type must be from the t3 family."
}
}
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 "disk_size" {
type = number
description = "Root disk size in GB"
validation {
condition = var.disk_size >= 20 && var.disk_size <= 1000
error_message = "Disk size must be between 20 and 1000 GB."
}
}
If validation fails, Terraform shows the error before making changes:
Error: Invalid value for variable
on variables.tf line 12:
12: variable "environment" {
Environment must be dev, staging, or prod.
Sensitive Variables
Mark variables containing secrets:
variable "database_password" {
type = string
description = "Database admin password"
sensitive = true
}
variable "api_key" {
type = string
sensitive = true
}
Terraform won't show these values in logs or output:
# aws_db_instance.main will be created
+ resource "aws_db_instance" "main" {
+ password = (sensitive value)
...
}
Still never commit secrets to version control. Use environment variables or secret management systems.
Output Values
Outputs expose information about your infrastructure after Terraform creates it.
Basic Outputs
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
}
output "instance_id" {
description = "ID of the EC2 instance"
value = aws_instance.web.id
}
output "instance_public_ip" {
description = "Public IP address"
value = aws_instance.web.public_ip
}
output "instance_private_ip" {
description = "Private IP address"
value = aws_instance.web.private_ip
}
After terraform apply, outputs appear:
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
instance_id = "i-0123456789abcdef"
instance_private_ip = "10.0.1.25"
instance_public_ip = "54.123.45.67"
View outputs anytime:
terraform output
Get a specific output:
terraform output instance_public_ip
Get JSON format:
terraform output -json
Complex Outputs
Output collections and structures:
resource "aws_instance" "web" {
count = 3
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
}
output "all_instance_ids" {
description = "IDs of all instances"
value = aws_instance.web[*].id
}
output "all_instance_ips" {
description = "Public IPs of all instances"
value = aws_instance.web[*].public_ip
}
output "instance_details" {
description = "Detailed information about instances"
value = {
ids = aws_instance.web[*].id
public_ips = aws_instance.web[*].public_ip
private_ips = aws_instance.web[*].private_ip
}
}
Output:
Outputs:
all_instance_ids = [
"i-0123456789abcdef",
"i-0123456789abcdeg",
"i-0123456789abcdeh",
]
instance_details = {
"ids" = [
"i-0123456789abcdef",
"i-0123456789abcdeg",
"i-0123456789abcdeh",
]
"private_ips" = [
"10.0.1.25",
"10.0.1.26",
"10.0.1.27",
]
"public_ips" = [
"54.123.45.67",
"54.123.45.68",
"54.123.45.69",
]
}
Sensitive Outputs
Hide sensitive information in output:
resource "aws_db_instance" "main" {
identifier = "mydb"
engine = "postgres"
password = var.database_password
# ... other config ...
}
output "database_endpoint" {
description = "Database connection endpoint"
value = aws_db_instance.main.endpoint
}
output "database_password" {
description = "Database password"
value = aws_db_instance.main.password
sensitive = true
}
Sensitive outputs don't appear in normal output:
Outputs:
database_endpoint = "mydb.abc123.us-east-1.rds.amazonaws.com:5432"
database_password = <sensitive>
Retrieve the value explicitly:
terraform output database_password
Or with -json to parse it:
terraform output -json database_password | jq -r
Outputs from Modules
Outputs are how modules return values. If you use a VPC module:
module "vpc" {
source = "./modules/vpc"
cidr_block = "10.0.0.0/16"
name = "main-vpc"
}
# Use the module's outputs
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
subnet_id = module.vpc.public_subnet_id # Output from module
}
# Re-export module outputs
output "vpc_id" {
description = "ID of the VPC"
value = module.vpc.vpc_id
}
Practical Example: Flexible Infrastructure
Here's a complete example using variables and outputs:
# variables.tf
variable "environment" {
description = "Environment name"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Must be dev, staging, or prod."
}
}
variable "project" {
description = "Project name"
type = string
default = "webapp"
}
variable "instance_configs" {
description = "Configuration for each instance type"
type = map(object({
instance_type = string
count = number
disk_size = number
}))
default = {
dev = {
instance_type = "t3.micro"
count = 1
disk_size = 20
}
staging = {
instance_type = "t3.small"
count = 2
disk_size = 30
}
prod = {
instance_type = "t3.large"
count = 5
disk_size = 50
}
}
}
variable "allowed_ssh_cidrs" {
description = "CIDR blocks allowed to SSH"
type = list(string)
default = []
}
# main.tf
locals {
config = var.instance_configs[var.environment]
common_tags = {
Environment = var.environment
Project = var.project
ManagedBy = "terraform"
}
}
resource "aws_security_group" "web" {
name = "${var.project}-${var.environment}-sg"
description = "Security group for ${var.environment}"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
dynamic "ingress" {
for_each = length(var.allowed_ssh_cidrs) > 0 ? [1] : []
content {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = var.allowed_ssh_cidrs
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(local.common_tags, {
Name = "${var.project}-${var.environment}-sg"
})
}
resource "aws_instance" "web" {
count = local.config.count
ami = data.aws_ami.ubuntu.id
instance_type = local.config.instance_type
vpc_security_group_ids = [aws_security_group.web.id]
subnet_id = aws_subnet.public.id
root_block_device {
volume_size = local.config.disk_size
}
tags = merge(local.common_tags, {
Name = "${var.project}-${var.environment}-web-${count.index + 1}"
})
}
# outputs.tf
output "environment" {
description = "Deployment environment"
value = var.environment
}
output "instance_count" {
description = "Number of instances created"
value = local.config.count
}
output "instance_ids" {
description = "IDs of all web instances"
value = aws_instance.web[*].id
}
output "instance_ips" {
description = "Public IP addresses"
value = aws_instance.web[*].public_ip
}
output "ssh_commands" {
description = "SSH commands for each instance"
value = [
for idx, instance in aws_instance.web :
"ssh ubuntu@${instance.public_ip} # ${var.project}-${var.environment}-web-${idx + 1}"
]
}
output "load_balancer_endpoint" {
description = "Load balancer endpoint"
value = aws_lb.web.dns_name
depends_on = [aws_lb.web]
}
Use it for different environments:
# Development
terraform apply -var="environment=dev"
# Staging
terraform apply -var="environment=staging" -var="allowed_ssh_cidrs=[\"203.0.113.0/24\"]"
# Production
terraform apply -var-file="prod.tfvars"
Where prod.tfvars contains:
environment = "prod"
project = "webapp"
allowed_ssh_cidrs = [
"203.0.113.0/24", # Office network
"198.51.100.0/24" # VPN network
]
This setup gives you:
- Environment-specific instance counts and sizes
- Validation preventing typos in environment names
- Flexible SSH access control
- Detailed outputs for connecting to resources
- Reusable configuration across environments
Understanding variables and outputs lets you build flexible, reusable Terraform configurations. Next, we'll explore modules - how to organize and share Terraform code across projects.
Found an issue?