In our last Terraform post, we covered the basics — providers, resources, and state. If you followed along, you probably noticed we hardcoded a lot of values. That works for learning, but it’s not how you build real infrastructure.
Today we’re going deep on variables and outputs — the features that transform your Terraform code from a one-off script into something reusable and production-ready.
Why Variables Matter
Here’s a resource group without variables:
resource "azurerm_resource_group" "main" {
name = "rg-myapp-prod-eastus"
location = "eastus"
}
This works, but what happens when you need a dev environment? With variables:
resource "azurerm_resource_group" "main" {
name = "rg-${var.app_name}-${var.environment}-${var.location}"
location = var.location
tags = var.tags
}
Now the same code works for any environment.
Variable Types
Primitive Types
variable "environment" {
type = string
default = "dev"
}
variable "instance_count" {
type = number
default = 2
}
variable "enable_monitoring" {
type = bool
default = true
}
Collection Types
variable "availability_zones" {
type = list(string)
default = ["1", "2", "3"]
}
variable "tags" {
type = map(string)
default = {
owner = "platform"
managed_by = "terraform"
}
}
Structural Types
variable "vm_config" {
type = object({
size = string
disk_size_gb = number
enable_backup = bool
})
}
Setting Variable Values
Multiple ways to set values (in order of precedence):
- Command line:
terraform apply -var="environment=staging" - Variable file: Create
terraform.tfvars - Environment variables:
TF_VAR_environment=dev - Default values: From the variable definition
Sensitive Variables
variable "db_password" {
description = "Database administrator password"
type = string
sensitive = true
}
With sensitive = true:
- Values are redacted in plan/apply output
- Values are still stored in state — protect your state file!
Locals: Computed Values
Locals are computed within your configuration:
locals {
resource_prefix = "${var.app_name}-${var.environment}"
is_production = var.environment == "prod"
common_tags = merge(var.tags, {
application = var.app_name
environment = var.environment
managed_by = "terraform"
})
vm_size = local.is_production ? "Standard_D4s_v3" : "Standard_B2s"
}
Use them in resources:
resource "azurerm_resource_group" "main" {
name = "rg-${local.resource_prefix}"
location = var.location
tags = local.common_tags
}
Outputs
Outputs expose values from your Terraform configuration:
output "resource_group_name" {
description = "Name of the created resource group"
value = azurerm_resource_group.main.name
}
output "resource_group_id" {
description = "ID of the created resource group"
value = azurerm_resource_group.main.id
}
Query outputs:
terraform output
terraform output resource_group_name
terraform output -raw resource_group_name
terraform output -json
Validation Rules
variable "environment" {
description = "Deployment environment"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
Real-World Patterns
Environment-Based Configuration
locals {
env_config = {
dev = {
vm_size = "Standard_B2s"
instance_count = 1
}
prod = {
vm_size = "Standard_D4s_v3"
instance_count = 3
}
}
config = local.env_config[var.environment]
}
Feature Flags
variable "features" {
type = object({
enable_monitoring = bool
enable_backup = bool
})
}
resource "azurerm_monitor_diagnostic_setting" "main" {
count = var.features.enable_monitoring ? 1 : 0
# ...
}
Common Pitfalls
- Don’t Overuse Defaults — Critical values shouldn’t have defaults
- Keep tfvars Out of Git (Sometimes) — Commit non-sensitive, gitignore local overrides
- Document Everything — Always include descriptions