Mastering Terraform Variables

In today’s blog post, we will discuss terraform variables, specifically variable syntax, data type, declaration, assignment, and validation. A variable is used to pass inputs to a terraform module, resource, data source or in any other configuration and store values. A variable enables you to keep your terraform code reusable.

What are Terraform Variables?

In Terraform, variables act as placeholders for values that can change between deployments or environments. Instead of hardcoding values directly into your configuration, you use variables, allowing you to easily reuse the same Terraform code for different purposes – be it deploying a development, staging, or production environment, or simply tweaking a resource configuration.

Think of them like arguments to a function in a programming language. They make your code modular, flexible, and helps reduce duplication.

Declaring Input Variables: The variable Block

You declare input variables using the variable block within your Terraform configuration files (commonly in variables.tf).

# variables.tf

variable "region" {
  description = "The AWS region where resources will be deployed."
  type        = string
  default     = "us-east-1"
}

variable "instance_count" {
  description = "Number of EC2 instances to provision."
  type        = number
  default     = 1
}

variable "tags" {
  description = "A map of tags to apply to resources."
  type        = map(string)
  default = {
    Environment = "dev"
    Project     = "my-app"
  }
}

Explanation:

  • variable "region": This defines a variable named region.
  • description: (Optional) Provides a human-readable explanation of the variable’s purpose. This is for documentation, especially when your code is shared across projects.
  • type: (Optional) Specifies the data type of the variable. Terraform will validate the provided value against this type. If omitted, Terraform assumes any.
  • default: (Optional) Provides a default value for the variable. If no value is explicitly provided during terraform plan or apply, this default will be used. Variables without a default value are considered mandatory and you must provide the value during runtime.

Terraform Variable Types

Terraform supports several primitive and complex data types for variables:

Primitive Types:

string

A sequence of Unicode characters (text).

variable "bucket_name_prefix" {
  type = string
  description = "Prefix for S3 bucket name."
}
number

A numeric value (integers or floating-point).

variable "disk_size_gb" {
  type = number
  description = "Size of the disk in GB."
}
bool

A boolean value (true or false).

variable "enable_public_ip" {
  type = bool
  description = "Whether to assign a public IP to the instance."
  default = false
}

Complex Types:

list(TYPE)

An ordered sequence of values of a specified type.

variable "instance_types" {
  type = list(string)
  description = "List of allowed EC2 instance types."
  default = ["t2.micro", "t3.small"]
}
map(TYPE)

A collection of key-value pairs, where both keys (strings) and values are of a specified type.

variable "ami_ids" {
  type = map(string)
  description = "Map of AMIs per region."
  default = {
    "us-east-1" = "ami-0abcdef1234567890"
    "us-west-2" = "ami-0fedcba9876543210"
  }
}
set(TYPE)

An unordered collection of unique values of a specified type.

variable "allowed_cidrs" {
  type = set(string)
  description = "Set of CIDR blocks allowed access."
  default = ["0.0.0.0/0"]
}
object(...):

A structured type that can contain attributes of different types.

variable "server_config" {
  type = object({
    cpu_cores   = number
    memory_gb   = number
    environment = string
  })
  description = "Configuration for the server."
  default = {
    cpu_cores   = 2
    memory_gb   = 8
    environment = "dev"
  }
}
tuple(...)

An ordered sequence of values where each element can have a different type, fixed in length.

variable "database_credentials" {
  type = tuple([string, string, number]) # username, password, port
  description = "Database access credentials (username, password, port)."
  default = ["admin", "password123", 5432]
}
any

Use any when the type of the variable is not known in advance, or when it needs to accept multiple types. This sacrifices type safety for flexibility.

Providing Variable Values: Multiple Methods

Terraform offers several ways to provide values for your variables, each with its own use case and precedence.

Command-Line Arguments (-var and -var-file)

You can pass individual variable values directly on the command line using the -var flag:

terraform plan -var="region=us-west-2" -var="instance_count=3"

For a larger set of variables, it is more convenient to use a variable definition file with -var-file:

terraform plan -var-file="production.tfvars"

.tfvars Files (Automatic Loading)

Terraform automatically loads variable definitions from files with specific names in the current working directory:

  • terraform.tfvars
  • terraform.tfvars.json
  • Any files ending with .auto.tfvars or .auto.tfvars.json (e.g., dev.auto.tfvars, prod.auto.tfvars)

Example terraform.tfvars:

# terraform.tfvars

region         = "eu-central-1"
instance_count = 2
tags = {
  Environment = "prod"
  Owner       = "ops-team"
}

Example dev.auto.tfvars:

# dev.auto.tfvars

instance_count = 1
enable_public_ip = true

Environment Variables (TF_VAR_)

Terraform looks for environment variables prefixed with TF_VAR_ followed by the variable name (case-insensitive). This is particularly useful in CI/CD pipelines.

export TF_VAR_region="ap-southeast-1"
export TF_VAR_instance_count=5
terraform apply

Important Note on Environment Variables: While convenient, be cautious with sensitive information. Environment variables can sometimes be visible in system logs. For truly sensitive data, consider dedicated secret management tools.

Prompting for Input

If a variable is declared without a default value and no value is provided through other means, Terraform will prompt you for input during terraform plan or apply.

# variables.tf
variable "admin_password" {
  description = "Admin password for the database."
  type        = string
  sensitive   = true # Mark as sensitive to prevent showing in logs
}

When you run terraform plan, you will see:

var.admin_password
  Admin password for the database.

  Enter a value:

Variable Precedence: Who Wins?

When the same variable is defined in multiple places, Terraform follows a specific order of precedence, with later sources overriding earlier ones:

  1. Environment variables (TF_VAR_name)
  2. terraform.tfvars (if present)
  3. terraform.tfvars.json (if present)
  4. Any *.auto.tfvars or *.auto.tfvars.json files (processed in lexical/alphabetical order of their filenames)
  5. -var and -var-file options on the command line (in the order they are provided)

Example:

If region is set to us-east-1 in terraform.tfvars, but you run terraform apply -var="region=us-west-2", then us-west-2 will be used.

Using Variables in Your Configuration (var. prefix)

Once declared, you can reference your input variables within your Terraform configuration using the var. prefix.

# main.tf

resource "aws_instance" "web_server" {
  ami           = "ami-0abcdef1234567890" # Example AMI ID
  instance_type = "t2.micro"
  count         = var.instance_count # Using a number variable

  tags = var.tags # Using a map variable

  user_data = <<-EOF
              #!/bin/bash
              echo "Hello from $(hostname) in ${var.region}!" > /var/www/html/index.html
              EOF
}

resource "aws_s3_bucket" "my_bucket" {
  bucket = "${var.bucket_name_prefix}-${var.region}" # String interpolation
  acl    = "private"

  tags = var.tags
}

Explanation:

  • count = var.instance_count: The count meta-argument takes its value from the instance_count variable.
  • tags = var.tags: The tags block directly uses the tags map variable.
  • "${var.bucket_name_prefix}-${var.region}": String interpolation is used to combine the bucket_name_prefix and region variables into a single bucket name.
  • user_data = <<-EOF ... ${var.region} ... EOF: Here-doc syntax also supports variable interpolation.

Advanced Variable Concepts

Variable Validation (validation block)

Terraform allows you to define validation rules for your variables using the validation block. This ensures that the values provided by users meet certain criteria, preventing misconfigurations early.

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

variable "vpc_cidr_block" {
  description = "CIDR block for the VPC."
  type        = string
  validation {
    condition     = can(cidrhost(var.vpc_cidr_block, 0)) # Checks if it is a valid CIDR
    error_message = "The VPC CIDR block must be a valid CIDR notation (e.g., '10.0.0.0/16')."
  }
}

Explanation:

  • condition: A boolean expression that must evaluate to true for the validation to pass.
  • error_message: The message displayed if the condition evaluates to false.

Sensitive Variables (sensitive = true)

For variables containing confidential information like passwords, API keys, or private keys, use the sensitive = true argument. This tells Terraform to redact the variable’s value from CLI output (e.g., terraform plan, terraform apply) and state files.

variable "db_password" {
  description = "Password for the database user."
  type        = string
  sensitive   = true
}

When you run terraform plan, you will see (sensitive value) instead of the actual password:

+ resource "aws_db_instance" "my_db" {
    ...
    password = (sensitive value)
    ...
  }

Important: While sensitive = true redacts output, it does not encrypt the value in the state file. For true security, integrate with dedicated secret management solutions like HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault.

Output Variables: Sharing Information (output block)

Output variables are like return values from your Terraform configuration or module. They allow you to display specific information about the infrastructure you have provisioned, making it accessible to other Terraform configurations, CI/CD pipelines, or simply for your own reference.

# outputs.tf

output "instance_public_ip" {
  description = "The public IP address of the primary web server instance."
  value       = aws_instance.web_server[0].public_ip # Accessing the first instance's IP
}

output "s3_bucket_arn" {
  description = "The ARN of the S3 bucket created."
  value       = aws_s3_bucket.my_bucket.arn
}

output "server_config_output" {
  description = "The applied server configuration."
  value       = var.server_config
  sensitive   = true # If your object contains sensitive data, mark the output sensitive
}

After terraform apply, you can retrieve these values using terraform output (or can be simply viewed under plan, apply terminal window):

terraform output instance_public_ip

Input and Output Variables in Modules

Modules are used for structuring and reusing Terraform code. Input variables allow you to customize a module’s behavior, while output variables allow the module to pass information back to the calling configuration.

Module vpc/main.tf:

# modules/vpc/main.tf
resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidr_block
  # ... other VPC configuration
}

output "vpc_id" {
  value = aws_vpc.main.id
}

Root Configuration main.tf using the module:

# main.tf
module "my_vpc" {
  source       = "./modules/vpc"
  vpc_cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "public" {
  vpc_id     = module.my_vpc.vpc_id # Using output from the VPC module
  cidr_block = "10.0.1.0/24"
  # ...
}

Best Practices for Terraform Variables

  • Declare variables in variables.tf: Keep your variable declarations organized in a dedicated file.
  • Always provide description: Clear descriptions are invaluable for anyone using or maintaining your code.
  • Specify type: Enforce type safety to catch errors early.
  • Use default values carefully Provide defaults for optional variables, but require input for environment-specific or critical values.
  • Use .tfvars files for environment-specific values: Avoid hardcoding values in main.tf. Use terraform.tfvars or *.auto.tfvars for different environments (e.g., dev.auto.tfvars, prod.auto.tfvars).
  • Use sensitive = true for secrets: Hide sensitive data from console output. For stronger security, use a secret management solution.
  • Avoid over-parameterization: Only create variables for values that genuinely need to change. If a value is consistently static, hardcode it or use a local value.
  • Use local values for complex expressions: For values derived from other variables or expressions, define them as locals (locals.tf) for readability and reusability within a single module. This is different from var. as locals are not exposed as inputs.
  • Naming Conventions: Use clear, descriptive, lowercase names with underscores (e.g., instance_type, vpc_cidr_block). Boolean variables should have positive names (e.g., enable_feature instead of disable_feature).

Conclusion

Mastering Terraform variables is an important step in writing efficient and maintainable terraform code. By understanding how to declare them, provide their values, and use advanced features like validation and sensitivity, you can create flexible, reusable, and secure configurations that can used in different environments and requirements.

Author

Debjeet Bhowmik

Experienced Cloud & DevOps Engineer with hands-on experience in AWS, GCP, Terraform, Ansible, ELK, Docker, Git, GitLab, Python, PowerShell, Shell, and theoretical knowledge on Azure, Kubernetes & Jenkins. In my free time, I write blogs on ckdbtech.com

Leave a Comment