DEV Community

Cover image for Terraform Variables and Outputs: Making Your Infrastructure Flexible

Terraform Variables and Outputs: Making Your Infrastructure Flexible

πŸ‘‹ Hey there, tech enthusiasts!

I'm Sarvar, a Cloud Architect with a passion for transforming complex technological challenges into elegant solutions. With extensive experience spanning Cloud Operations (AWS & Azure), Data Operations, Analytics, DevOps, and Generative AI, I've had the privilege of architecting solutions for global enterprises that drive real business impact. Through this article series, I'm excited to share practical insights, best practices, and hands-on experiences from my journey in the tech world. Whether you're a seasoned professional or just starting out, I aim to break down complex concepts into digestible pieces that you can apply in your projects.


"The difference between a script and reusable infrastructure code is variables."


🎯 Welcome Back!

Remember in Article 3 when you created that S3 bucket? You hardcoded the bucket name directly in the code:

resource "aws_s3_bucket" "my_first_bucket" {
  bucket = "terraform-first-bucket-yourname-2026"  # Hardcoded!
}
Enter fullscreen mode Exit fullscreen mode

That works... once. But what if you need:

  • 10 buckets with different names?
  • Same code for dev, staging, and production?
  • To let your team customize configurations?
  • To reuse this code in different projects?

Today, you'll learn how to make your Terraform code flexible and reusable using variables and outputs.

By the end of this article, you'll:

  • βœ… Convert hardcoded values to variables
  • βœ… Use all variable types (string, number, bool, list, map)
  • βœ… Work with terraform.tfvars files
  • βœ… Create outputs to expose information
  • βœ… Build dev/prod configurations from the same code
  • βœ… Use variable validation and sensitive values

Time Required: 30 minutes

Cost: $0 (using free tier resources)

Difficulty: Beginner-friendly

Let's make your code reusable! πŸš€


πŸ’” The Problem: Hardcoded Hell

The Painful Reality

Scenario 1: Multiple Environments

# dev.tf
resource "aws_s3_bucket" "app" {
  bucket = "myapp-dev-bucket"
}

# staging.tf (copy-paste and change)
resource "aws_s3_bucket" "app" {
  bucket = "myapp-staging-bucket"
}

# prod.tf (copy-paste and change again)
resource "aws_s3_bucket" "app" {
  bucket = "myapp-prod-bucket"
}
Enter fullscreen mode Exit fullscreen mode

Problems:

  • ❌ Three copies of the same code
  • ❌ Change one thing = update three files
  • ❌ High risk of mistakes
  • ❌ Nightmare to maintain

Scenario 2: Team Collaboration

Developer 1: "I need the bucket name to be X"
Developer 2: "I need it to be Y"
DevOps: "Production needs Z"
Enter fullscreen mode Exit fullscreen mode

Everyone edits the same .tf file = merge conflicts and chaos!

There's a better way. Enter variables.


🌟 What Are Variables?

Simple Definition

Variables are like function parameters for your infrastructure code.

Think of it like this:

Without Variables (Hardcoded):

function createBucket() {
  return "my-hardcoded-bucket-name";
}
Enter fullscreen mode Exit fullscreen mode

With Variables (Flexible):

function createBucket(bucketName) {
  return bucketName;
}

createBucket("dev-bucket");    // Different uses
createBucket("prod-bucket");   // Same code!
Enter fullscreen mode Exit fullscreen mode

In Terraform:

# Define the variable
variable "bucket_name" {
  description = "Name of the S3 bucket"
  type        = string
}

# Use the variable
resource "aws_s3_bucket" "app" {
  bucket = var.bucket_name  # Reference with var.
}
Enter fullscreen mode Exit fullscreen mode

Same code, different values! That's the power of variables.


πŸ› οΈ Hands-On: Your First Variable

Let's convert our hardcoded S3 bucket to use variables.

Step 1: Create Project Directory

mkdir -p ~/terraform-variables-demo
cd ~/terraform-variables-demo
Enter fullscreen mode Exit fullscreen mode

Step 2: Create variables.tf

Create a new file called variables.tf:

nano variables.tf
Enter fullscreen mode Exit fullscreen mode

Add this code:

# String variable - for text values
variable "aws_region" {
  description = "AWS region where resources will be created"
  type        = string
  default     = "us-east-1"
}

variable "bucket_name" {
  description = "Name of the S3 bucket (must be globally unique)"
  type        = string
  # No default - user must provide this value
}

# Map variable - for key-value pairs like tags
variable "common_tags" {
  description = "Common tags to apply to all resources"
  type        = map(string)
  default = {
    Environment = "Development"
    ManagedBy   = "Terraform"
    Project     = "Learning"
  }
}
Enter fullscreen mode Exit fullscreen mode

Understanding the syntax:

  • description - Explains what the variable is for (always add this!)
  • type - What kind of value (string, number, bool, list, map)
  • default - Optional default value (if not provided, user must supply it)

Step 3: Create main.tf

nano main.tf
Enter fullscreen mode Exit fullscreen mode

Add this code:

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

# Use variable in provider
provider "aws" {
  region = var.aws_region  # Reference variable with var.
}

# Use variables in resource
resource "aws_s3_bucket" "example" {
  bucket = var.bucket_name  # Variable for bucket name

  # Merge common_tags with specific tags
  tags = merge(
    var.common_tags,
    {
      Name = var.bucket_name
    }
  )
}
Enter fullscreen mode Exit fullscreen mode

Notice: We reference variables with var.variable_name

Step 4: Initialize and Test

terraform init
terraform plan
Enter fullscreen mode Exit fullscreen mode

Terraform will prompt you:

var.bucket_name
  Name of the S3 bucket (must be globally unique)

  Enter a value: 
Enter fullscreen mode Exit fullscreen mode

Type: my-first-variable-bucket-yourname-2026 and press Enter.

You'll see the plan:

Terraform will perform the following actions:

  # aws_s3_bucket.example will be created
  + resource "aws_s3_bucket" "example" {
      + bucket = "my-first-variable-bucket-yourname-2026"
      + tags   = {
          + "Environment" = "Development"
          + "ManagedBy"   = "Terraform"
          + "Name"        = "my-first-variable-bucket-yourname-2026"
          + "Project"     = "Learning"
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.
Enter fullscreen mode Exit fullscreen mode

Don't apply yet! Let's learn better ways to pass variables.


πŸ“ Three Ways to Pass Variables

Method 1: Command Line (What We Just Did)

terraform plan
# Terraform prompts for bucket_name
Enter fullscreen mode Exit fullscreen mode

Pros: Interactive

Cons: Tedious for multiple variables

Method 2: Command Line Flags

terraform plan -var="bucket_name=my-bucket-2026"
Enter fullscreen mode Exit fullscreen mode

Pros: Good for CI/CD pipelines

Cons: Long commands with many variables

Method 3: terraform.tfvars File (Best for Most Cases)

Create terraform.tfvars:

nano terraform.tfvars
Enter fullscreen mode Exit fullscreen mode

Add this:

bucket_name = "my-variable-bucket-yourname-2026"
aws_region  = "us-east-1"

common_tags = {
  Environment = "Development"
  ManagedBy   = "Terraform"
  Project     = "Variables Demo"
  Owner       = "YourName"
}
Enter fullscreen mode Exit fullscreen mode

Now run:

terraform plan
Enter fullscreen mode Exit fullscreen mode

No prompts! Terraform automatically loads terraform.tfvars.

Apply it:

terraform apply
Enter fullscreen mode Exit fullscreen mode

Type yes when prompted.

πŸŽ‰ Success! You just created infrastructure using variables!


🎨 Variable Types: The Complete Guide

Terraform supports multiple variable types. Let's explore them all!

Update variables.tf

Replace your variables.tf with this comprehensive version:

# 1. STRING - Text values
variable "aws_region" {
  description = "AWS region where resources will be created"
  type        = string
  default     = "us-east-1"
}

variable "bucket_name" {
  description = "Name of the S3 bucket (must be globally unique)"
  type        = string
}

variable "bucket_prefix" {
  description = "Prefix for multiple bucket names"
  type        = string
  default     = "terraform-demo"
}

# 2. NUMBER - Numeric values
variable "bucket_count" {
  description = "Number of S3 buckets to create"
  type        = number
  default     = 2

  # Validation rule
  validation {
    condition     = var.bucket_count >= 1 && var.bucket_count <= 5
    error_message = "Bucket count must be between 1 and 5."
  }
}

# 3. BOOLEAN - True/False values
variable "enable_versioning" {
  description = "Enable versioning on the S3 bucket"
  type        = bool
  default     = false
}

# 4. LIST - Ordered collection of values
variable "allowed_ips" {
  description = "List of IP addresses allowed to access the bucket"
  type        = list(string)
  default     = []
}

# 5. MAP - Key-value pairs
variable "common_tags" {
  description = "Common tags to apply to all resources"
  type        = map(string)
  default = {
    Environment = "Development"
    ManagedBy   = "Terraform"
    Project     = "Learning"
  }
}

# 6. ENVIRONMENT - With validation
variable "environment" {
  description = "Environment name (dev, staging, prod)"
  type        = string
  default     = "dev"

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

Update main.tf to Use All Variable Types

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

provider "aws" {
  region = var.aws_region
}

# Main S3 bucket with variables
resource "aws_s3_bucket" "example" {
  bucket = var.bucket_name

  tags = merge(
    var.common_tags,
    {
      Name = var.bucket_name
    }
  )
}

# Conditional resource - only created if enable_versioning is true
resource "aws_s3_bucket_versioning" "example" {
  count  = var.enable_versioning ? 1 : 0  # Conditional creation
  bucket = aws_s3_bucket.example.id

  versioning_configuration {
    status = "Enabled"
  }
}

# Multiple buckets using count (number variable)
resource "aws_s3_bucket" "multiple" {
  count  = var.bucket_count
  bucket = "${var.bucket_prefix}-${count.index + 1}-yourname-2026"

  tags = var.common_tags
}
Enter fullscreen mode Exit fullscreen mode

What's happening here:

  • var.enable_versioning ? 1 : 0 - Conditional: if true, create 1 resource; if false, create 0
  • count = var.bucket_count - Create multiple resources based on number
  • count.index - Access the current index (0, 1, 2...)
  • merge() - Combine two maps together

πŸ“€ Outputs: Exposing Information

Variables let data IN. Outputs let data OUT.

Why Outputs Matter

Use cases:

  • Display important information after terraform apply
  • Pass values to other Terraform modules
  • Use in scripts or CI/CD pipelines
  • Share information with team members

Create outputs.tf

nano outputs.tf
Enter fullscreen mode Exit fullscreen mode

Add this code:

# Basic outputs
output "bucket_name" {
  description = "Name of the created S3 bucket"
  value       = aws_s3_bucket.example.id
}

output "bucket_arn" {
  description = "ARN of the S3 bucket"
  value       = aws_s3_bucket.example.arn
}

output "bucket_region" {
  description = "Region where the bucket was created"
  value       = aws_s3_bucket.example.region
}

output "bucket_domain_name" {
  description = "Domain name of the bucket"
  value       = aws_s3_bucket.example.bucket_domain_name
}

# Multiple buckets output
output "multiple_bucket_names" {
  description = "Names of all created buckets"
  value       = aws_s3_bucket.multiple[*].id
}

# Conditional output
output "versioning_enabled" {
  description = "Whether versioning is enabled"
  value       = var.enable_versioning
}

# Map output
output "bucket_tags" {
  description = "Tags applied to the bucket"
  value       = aws_s3_bucket.example.tags
}

# Sensitive output example
output "bucket_id_sensitive" {
  description = "Bucket ID (marked as sensitive)"
  value       = aws_s3_bucket.example.id
  sensitive   = true  # Won't display in plan/apply output
}
Enter fullscreen mode Exit fullscreen mode

Update terraform.tfvars

bucket_name      = "my-variable-bucket-yourname-2026"
bucket_prefix    = "demo-bucket-yourname"
bucket_count     = 2
enable_versioning = true
aws_region       = "us-east-1"

common_tags = {
  Environment = "Development"
  ManagedBy   = "Terraform"
  Project     = "Variables Demo"
  Owner       = "YourName"
}
Enter fullscreen mode Exit fullscreen mode

Apply and See Outputs

terraform apply
Enter fullscreen mode Exit fullscreen mode

After applying, you'll see:

Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

Outputs:

bucket_arn = "arn:aws:s3:::my-variable-bucket-yourname-2026"
bucket_domain_name = "my-variable-bucket-yourname-2026.s3.amazonaws.com"
bucket_id_sensitive = <sensitive>
bucket_name = "my-variable-bucket-yourname-2026"
bucket_region = "us-east-1"
bucket_tags = tomap({
  "Environment" = "Development"
  "ManagedBy" = "Terraform"
  "Name" = "my-variable-bucket-yourname-2026"
  "Owner" = "YourName"
  "Project" = "Variables Demo"
})
multiple_bucket_names = [
  "demo-bucket-yourname-1-yourname-2026",
  "demo-bucket-yourname-2-yourname-2026",
]
versioning_enabled = true
Enter fullscreen mode Exit fullscreen mode

View Outputs Anytime

# View all outputs
terraform output

# View specific output
terraform output bucket_name

# View sensitive output (will reveal the value)
terraform output bucket_id_sensitive

# Output as JSON (useful for scripts)
terraform output -json
Enter fullscreen mode Exit fullscreen mode

🌍 Real-World: Dev vs Prod Configurations

The real power of variables: same code, different configurations!

Create dev.tfvars

nano dev.tfvars
Enter fullscreen mode Exit fullscreen mode
# Development environment configuration
environment       = "dev"
bucket_name       = "myapp-dev-yourname-2026"
bucket_prefix     = "myapp-dev-yourname"
bucket_count      = 2
enable_versioning = false  # Save costs in dev
aws_region        = "us-east-1"

common_tags = {
  Environment = "Development"
  ManagedBy   = "Terraform"
  Project     = "MyApp"
  CostCenter  = "Engineering"
}
Enter fullscreen mode Exit fullscreen mode

Create prod.tfvars

nano prod.tfvars
Enter fullscreen mode Exit fullscreen mode
# Production environment configuration
environment       = "prod"
bucket_name       = "myapp-prod-yourname-2026"
bucket_prefix     = "myapp-prod-yourname"
bucket_count      = 3
enable_versioning = true  # Important for prod!
aws_region        = "us-east-1"

common_tags = {
  Environment = "Production"
  ManagedBy   = "Terraform"
  Project     = "MyApp"
  CostCenter  = "Operations"
  Compliance  = "Required"
}
Enter fullscreen mode Exit fullscreen mode

Deploy to Dev

terraform plan -var-file="dev.tfvars"
terraform apply -var-file="dev.tfvars"
Enter fullscreen mode Exit fullscreen mode

Result: 3 buckets created (1 main + 2 multiple), no versioning

Deploy to Prod

# First destroy dev
terraform destroy -var-file="dev.tfvars"

# Then deploy prod
terraform plan -var-file="prod.tfvars"
terraform apply -var-file="prod.tfvars"
Enter fullscreen mode Exit fullscreen mode

Result: 4 buckets created (1 main + 3 multiple), versioning enabled

Same code, different results! This is how professionals manage multiple environments.


βœ… Best Practices

1. Always Add Descriptions

Bad:

variable "name" {
  type = string
}
Enter fullscreen mode Exit fullscreen mode

Good:

variable "bucket_name" {
  description = "Name of the S3 bucket for application logs (must be globally unique)"
  type        = string
}
Enter fullscreen mode Exit fullscreen mode

2. Use Validation Rules

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

  validation {
    condition     = contains(["t2.micro", "t2.small", "t3.micro"], var.instance_type)
    error_message = "Instance type must be t2.micro, t2.small, or t3.micro."
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Provide Sensible Defaults

variable "aws_region" {
  description = "AWS region"
  type        = string
  default     = "us-east-1"  # Most common region
}
Enter fullscreen mode Exit fullscreen mode

4. Use Meaningful Variable Names

Bad: var.n, var.x, var.thing

Good: var.bucket_name, var.instance_count, var.enable_monitoring

5. Group Related Variables

# Network variables
variable "vpc_cidr" { }
variable "subnet_cidrs" { }

# Application variables
variable "app_name" { }
variable "app_version" { }

# Tags
variable "common_tags" { }
Enter fullscreen mode Exit fullscreen mode

6. Never Commit Secrets to tfvars

Bad:

# terraform.tfvars
database_password = "super-secret-password"  # DON'T DO THIS!
Enter fullscreen mode Exit fullscreen mode

Good:

# Use environment variables for secrets
export TF_VAR_database_password="super-secret-password"
Enter fullscreen mode Exit fullscreen mode

Or use AWS Secrets Manager, HashiCorp Vault, etc.

7. Document Your Variables

Create a README.md:

## Required Variables

- `bucket_name` - Globally unique S3 bucket name

## Optional Variables

- `aws_region` - AWS region (default: us-east-1)
- `enable_versioning` - Enable bucket versioning (default: false)
Enter fullscreen mode Exit fullscreen mode

πŸ› Common Issues and Solutions

Issue 1: Variable Not Found

Error:

Error: Reference to undeclared input variable
Enter fullscreen mode Exit fullscreen mode

Solution: Make sure variable is declared in variables.tf:

variable "bucket_name" {
  description = "Bucket name"
  type        = string
}
Enter fullscreen mode Exit fullscreen mode

Issue 2: Type Mismatch

Error:

Error: Invalid value for input variable
Enter fullscreen mode Exit fullscreen mode

Solution: Check your variable type matches the value:

variable "bucket_count" {
  type = number  # Not string!
}
Enter fullscreen mode Exit fullscreen mode

In terraform.tfvars:

bucket_count = 2  # Not "2" (string)
Enter fullscreen mode Exit fullscreen mode

Issue 3: Validation Failed

Error:

Error: Invalid value for variable
Bucket count must be between 1 and 5.
Enter fullscreen mode Exit fullscreen mode

Solution: Provide a value that meets the validation condition:

bucket_count = 3  # Within 1-5 range
Enter fullscreen mode Exit fullscreen mode

Issue 4: Forgot to Pass tfvars File

Problem: Terraform uses default values instead of your custom file

Solution: Always specify the file:

terraform apply -var-file="prod.tfvars"
Enter fullscreen mode Exit fullscreen mode

Issue 5: Sensitive Output Not Hidden

Problem: Sensitive data showing in output

Solution: Mark output as sensitive:

output "database_password" {
  value     = var.db_password
  sensitive = true  # Add this
}
Enter fullscreen mode Exit fullscreen mode

πŸŽ“ What You Just Learned

Let's recap your new superpowers:

βœ… Variables:

  • Convert hardcoded values to flexible variables
  • Use all variable types (string, number, bool, list, map)
  • Add validation rules
  • Provide defaults

βœ… terraform.tfvars:

  • Store variable values in files
  • Create environment-specific configs (dev.tfvars, prod.tfvars)
  • Auto-loading behavior

βœ… Outputs:

  • Display important information
  • Mark sensitive data
  • Use in scripts and modules

βœ… Real-World Patterns:

  • Same code, multiple environments
  • Conditional resource creation
  • Multiple resource creation with count

🎯 Challenge: Try It Yourself

Before moving to the next article, try these challenges:

Challenge 1: Add More Variables

Add variables for:

  • owner_email
  • cost_center
  • backup_retention_days

Challenge 2: Create staging.tfvars

Create a staging environment configuration between dev and prod.

Challenge 3: Use List Variable

Create a variable for multiple availability zones:

variable "availability_zones" {
  type    = list(string)
  default = ["us-east-1a", "us-east-1b"]
}
Enter fullscreen mode Exit fullscreen mode

Challenge 4: Complex Map Variable

Create a variable for instance configurations:

variable "instance_configs" {
  type = map(object({
    instance_type = string
    volume_size   = number
  }))
}
Enter fullscreen mode Exit fullscreen mode

Challenge 5: Output Formatting

Create an output that combines multiple values:

output "bucket_info" {
  value = "Bucket ${aws_s3_bucket.example.id} in ${var.aws_region}"
}
Enter fullscreen mode Exit fullscreen mode

πŸ“š Additional Variable Features

Variable Precedence (Order of Priority)

Terraform loads variables in this order (last wins):

  1. Environment variables (TF_VAR_name)
  2. terraform.tfvars file
  3. terraform.tfvars.json file
  4. *.auto.tfvars files (alphabetical order)
  5. -var and -var-file command line flags

Example:

# All of these set the same variable
export TF_VAR_bucket_name="from-env"           # Priority 1
# terraform.tfvars: bucket_name = "from-file"  # Priority 2
terraform apply -var="bucket_name=from-cli"    # Priority 3 (wins!)
Enter fullscreen mode Exit fullscreen mode

Environment Variables

# Set variable via environment
export TF_VAR_bucket_name="my-bucket-2026"
export TF_VAR_aws_region="us-west-2"

terraform plan  # Uses environment variables
Enter fullscreen mode Exit fullscreen mode

Auto-Loading tfvars Files

Terraform automatically loads:

  • terraform.tfvars
  • terraform.tfvars.json
  • *.auto.tfvars
  • *.auto.tfvars.json

Example:

# These are auto-loaded
terraform.tfvars
common.auto.tfvars
secrets.auto.tfvars

# These need -var-file flag
dev.tfvars
prod.tfvars
Enter fullscreen mode Exit fullscreen mode

πŸ” Understanding Variable Interpolation

You can use variables in many ways:

String Interpolation

resource "aws_s3_bucket" "example" {
  bucket = "${var.environment}-${var.app_name}-bucket"
  # Result: "dev-myapp-bucket"
}
Enter fullscreen mode Exit fullscreen mode

Conditional Expressions

resource "aws_instance" "web" {
  instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"
  # prod = t3.large, anything else = t3.micro
}
Enter fullscreen mode Exit fullscreen mode

For Expressions

locals {
  bucket_names = [for i in range(var.bucket_count) : "${var.prefix}-${i}"]
  # Result: ["app-0", "app-1", "app-2"]
}
Enter fullscreen mode Exit fullscreen mode

πŸ”— Useful Resources


🎬 What's Next?

In Article 6: Building a VPC from Scratch, we'll:

  • Create a complete AWS VPC with subnets
  • Use variables for network configuration
  • Build public and private subnets
  • Configure route tables and internet gateways
  • Apply everything you learned about variables

You'll learn:

  • Real-world networking with Terraform
  • Complex resource dependencies
  • Advanced variable usage
  • Production-ready VPC architecture

πŸ“Œ Wrapping Up

Thank you for reading. I hope this article provided practical insights and a clearer understanding of the topic.

If you found this useful:

  • ❀️ Like if it added value
  • πŸ¦„ Unicorn if you’re applying it today
  • πŸ’Ύ Save it for your next optimization session
  • πŸ”„ Share it with your team

πŸ’‘ What’s Next

More deep dives are coming soon on:

  • Cloud Operations
  • GenAI & Agentic AI
  • DevOps Automation
  • Data & Platform Engineering

Follow along for weekly insights and hands-on guides.


🌐 Portfolio & Work

You can explore my full body of work, certifications, architecture projects, and technical articles here:

πŸ‘‰ Visit My Website


πŸ› οΈ Services I Offer

If you're looking for hands-on guidance or collaboration, I provide:

  • Cloud Architecture Consulting (AWS / Azure)
  • DevSecOps & Automation Design
  • FinOps Optimization Reviews
  • Technical Writing (Cloud, DevOps, GenAI)
  • Product & Architecture Reviews
  • Mentorship & 1:1 Technical Guidance

🀝 Let’s Connect

I’d love to hear your thoughts. Feel free to drop a comment or connect with me on:

πŸ”— LinkedIn

For collaborations, consulting, or technical discussions, reach out at:

πŸ“§ simplynadaf@gmail.com


Found this helpful? Share it with your team.

⭐ Star the repo β€’ πŸ“– Follow the series β€’ πŸ’¬ Ask questions

Made by Sarvar Nadaf

🌐 https://sarvarnadaf.com


Top comments (0)