Introduction
Terraform variables are the foundation of infrastructure-as-code reusability. Without them, you'd be hardcoding values across dozens of files, copying and pasting configurations for each environment and duplicating effort every time you deploy to a new region or account. This article covers how to declare variables properly, set their values safely, protect sensitive data, and validate inputs before they cause production outages.
Why Variables Matter in Infrastructure Code
When you're managing infrastructure through Terraform, configuration drift and maintainability become real concerns fast. A variable-driven approach lets you write your infrastructure once and reuse it across dev, staging, and production environments without touching the core logic. You define the structure, then inject different values depending on where you're deploying.
This is especially critical in teams. When a junior engineer needs to deploy a new database instance to staging, they shouldn't need to understand or modify your core Terraform modules. They should set a few variables and let the infrastructure do the work.
Declare: Type, Description and Default
Every variable starts with a declaration block. You specify the type, add a description for teammates, and optionally provide a default value. If you don't provide a default, Terraform will prompt you for a value at runtime—or require it via other input methods.
variable "environment" {
type = string
description = "Deployment environment"
default = "dev"
}Types matter. Terraform supports string, number, bool, list, map, set, object, and tuple. Using the right type prevents mistakes at the Terraform level rather than downstream in your infrastructure.
variable "instance_count" {
type = number
description = "Number of web server instances"
default = 3
}
variable "tags" {
type = map(string)
description = "Common tags for all resources"
default = {
Team = "platform"
CostCenter = "engineering"
}
}
variable "allowed_cidrs" {
type = list(string)
description = "CIDR blocks allowed for SSH access"
default = ["10.0.0.0/8"]
}The description field is not just documentation. It appears in terraform plan output and variable UI in Terraform Cloud. Write descriptions your team will actually read and understand.
Validation: Catch Mistakes Before Plan
Validation blocks let you enforce constraints on variables. Instead of catching bad input deep in your infrastructure provisioning—or worse, in production—you catch it immediately when Terraform reads the variable.
variable "environment" {
type = string
description = "Deployment environment"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Use dev, staging or prod."
}
}This validates before any resource is created. If someone sets environment = "production" instead of prod, Terraform fails immediately with a clear message. You can chain multiple validation blocks for complex rules.
variable "database_size" {
type = number
description = "Database instance size in GB"
validation {
condition = var.database_size >= 10
error_message = "Database must be at least 10 GB."
}
validation {
condition = var.database_size <= 5000
error_message = "Database cannot exceed 5000 GB."
}
}Real-world scenario: You have a naming convention that all resources must follow. Validate it at the variable level.
variable "resource_prefix" {
type = string
description = "Prefix for all resource names"
validation {
condition = can(regex("^[a-z][a-z0-9-]{2,}$", var.resource_prefix))
error_message = "Prefix must start with lowercase, contain only lowercase/numbers/hyphens, and be at least 3 chars."
}
}Validation blocks are a guardrail your team will thank you for. They shift errors left and make infrastructure code more predictable.
Set Values: Three Ways to Inject Variables
Method 1: .tfvars Files
The most common approach in teams is using .tfvars files. These are Terraform-specific variable files that you can version control and share.
environment = "prod"
instance_count = 5
database_size = 100
allowed_cidrs = ["203.0.113.0/24", "198.51.100.0/24"]
resource_prefix = "api-svc"When you run Terraform, load the file explicitly:
terraform plan -var-file="prod.tfvars"
terraform apply -var-file="prod.tfvars"You can also follow Terraform's automatic loading convention: any file named *.auto.tfvars is loaded automatically without needing the -var-file flag. This works well in CI/CD pipelines where you want predictable behavior without extra flags.
Method 2: Command-Line Flags
For quick overrides or one-off deployments, use -var:
terraform plan -var="environment=staging" -var="instance_count=2"This is handy in development but becomes unwieldy at scale. Your CI/CD pipeline shouldn't have dozens of -var flags in a command line.
Method 3: Environment Variables
Terraform reads environment variables prefixed with TF_VAR_. This integrates well with CI/CD systems and secrets management.
export TF_VAR_environment="prod"
export TF_VAR_instance_count="5"
terraform planIn CI/CD (GitHub Actions, GitLab CI, Jenkins), you often set these from secrets:
env:
TF_VAR_database_password: ${{ secrets.DB_PASSWORD }}
TF_VAR_aws_region: us-east-1Environment variables are powerful but less visible than files. Team members might not realize a value is being injected, leading to confusion. Use them for secrets and sensitive overrides, but keep your main configuration in .tfvars files.
Sensitive Variables: Hide Secrets from Output
When you run terraform plan or terraform apply, Terraform prints the values of variables in the output. For passwords, API keys, and other secrets, this is a security risk.
Mark sensitive variables with sensitive = true:
variable "database_password" {
type = string
description = "Database root password"
sensitive = true
}Now Terraform redacts that value in logs and output, displaying <sensitive> instead. This prevents accidental exposure in logs, CI/CD output, or Terraform Cloud runs.
variable "api_keys" {
type = map(string)
description = "Third-party API keys"
sensitive = true
}Important note: sensitive = true is a display-level protection. It doesn't encrypt the value in state files. You still need to protect your state with encryption and access controls. Keep state files in remote backends (S3, Terraform Cloud) with encryption enabled, and restrict who can read .tfstate files.
In practice, avoid storing raw secrets in Terraform at all. Use AWS Secrets Manager, HashiCorp Vault, or similar, then reference them in your variables:
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/database/password"
}
variable "database_password" {
type = string
default = data.aws_secretsmanager_secret_version.db_password.secret_string
sensitive = true
}Variable Defaults and Optional Inputs
Defaults make your modules user-friendly. If a variable has a sensible default, most users won't need to specify it. But be careful: a bad default can cause silent failures.
variable "enable_monitoring" {
type = bool
description = "Enable CloudWatch monitoring"
default = true # Good: safe default
}
variable "log_retention_days" {
type = number
description = "CloudWatch log retention in days"
default = 30 # Good: reasonable default
}Avoid defaults that hide infrastructure decisions:
# Bad: defaults hide cost implications
variable "instance_type" {
type = string
description = "EC2 instance type"
default = "t3.large" # Risky default; should be explicit per environment
}Better to require it explicitly or use environment-specific defaults via a map:
variable "instance_type" {
type = string
description = "EC2 instance type"
# No default; caller must provide
}
# Or use a local to vary by environment
locals {
instance_types = {
dev = "t3.micro"
staging = "t3.small"
prod = "t3.xlarge"
}
instance_type = local.instance_types[var.environment]
}Common Mistakes
- Not validating early: Skipping validation blocks means bad input gets far into your infrastructure before failing. Validate at the variable layer.
- Hardcoding secrets: Never commit passwords or API keys to version control, even in
.tfvarsfiles. Use secret management systems and load them at runtime. - Missing descriptions: Variables without descriptions force teammates to dig through code. Descriptions are not optional.
- Overusing complex types: A
variable "config" { type = object(...) }with 15 nested fields is harder to maintain than breaking it into separate variables. - Conflicting input methods: Mixing environment variables,
.tfvarsfiles, and-varflags in the same workflow creates confusion. Standardize: use.auto.tfvarsin CI/CD or a single.tfvarsfile per environment. - Forgetting about precedence: Terraform applies variables in order: defaults, then
.tfvarsfiles, then-varflags, then environment variables. Later values override earlier ones. Know this order.
Key Takeaways
- Declare variables with explicit types and clear descriptions so your team understands what each value does.
- Use validation blocks to catch configuration errors before Terraform creates or modifies infrastructure.
- Store configuration in
.tfvarsfiles and inject sensitive values via environment variables or secret management systems. - Mark sensitive data with
sensitive = trueto redact values from logs, but remember this doesn't encrypt state files. - Provide sensible defaults for optional settings, but require explicit values for infrastructure decisions that affect cost or security.
- Standardize how your team sets variables in CI/CD to avoid confusion and silent failures.






Comments
Loading comments…