Terraform Variables: Input vs Output vs Local Variables

Software Engineer with hands-on project experience in building backend systems and web applications using Node.js, TypeScript, React, and Kubernetes.
In previous posts, we set up the AWS provider, wrote our first resources, and learned how Terraform tracks infrastructure. Now it's time to make our code reusable — and that's where variables come in.
The Problem: Why Do We Even Need Variables?
Imagine you're setting up infrastructure for three environments — dev, staging, and production. Without variables, you'd write three separate Terraform files, each nearly identical, just with a few words changed.
That's a maintenance nightmare. Fix a bug in one? You need to fix it in all three.
Think of it like this: variables in Terraform work exactly like variables in any programming language. Instead of hardcoding "production" in 20 different places, you define it once and reference it everywhere. Change it in one place, everything updates.
Terraform has three types of variables, each serving a different purpose:
Input Variables | Values you pass into a process or module | Like function parameters |
Local Variables | Values computed internally during execution | Like local variables in code |
Output Variables | Values you receive as results from the process | Like function return values |
Let's walk through each one.
Part 1: Input Variables
What Are They?
Input variables are how you pass information into your Terraform configuration. Instead of writing "staging" directly in your resource, you write var.environment, and let whoever runs terraform apply decide what the value should be.
Without variables:
resource "aws_s3_bucket" "demo" {
bucket = "my-app-bucket"
tags = {
Environment = "staging" # hardcoded — bad!
}
}
With variables:
variable "environment" {
type = string
default = "staging"
}
resource "aws_s3_bucket" "demo" {
bucket = "my-app-bucket"
tags = {
Environment = var.environment # flexible — good!
}
}
Now you can reuse the exact same code for dev, staging, and production just by changing the environment value.
Anatomy of an Input Variable
variable "environment" {
description = "The deployment environment (dev, staging, production)"
type = string
default = "staging"
}
| Field | Purpose |
|---|---|
description |
Documents what the variable is for |
type |
Enforces the data type (string, number, bool, list, map) |
default |
Optional fallback — if no value is provided, this is used |
If you don't set a default, Terraform will prompt you to enter a value when you run terraform plan or terraform apply.
Using Variables Inside String Literals
Sometimes you need a variable inside a string, not just as a standalone value. Use the ${} syntax for that:
variable "environment" {
type = string
default = "staging"
}
variable "app_name" {
type = string
default = "myapp"
}
resource "aws_s3_bucket" "demo" {
# Combining two variables inside a string
bucket = "\({var.environment}-\){var.app_name}-bucket"
# Result: "staging-myapp-bucket"
}
This is called string interpolation — you're embedding a variable's value inside a larger string.
How to Provide Values to Input Variables
There are four ways, listed from lowest to highest priority:
Method 1 — Default value in variables.tf:
variable "environment" {
default = "staging"
}
Method 2 — terraform.tfvars file (auto-loaded, most common):
# terraform.tfvars
environment = "demo"
bucket_name = "terraform-demo-bucket"
Method 3 — Environment variable:
export TF_VAR_environment="development"
terraform plan
Method 4 — Command line (highest priority, overrides everything):
terraform plan -var="environment=production"
Walkthrough: Seeing Input Variables in Action
Step 1: Create variables.tf:
variable "environment" {
description = "Deployment environment"
type = string
default = "staging"
}
variable "bucket_name" {
description = "Base name for the S3 bucket"
type = string
default = "my-terraform-bucket"
}
Step 2: Create terraform.tfvars:
environment = "demo"
bucket_name = "terraform-demo-bucket"
Step 3: Reference in main.tf:
resource "aws_s3_bucket" "demo" {
bucket = "\({var.environment}-\){var.bucket_name}"
tags = {
Environment = var.environment
}
}
Step 4: Test variable precedence:
# Uses terraform.tfvars → environment = "demo"
terraform plan
# Command line overrides tfvars → environment = "production"
terraform plan -var="environment=production"
# Hide tfvars to fall back to defaults → environment = "staging"
mv terraform.tfvars terraform.tfvars.bak
terraform plan
mv terraform.tfvars.bak terraform.tfvars # restore
Part 2: Local Variables
What Are They?
Local variables are internal computed values, you derive them from other variables or resources, and then reuse them across your config. Think of them like local variables inside a function: nobody passes them in, and nobody sees them on the outside. They just help you keep your code clean.
A common use case: you have an environment and a bucket_name, and you want a full bucket name that combines them both with a random suffix. Instead of rewriting that formula in five places, you compute it once in locals.
Syntax
locals {
name_of_local = <expression>
}
And you reference it with local.name_of_local (note: local, not locals).
Real Use Case: Tags and Computed Names
Here's where locals shine. Every AWS resource in a project should have consistent tags — the environment, the project name, the owner. Instead of copy-pasting these tags into every resource, define them once as a local:
# locals.tf
locals {
# Compute a consistent bucket name from input variables + random suffix
full_bucket_name = "\({var.environment}-\){var.bucket_name}-${random_string.suffix.result}"
# Define tags once, use them everywhere
common_tags = {
Environment = var.environment
Project = "MyApp"
Owner = "DevOps-Team"
ManagedBy = "Terraform"
}
}
Now in main.tf, every resource gets the same tags with zero duplication:
resource "aws_s3_bucket" "demo" {
bucket = local.full_bucket_name
tags = local.common_tags # ← one line, consistent tags
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = local.common_tags # ← same tags, no copy-paste
}
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = local.common_tags # ← still same tags
}
Now if you need to add a CostCenter tag, you add it once in locals.tf and all three resources get it automatically.
Walkthrough: Seeing Locals in Action
Step 1: Add a random suffix resource to main.tf (so bucket names are globally unique):
resource "random_string" "suffix" {
length = 6
special = false
upper = false
}
Step 2: Create locals.tf:
locals {
full_bucket_name = "\({var.environment}-\){var.bucket_name}-${random_string.suffix.result}"
common_tags = {
Environment = var.environment
Project = "Terraform-Demo"
Owner = "DevOps-Team"
ManagedBy = "Terraform"
}
}
Step 3: Use locals in main.tf:
resource "aws_s3_bucket" "demo" {
bucket = local.full_bucket_name
tags = local.common_tags
}
Step 4: Run terraform plan
terraform plan
At this stage Terraform cannot yet compute the bucket name, because the random suffix hasn't been generated.
You will see:
+ bucket = (known after apply)
This happens because:
local.full_bucket_name
↓
random_string.suffix.result
↓
generated only during apply
Step 5: Apply the configuration
terraform apply
Terraform will:
Create the
random_string.suffixCompute
local.full_bucket_nameCreate the S3 bucket
Example result:
aws_s3_bucket.demo: Creation complete
Bucket created:
demo-terraform-demo-bucket-a3f9k2
Step 6: Run terraform plan again
terraform plan
Now Terraform already knows the random string from the state file, so it can compute the bucket name.
You will see something like:
No changes. Your infrastructure matches the configuration.
Terraform now knows the values stored in state, including:
bucket = "demo-terraform-demo-bucket-a3f9k2"
The key insight: You never set
full_bucket_nameanywhere explicitly. Terraform computed it from your other variables. That's the power of locals.
Part 3: Output Variables
What Are They?
Output variables are what Terraform tells you after it finishes creating your infrastructure. Think of them like the return value of a function — after terraform apply completes, outputs show you the important information you need: bucket names, IP addresses, ARNs, URLs.
They're also how one Terraform module shares data with another module, but that's a topic for a future post.
Syntax
output "output_name" {
description = "What this output shows"
value = resource.resource_name.attribute
}
What Can You Output?
You can output values from any of the three variable types:
# outputs.tf
# Output a resource attribute (most common)
output "bucket_name" {
description = "The final name of the S3 bucket"
value = aws_s3_bucket.demo.bucket
}
output "bucket_arn" {
description = "The ARN of the S3 bucket"
value = aws_s3_bucket.demo.arn
}
# Output an input variable (to confirm what was used)
output "environment" {
description = "The environment that was deployed"
value = var.environment
}
# Output a local variable (to see computed values)
output "applied_tags" {
description = "Tags applied to all resources"
value = local.common_tags
}
Viewing Outputs
# After terraform apply, outputs are printed automatically.
# You can also query them anytime:
terraform output # show all outputs
terraform output bucket_name # show a specific output
terraform output -raw bucket_name # show value without quotes (useful in scripts)
terraform output -json # show all outputs as JSON
Example output after terraform apply:
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
applied_tags = {
"Environment" = "demo"
"ManagedBy" = "Terraform"
"Owner" = "DevOps-Team"
"Project" = "Terraform-Demo"
}
bucket_arn = "arn:aws:s3:::demo-terraform-demo-bucket-a3f9k2"
bucket_name = "demo-terraform-demo-bucket-a3f9k2"
environment = "demo"
Walkthrough: Seeing Outputs in Action
Step 1: Create outputs.tf:
output "bucket_name" {
description = "Name of the S3 bucket"
value = aws_s3_bucket.demo.bucket
}
output "bucket_arn" {
description = "ARN of the S3 bucket"
value = aws_s3_bucket.demo.arn
}
output "environment" {
description = "Deployed environment"
value = var.environment
}
output "applied_tags" {
description = "Tags applied to resources"
value = local.common_tags
}
⚠️ Note: Should give proper name for values. (resource_name.local_name)
Step 2: Apply and view outputs:
terraform apply -auto-approve
# Then use outputs in a shell script:
echo "Bucket deployed: $(terraform output -raw bucket_name)"
echo "Environment: $(terraform output -raw environment)"
The Complete File Structure
.
├── main.tf # S3 bucket + random_string resources
├── variables.tf # Input variable definitions
├── locals.tf # Computed/reusable internal values
├── outputs.tf # What to display after deployment
├── provider.tf # AWS provider config
└── terraform.tfvars # Your actual variable values
Each file has a clear, single responsibility. You'll thank yourself later when your project grows.
Complete Demonstration: Full End-to-End
Here's the complete code for everything discussed in this post.
provider.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.0"
}
}
}
provider "aws" {
region = "ap-south-1"
}
variables.tf
variable "environment" {
description = "Deployment environment (dev, staging, production)"
type = string
default = "staging"
}
variable "bucket_name" {
description = "Base name for the S3 bucket"
type = string
default = "my-terraform-bucket"
}
terraform.tfvars
environment = "demo"
bucket_name = "terraform-demo-bucket"
locals.tf
locals {
full_bucket_name = "\({var.environment}-\){var.bucket_name}-${random_string.suffix.result}"
common_tags = {
Environment = var.environment
Project = "Terraform-Demo"
Owner = "DevOps-Team"
ManagedBy = "Terraform"
}
}
main.tf
resource "random_string" "suffix" {
length = 6
special = false
upper = false
}
resource "aws_s3_bucket" "demo" {
bucket = local.full_bucket_name
tags = local.common_tags
}
outputs.tf
output "bucket_name" {
description = "Name of the created S3 bucket"
value = aws_s3_bucket.demo.bucket
}
output "bucket_arn" {
description = "ARN of the created S3 bucket"
value = aws_s3_bucket.demo.arn
}
output "environment" {
description = "Environment that was deployed"
value = var.environment
}
output "applied_tags" {
description = "Tags applied to all resources"
value = local.common_tags
}
Run it:
terraform init
terraform plan
terraform apply -auto-approve
terraform output
Destroy the resources (Cleanup)
After finishing the experiment, destroy the created infrastructure:
terraform destroy -auto-approve
⚠️ Note: This is to save cost. Always destroy the resources after experimenting, especially when you are learning, to avoid unnecessary cloud charges. 💰
Found this helpful? Follow along with the full Terraform × AWS series — we're building real skills, one concept at a time.



