variables.tf vs terraform.tfvars - What's the Difference?
If you're new to Terraform, you've probably seen both** variables.tf** and terraform.tfvars files and wondered: "Aren't they doing the same thing?"
Spoiler alert: They're not! Let me break it down in the simplest way possible.
The Quick Answer:
variables.tf = The template (declares WHAT variables exist)
terraform.tfvars = The configuration (provides the ACTUAL values)
Think of it this way:
variables.tf asks the questions
terraform.tfvars gives the answers
Real-World Analogy
Imagine you're ordering a custom pizza:
variables.tf (The Order Form)
variable "size" {
description = "Pizza size"
type = string
validation {
condition = contains(["small", "medium", "large"], var.size)
error_message = "Size must be small, medium, or large"
}
}
variable "toppings" {
description = "Pizza toppings"
type = list(string)
default = ["cheese"]
}
variable "delivery_address" {
description = "Where to deliver"
type = string
# No default - this is required!
}
variable "phone" {
description = "Contact number"
type = string
default = null # Optional
}
terraform.tfvars (Your Filled Order)
size = "large"
toppings = ["pepperoni", "mushrooms", "olives"]
delivery_address = "123 Main Street, Apt 4B"
phone = "555-1234"
The restaurant keeps the same order form (variables.tf) for everyone, but YOUR specific order (terraform.tfvars) is unique to you!
Deep Dive: variables.tf
Purpose: Declares the structure and rules for variables
variable "subscription_id" {
description = "Azure Subscription ID"
type = string
sensitive = true # Won't show in logs
}
variable "vm_name" {
description = "Name of the Virtual Machine"
type = string
default = "ubuntu-vm" # Optional default value
}
variable "vm_size" {
description = "VM size"
type = string
default = "Standard_B2s"
validation {
condition = can(regex("^Standard_", var.vm_size))
error_message = "VM size must start with 'Standard_'"
}
}
variable "allowed_ips" {
description = "Allowed IP addresses"
type = list(string)
default = []
}
What it contains:
- Variable name
- Type (string, number, bool, list, map, object)
- Description (documentation)
- Default value (optional)
- Validation rules (optional)
- Sensitive flag (optional)
Think of it as: The schema, the blueprint, the contract
Deep Dive: terraform.tfvars
Purpose: Provides actual values for your infrastructure
subscription_id = "12345678-1234-1234-1234-123456789abc"
vm_name = "production-web-server"
vm_size = "Standard_D4s_v3"
allowed_ips = ["203.0.113.0/24", "198.51.100.0/24"]
What it contains:
- Just the values!
- No types, no descriptions, no validation
- Your specific configuration
Think of it as: The actual data, your answers, the configuration
Why This Separation?
1. Reusability Across Environments
Keep the same variables.tf, but use different values files:
my-terraform-project/
├── variables.tf # Same for all environments
├── main.tf # Same for all environments
├── dev.tfvars # Dev-specific values
├── staging.tfvars # Staging-specific values
└── prod.tfvars # Production-specific values
Deploy to different environments:
terraform apply -var-file="dev.tfvars" # Deploy to dev
terraform apply -var-file="staging.tfvars" # Deploy to staging
terraform apply -var-file="prod.tfvars" # Deploy to production
2. Security & Git Management
**.gitignore
**terraform.tfvars # Contains secrets - DON'T COMMIT
*.auto.tfvars # Contains secrets - DON'T COMMIT
Safe to commit
variables.tf # Just the template
terraform.tfvars.example # Example with fake values
Example file to commit:
terraform.tfvars.example
subscription_id = "YOUR_SUBSCRIPTION_ID_HERE"
vm_name = "your-vm-name-here"
vm_size = "Standard_B2s"
3. Type Safety & Validation
variables.tf enforces rules:
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod"
}
}
variable "vm_count" {
type = number
validation {
condition = var.vm_count >= 1 && var.vm_count <= 10
error_message = "VM count must be between 1 and 10"
}
}
If you try to pass invalid values in terraform.tfvars, Terraform will catch it!
The Complete Flow
Alternative Ways to Provide Values
You don't HAVE to use terraform.tfvars. Here are your options:
Option 1: terraform.tfvars (Recommended ✅)
terraform apply
# Automatically reads terraform.tfvars
Option 2: Custom .tfvars file
terraform apply -var-file="production.tfvars"
Option 3: Command-line flags
terraform apply -var="vm_name=my-vm" -var="location=East US"
# Gets tedious with many variables!
Option 4: Environment variables
export TF_VAR_vm_name="my-vm"
export TF_VAR_location="East US"
terraform apply
Option 5: Interactive prompts
terraform apply
# Terraform prompts you:
# var.vm_name
# Enter a value: _
**
Best Practices
**
✅ DO:
- Always create variables.tf - it's your documentation
- Use terraform.tfvars for local development
- Create terraform.tfvars.example with dummy values to commit
- Use separate .tfvars files per environment
- Add terraform.tfvars to*.gitignore*
- Use descriptive variable names and descriptions
- Set reasonable defaults where appropriate
- Use validation rules for critical variables
❌ DON'T:
- Commit terraform.tfvars with real secrets to Git
- Put actual values in variables.tf (except defaults)
- Use the same values across all environments
- Skip variable descriptions
- Forget to document required variables
Practical Example
💻
Let's say you're deploying Azure VMs with Terraform:
variables.tf
variable "subscription_id" {
description = "Azure Subscription ID"
type = string
sensitive = true
}
variable "resource_group_name" {
description = "Name of the resource group"
type = string
}
variable "location" {
description = "Azure region"
type = string
default = "East US"
}
variable "vm_size" {
description = "Size of the VM"
type = string
default = "Standard_B2s"
}
variable "environment" {
description = "Environment (dev/staging/prod)"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Must be dev, staging, or prod"
}
}
dev.tfvars
subscription_id = "dev-subscription-id-here"
resource_group_name = "rg-myapp-dev"
location = "East US"
vm_size = "Standard_B2s" # Smaller for dev
environment = "dev"
prod.tfvars
subscription_id = "prod-subscription-id-here"
resource_group_name = "rg-myapp-prod"
location = "East US"
vm_size = "Standard_D4s_v3" # Larger for prod
environment = "prod"
Deploy:
terraform apply -var-file="dev.tfvars" # For development
terraform apply -var-file="prod.tfvars" # For production
Conclusion
🎓
Remember the golden rule:
variables.tf = The questions (template/schema)
terraform.tfvars = The answers (your specific values)
This separation gives you:
✅ Reusable code across environments
✅ Better security (secrets not in code)
✅ Type safety and validation
✅ Clear documentation
✅ Team collaboration
Now go forth and Terraform with confidence! 🚀
Found this helpful? Give it a ❤️ and follow me for more DevOps and Cloud content!
Questions? Drop them in the comments below! 👇
Related Articles
https://learn.microsoft.com/en-us/azure/developer/terraform/provision-infrastructure-using-azure-deployment-slots
Top comments (0)