Skip to main content

Command Palette

Search for a command to run...

Terraform Variables: Input vs Output vs Local Variables

Updated
11 min read
Terraform Variables: Input vs Output vs Local Variables
I

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:

  1. Create the random_string.suffix

  2. Compute local.full_bucket_name

  3. Create 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_name anywhere 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.

Terraform

Part 3 of 5

In this series, I will share my journey of learning terraform and implementing using AWS resources. I would try my best to keep the contents simple and easy to follow. Hope you enjoy!

Up next

Terraform Type Constraints

Prerequisites: Terraform Variables In the previous blog, we explored how Terraform variables make your infrastructure code reusable and flexible. We defined variables, gave them defaults, and passed