Wednesday, November 5, 2025

A Guide to Terraform Variables, Outputs, and Best Practices

A Guide to Terraform Variables, Outputs, and Best Practices
Terraform Variables Outputs Configuration DevOps

Mastering Terraform Variables and Outputs

Published on: November 2, 2023 | Author: DevOps Engineering Team

Dynamic Terraform Configurations

Welcome to Part 5 of our Terraform Mastery Series! So far, you've learned HCL syntax and state management. Now it's time to make your configurations dynamic and reusable. Variables and outputs are the key to creating flexible, maintainable Terraform code that works across different environments and use cases.

Why Variables Matter

Variables transform static configurations into dynamic, reusable templates. Here's what they enable:

Environment Flexibility

Use the same configuration for dev, staging, and production with different variable values.

Team Collaboration

Different team members can use the same code with their own settings.

Security

Keep sensitive values out of your codebase and inject them at runtime.

Variables vs. Hardcoded Values

Hardcoded (inflexible): instance_type = "t3.micro"

Variable (flexible): instance_type = var.instance_size

Variables make your code adaptable to different requirements without changing the core logic.

Variable Types and Validation

Terraform supports several variable types with built-in validation:

Basic Types
Complex Types
Validation
Sensitive Data

Basic Variable Types

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

variable "instance_count" {
  type        = number
  description = "Number of instances to create"
  default     = 1
}

variable "enable_monitoring" {
  type        = bool
  description = "Enable detailed monitoring"
  default     = false
}

Complex Variable Types

variable "availability_zones" {
  type        = list(string)
  description = "List of availability zones"
  default     = ["us-east-1a", "us-east-1b"]
}

variable "tags" {
  type        = map(string)
  description = "Resource tags"
  default     = {
    Environment = "dev"
    Project     = "terraform-learning"
  }
}

variable "network_config" {
  type = object({
    vpc_cidr    = string
    subnet_count = number
    enable_nat   = bool
  })
  default = {
    vpc_cidr    = "10.0.0.0/16"
    subnet_count = 2
    enable_nat   = true
  }
}

Variable Validation

variable "environment" {
  type        = string
  description = "Deployment environment"
  
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

variable "instance_size" {
  type        = string
  description = "EC2 instance size"
  
  validation {
    condition     = can(regex("^[tcm][23a]?\\.[a-z]+$", var.instance_size))
    error_message = "Instance size must be a valid AWS instance type."
  }
}

Sensitive Variables

variable "database_password" {
  type        = string
  description = "Database administrator password"
  sensitive   = true
}

variable "api_keys" {
  type        = map(string)
  description = "API keys for external services"
  sensitive   = true
}

Sensitive Variable Behavior

Terraform will redact sensitive variable values from console output and logs, but they are still stored in state files in plain text. Always encrypt your state files!

Variable Input Methods

Terraform provides multiple ways to set variable values, each with different use cases:

1. terraform.tfvars Files

Recommended for most projects. Create environment-specific files:

instance_type = "t3.large"
instance_count = 3
environment = "prod"

tags = {
  Environment = "production"
  CostCenter  = "infrastructure"
}

Usage: terraform apply (automatically loaded)

2. Command Line (-var)

Quick overrides for individual variables:

terraform apply -var="instance_type=t3.large" \
                -var="instance_count=5"

Best for: Quick testing and debugging

3. Environment Variables (TF_VAR_)

Great for CI/CD pipelines and automation:

export TF_VAR_instance_type="t3.large"
export TF_VAR_database_password="secret123"
terraform apply

Best for: Automated deployments and secrets

4. Variable Definition Files (.auto.tfvars)

Automatically loaded files for different environments:

environment = "prod"
instance_count = 5
enable_monitoring = true

Usage: terraform apply (automatically loaded)

Variable Precedence Rules

When the same variable is set multiple ways, Terraform follows specific precedence rules:

1
Environment variables (TF_VAR_name)
Highest priority
2
terraform.tfvars files
3
*.auto.tfvars files
4
-var and -var-file flags
5
Variable defaults
Lowest priority

Precedence Example

If you have default = "t3.micro" but set TF_VAR_instance_type=t3.large, Terraform will use t3.large because environment variables have highest precedence.

Working with Sensitive Variables

Sensitive variables require special handling to protect your secrets:

1

Mark Variables as Sensitive

variable "db_password" {
  type        = string
  sensitive   = true
  description = "Database password"
}
2

Use Environment Variables for Secrets

export TF_VAR_db_password="my-secret-password"
terraform apply

This keeps secrets out of your code and version control.

3

Never Commit Secrets

Add sensitive files to .gitignore:

# Terraform sensitive files
*.tfvars
*.tfstate
*.tfstate.backup
.terraform/

Output Values Deep Dive

Outputs expose information from your infrastructure for other configurations or users:

Basic Outputs

output "instance_ip" {
  value       = aws_instance.web.public_ip
  description = "Public IP of the web server"
}

output "load_balancer_dns" {
  value       = aws_lb.web.dns_name
  description = "DNS name of the load balancer"
}

Complex Outputs

output "all_instance_ips" {
  value       = aws_instance.web.*.public_ip
  description = "List of all instance IPs"
}

output "security_group_rules" {
  value = {
    for sg in aws_security_group.web :
    sg.name => sg.ingress[*].from_port
  }
  description = "Security group ingress rules"
}

Sensitive Outputs

output "database_password" {
  value       = aws_db_instance.main.password
  sensitive   = true
  description = "Database administrator password"
}

Output Security Note

Even with sensitive = true, outputs are stored in plain text in state files. Always encrypt your state backend!

Real-World Examples

Let's see variables and outputs in action with a complete example:

variable "environment" {
  type        = string
  description = "Deployment environment"
  
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

variable "instance_config" {
  type = map(object({
    instance_type = string
    instance_count = number
  }))
  default = {
    dev = {
      instance_type  = "t3.micro"
      instance_count = 1
    }
    staging = {
      instance_type  = "t3.medium"
      instance_count = 2
    }
    prod = {
      instance_type  = "t3.large"
      instance_count = 3
    }
  }
}
resource "aws_instance" "web" {
  count = var.instance_config[var.environment].instance_count
  
  ami           = "ami-0c02fb55956c7d316"
  instance_type = var.instance_config[var.environment].instance_type
  
  tags = {
    Name        = "web-server-${count.index}"
    Environment = var.environment
  }
}
output "web_instance_ips" {
  value = aws_instance.web.*.public_ip
}

output "environment_summary" {
  value = "Deployed ${var.instance_config[var.environment].instance_count} instances of type ${var.instance_config[var.environment].instance_type} in ${var.environment} environment"
}

Variables and Outputs Best Practices

Follow these guidelines for maintainable and secure configurations:

Use Descriptive Names

Choose clear, meaningful names for variables and outputs:

# Good
variable "database_instance_type" {}

# Avoid
variable "db_type" {}

Provide Defaults When Possible

Default values make configurations easier to use:

variable "instance_type" {
  type    = string
  default = "t3.micro"
}

Add Descriptions

Document the purpose and expected values:

variable "environment" {
  description = "Deployment environment (dev, staging, prod)"
  type        = string
}

Use Validation

Catch errors early with input validation:

variable "port" {
  type        = number
  validation {
    condition = var.port > 0 && var.port < 65536
    error_message = "Port must be between 1 and 65535"
  }
}

Key Takeaways

  • Variables make configurations reusable across environments
  • Use appropriate types and validation for safety
  • Follow precedence rules for variable assignment
  • Mark sensitive data to protect secrets
  • Outputs expose important information for other systems
  • Never commit secrets to version control

In our next tutorial, we'll explore Terraform Modules, where you'll learn how to create reusable, composable infrastructure components that can be shared across your organization.


This is Part 5 of The Ultimate Terraform Mastery Series.

Next: Terraform Modules Deep Dive →

No comments:

Post a Comment

How to Create and Use Terraform Modules for Reusable Code

How to Create and Use Terraform Modules for Reusable Code Terraform...