Skip to content
Pipelines and Pizza 🍕
Go back

Terraform Variables and Outputs Explained

3 min read

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):

  1. Command line: terraform apply -var="environment=staging"
  2. Variable file: Create terraform.tfvars
  3. Environment variables: TF_VAR_environment=dev
  4. 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

References