Terraform Input Variables
Learn Terraform input variables in depth, including variable blocks, types, defaults, validation rules, tfvars files, CLI flags, environment variables, and sensitive values.
What Are Terraform Input Variables?
Terraform input variables are named values that let you parameterize your infrastructure code. Instead of hardcoding settings like AWS region, instance type, environment name, or subnet CIDR ranges directly into resources, you define variables and then pass values into them when Terraform runs.
Think of a Terraform module like a function in a programming language. A function becomes reusable because it accepts parameters. Terraform modules work the same way. Input variables are the parameters of your infrastructure code.
Without variables, you might write something like this:
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
tags = {
Name = "dev-web-1"
Environment = "dev"
}
}
This works, but it is rigid. If you want a staging environment that uses t3.small, or a production environment that uses t3.medium, you have to edit the file itself. That is fragile and inconvenient.
With variables, the configuration becomes reusable:
variable "instance_type" {
description = "EC2 instance type for the web server"
type = string
default = "t3.micro"
}
variable "environment" {
description = "Deployment environment name"
type = string
}
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890"
instance_type = var.instance_type
tags = {
Name = "${var.environment}-web-1"
Environment = var.environment
}
}
Now the same code can be used in development, staging, and production by passing different values.
Why Use Input Variables?
Terraform input variables solve several real-world problems for DevOps teams:
1. Reusability
A single configuration can be reused in different environments. For example:
devuses smaller instancesstaginguses similar settings to productionproductionuses larger instances and more replicas
2. Cleaner code
Hardcoded values scattered across resources are hard to maintain. Variables pull those decisions into one obvious place.
3. Safer collaboration
Instead of editing .tf files every time a value changes, teams can provide values using .tfvars files, environment variables, or CI/CD pipeline secrets.
4. Better module design
If you create modules later, variables become the public interface of the module. A good module clearly defines what callers can customize.
5. Validation and documentation
Variables let you describe expected inputs, enforce types, and reject invalid values before Terraform touches real infrastructure.
Declaring Variables with variable Blocks
A Terraform input variable is declared with a variable block:
variable "region" {
description = "AWS region to deploy into"
type = string
default = "us-east-1"
}
The label after variable is the variable name. You reference it elsewhere using var.<name>.
provider "aws" {
region = var.region
}
Common arguments in a variable block
| Argument | Purpose |
|---|---|
description | Explains what the variable is for |
type | Restricts the kind of value Terraform accepts |
default | Makes the variable optional by providing a fallback value |
sensitive | Hides the value in CLI output |
validation | Adds custom rules beyond the basic type check |
A well-defined variable block usually contains at least description and type, and often default if the value should be optional.
Required Variables vs Optional Variables
A variable is required when it has no default value.
variable "project_name" {
description = "Project identifier used in resource names"
type = string
}
If you run Terraform without providing a value, Terraform will stop and ask for one, or fail in automation.
A variable is optional when it has a default.
variable "enable_monitoring" {
description = "Enable CloudWatch detailed monitoring"
type = bool
default = false
}
If the caller does not supply a value, Terraform uses false.
Practical rule
- Use required variables for values that must be explicitly chosen, such as environment name, VPC CIDR, or database password.
- Use optional variables for safe defaults, such as tags, monitoring toggles, or standard instance types for learning environments.
Terraform Variable Types
Terraform supports rich type constraints. Defining types is important because it catches mistakes early and makes your configuration self-documenting.
string
A string is plain text.
variable "environment" {
description = "Environment name such as dev, staging, or prod"
type = string
}
Example use:
resource "aws_s3_bucket" "logs" {
bucket = "${var.environment}-application-logs"
}
Typical string values include:
- region names
- environment names
- project names
- AMI IDs
- DNS names
number
A number is used for integer or decimal values.
variable "instance_count" {
description = "Number of EC2 instances to create"
type = number
default = 2
}
Example use:
resource "aws_instance" "web" {
count = var.instance_count
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
}
Common number values include:
- replica counts
- disk sizes
- port numbers
- retention days
bool
A boolean can only be true or false.
variable "enable_versioning" {
description = "Enable S3 versioning"
type = bool
default = true
}
Example use:
resource "aws_s3_bucket_versioning" "example" {
bucket = aws_s3_bucket.example.id
versioning_configuration {
status = var.enable_versioning ? "Enabled" : "Suspended"
}
}
Boolean variables are especially useful for optional features.
list
A list is an ordered collection of values of the same type.
variable "availability_zones" {
description = "AZs used for public subnets"
type = list(string)
default = ["us-east-1a", "us-east-1b"]
}
Example use:
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
availability_zone = var.availability_zones[count.index]
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
}
Lists are common when you need:
- multiple subnet IDs
- multiple CIDR blocks
- allowed IP ranges
- lists of domains
map
A map stores key-value pairs of the same value type.
variable "tags" {
description = "Common tags applied to all resources"
type = map(string)
default = {
ManagedBy = "Terraform"
Owner = "platform-team"
}
}
Example use:
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
tags = var.tags
}
Maps are very useful for:
- tags
- environment-specific settings
- lookup values keyed by environment
For example:
variable "instance_sizes" {
type = map(string)
default = {
dev = "t3.micro"
staging = "t3.small"
prod = "t3.medium"
}
}
Then you can do:
instance_type = var.instance_sizes[var.environment]
object
An object groups multiple named attributes, each with its own type.
variable "app_config" {
description = "Application infrastructure settings"
type = object({
instance_type = string
instance_count = number
enable_https = bool
root_disk_size = number
})
}
Example value:
app_config = {
instance_type = "t3.small"
instance_count = 2
enable_https = true
root_disk_size = 30
}
Example use:
resource "aws_instance" "app" {
count = var.app_config.instance_count
ami = "ami-0abcdef1234567890"
instance_type = var.app_config.instance_type
}
Objects are excellent when related settings belong together.
tuple
A tuple is an ordered sequence where each position can have a different type.
variable "maintenance_window" {
description = "Day, start hour, and whether the window is enabled"
type = tuple([string, number, bool])
default = ["Sunday", 2, true]
}
Example access:
locals {
maintenance_day = var.maintenance_window[0]
maintenance_hour = var.maintenance_window[1]
maintenance_enabled = var.maintenance_window[2]
}
Tuples are supported, but they are less common in beginner Terraform code. In many cases, an object is easier to read because attribute names are clearer than positional indexes.
A Realistic Variable Set
Here is a more complete example for a small web application:
variable "project_name" {
description = "Project name used for tagging and naming"
type = string
}
variable "environment" {
description = "Deployment environment"
type = string
}
variable "region" {
description = "AWS region"
type = string
default = "us-east-1"
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "instance_count" {
description = "Number of EC2 instances"
type = number
default = 2
}
variable "subnet_ids" {
description = "Subnets where the app instances will run"
type = list(string)
}
variable "tags" {
description = "Additional resource tags"
type = map(string)
default = {}
}
This is the kind of interface you will see in well-written Terraform modules.
Variable Validation Blocks
Type constraints are useful, but sometimes they are not enough. A value can have the right type and still be invalid.
For example:
- a string might need to be one of a few allowed environments
- a number might need to be within a range
- a CIDR block might need a certain format
- a name might need to follow a naming convention
That is where validation blocks help.
Basic validation example
variable "environment" {
description = "Deployment environment"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment must be one of: dev, staging, prod."
}
}
If someone sets environment = "qa", Terraform fails before making changes.
Number range validation
variable "root_disk_size" {
description = "Size of the root volume in GB"
type = number
default = 20
validation {
condition = var.root_disk_size >= 8 && var.root_disk_size <= 200
error_message = "root_disk_size must be between 8 and 200 GB."
}
}
Naming pattern validation
variable "project_name" {
description = "Lowercase project name used in resource names"
type = string
validation {
condition = can(regex("^[a-z0-9-]+$", var.project_name))
error_message = "project_name may contain only lowercase letters, numbers, and hyphens."
}
}
Validation for a list
variable "allowed_ports" {
description = "List of inbound ports"
type = list(number)
default = [80, 443]
validation {
condition = alltrue([for p in var.allowed_ports : p > 0 && p < 65536])
error_message = "All allowed_ports values must be valid TCP/UDP ports between 1 and 65535."
}
}
Validation is one of the simplest ways to make Terraform safer for teams. It turns vague mistakes into clear, actionable errors.
Assigning Variable Values
Declaring a variable only defines the interface. You still need a way to provide actual values.
Terraform supports multiple input methods.
1. terraform.tfvars
If a file named terraform.tfvars exists in the root module directory, Terraform automatically loads it.
project_name = "billing-app"
environment = "dev"
instance_type = "t3.micro"
instance_count = 1
This is convenient for local development or small demos.
2. *.auto.tfvars
Any file ending in .auto.tfvars or .auto.tfvars.json is also loaded automatically.
Example:
# dev.auto.tfvars
project_name = "billing-app"
environment = "dev"
instance_type = "t3.micro"
# prod.auto.tfvars
project_name = "billing-app"
environment = "prod"
instance_type = "t3.medium"
instance_count = 3
These files are useful when you want separate environment-specific values.
3. -var flag
You can pass individual values directly on the command line:
terraform plan -var="environment=staging" -var="instance_count=2"
This is useful for quick experiments, but it becomes hard to manage when many variables are involved.
4. -var-file flag
You can explicitly tell Terraform which variable file to load:
terraform plan -var-file="dev.tfvars"
terraform apply -var-file="prod.tfvars"
This is a common pattern for different environments:
terraform plan -var-file="environments/dev.tfvars"
terraform plan -var-file="environments/staging.tfvars"
terraform plan -var-file="environments/prod.tfvars"
5. Environment variables with TF_VAR_
Terraform also reads environment variables prefixed with TF_VAR_.
export TF_VAR_environment="prod"
export TF_VAR_instance_type="t3.medium"
export TF_VAR_db_password="SuperSecretPassword"
terraform apply
The variable name after TF_VAR_ must match the Terraform variable name.
This is especially useful in CI/CD pipelines because secrets can be injected from the platform instead of stored in files.
Which method should you use?
A practical beginner-friendly pattern is:
- use
defaultfor safe, common values - use
*.tfvarsor-var-filefor environment-specific settings - use
TF_VAR_for secrets in CI/CD - use
-varonly for quick one-off overrides
Sensitive Variables
Some variables contain secrets such as:
- database passwords
- API tokens
- private keys
- access credentials
Mark them as sensitive:
variable "db_password" {
description = "Database administrator password"
type = string
sensitive = true
}
When Terraform shows a plan or apply output, it hides the value:
password = (sensitive value)
That reduces accidental exposure in terminal logs and screenshots.
Important limitation
sensitive = true hides the value in CLI output, but it does not remove the value from Terraform state. If a provider stores the secret in state, the state file can still contain it. That is one reason remote state security matters.
Real Example: Variables for an S3 Logging Bucket
variable "bucket_name" {
description = "Globally unique S3 bucket name"
type = string
validation {
condition = length(var.bucket_name) >= 3 && length(var.bucket_name) <= 63
error_message = "bucket_name must be between 3 and 63 characters."
}
}
variable "environment" {
description = "Environment name"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment must be dev, staging, or prod."
}
}
variable "enable_versioning" {
description = "Whether versioning should be enabled"
type = bool
default = true
}
variable "extra_tags" {
description = "Additional tags to merge with defaults"
type = map(string)
default = {}
}
resource "aws_s3_bucket" "logs" {
bucket = var.bucket_name
tags = merge(
{
Name = var.bucket_name
Environment = var.environment
ManagedBy = "Terraform"
},
var.extra_tags
)
}
This example shows good Terraform design:
- required inputs for important decisions
- defaults for optional behavior
- validation for safer usage
- a map for flexible tagging
Common Beginner Mistakes
Not declaring a type
Terraform can infer types in some cases, but explicit type constraints are clearer and safer.
Using too many command-line -var flags
A few are fine. Many are painful. Prefer variable files for stable configurations.
Storing secrets in committed .tfvars files
Never commit real passwords or tokens to Git. Use secure secret injection in your CI/CD platform or secret manager.
Making everything optional
If a value must be chosen intentionally, do not give it a default. Let Terraform force the caller to think about it.
Skipping validation
Validation blocks catch bad inputs early and make module usage much friendlier.
Best Practices for Terraform Input Variables
- Always add descriptions. Your future self and your teammates will thank you.
- Always define types. This prevents confusing runtime issues.
- Use required variables for important decisions. Do not hide critical settings behind weak defaults.
- Prefer objects for grouped settings. They are often clearer than a long flat list of unrelated variables.
- Use validation for business rules. Types alone do not express everything.
- Mark secrets as sensitive. It will not solve every security concern, but it is still important.
- Keep the interface simple. A module with 40 variables is harder to use than one with 8 well-designed inputs.
Final Thoughts
Input variables are one of the foundations of real-world Terraform. They make the difference between a one-off script and reusable infrastructure as code. Once you understand variables, you can create modules that work across multiple environments, pass values cleanly from CI/CD systems, validate user input, and keep your Terraform code easier to maintain.
A simple way to remember them is this:
- Variables accept input
- Types protect input
- Defaults simplify input
- Validation improves input
- Sensitive flags hide input in CLI output
Mastering these basics will make every later Terraform topic—from modules to remote state—much easier.
Knowledge Check
Question 1: Required Variables
When is a Terraform input variable considered required?
Question 2: Variable Validation
What is the main purpose of a validation block inside a variable declaration?
Question 3: Passing Values
Which method is commonly preferred for passing secrets like database passwords into Terraform in CI/CD pipelines?