Use Terraform Variables for Reusability

Learn how to use Terraform variables for reusability in this hands-on foundations lesson. Discover how to parameterize your infrastructure code, apply best practices, and troubleshoot common issues. Perfect for developers building a solid DevOps foundation.

Focus: use terraform variables for reusability

Sponsored

Hardcoding values like instance sizes, AMI IDs, or region names directly into your Terraform configuration is a one-way ticket to a maintenance nightmare. You end up copying and pasting whole blocks of code just to deploy a slightly different environment, and every change requires hunting through your files. This lesson shows you how to use Terraform variables for reusability, transforming your static configuration into a flexible, parameterized blueprint that works across dev, staging, and production with minimal effort.

The problem this lesson solves

As your infrastructure grows, you'll notice the same values appearing again and again. The instance_type might be t3.micro in dev, t3.medium in staging, and m5.large in production. Without variables, you'd either duplicate entire resource blocks or use complex conditional expressions that make your code unreadable. The real pain appears when a project requirement changes — say, the AMI ID for your base image gets updated. Now you're editing multiple files, hoping you didn't miss a spot, and the risk of human error skyrockets.

Static configuration is brittle. It ties your infrastructure code to specific environments, making it impossible to reuse a module across different teams or projects without modification. This is a direct violation of the Infrastructure as Code (IaC) principle: treat your infrastructure like software, which means write it once, use it many times. Variables are the core mechanism that makes this possible, and mastering them is non-negotiable for any serious Terraform practitioner.

By the end of this lesson, you'll be able to transform a hardcoded Terraform configuration into a parameterized one, ready for any environment. You'll understand the different variable types, how to set values from multiple sources, and how to avoid the common pitfalls that trip up even experienced developers.

Core concept / mental model

Think of a Terraform configuration as a function in programming. The resource blocks are the function body — they define what to create. Variables are the function parameters — they define what varies between calls. Just as a function like create_user(name, role) can create different users by passing different arguments, a Terraform resource like aws_instance can create different EC2 instances when you pass different variable values.

This mental model is powerful because it applies consistent design principles. You design your infrastructure code with a clear interface (variables), and the implementation (resources) stays generic and reusable. The variables become your contract with the users of your configuration — whether that's a colleague, a CI/CD pipeline, or your future self.

Terraform variables are declared in a variables.tf file (or alongside your other .tf files) using the variable block. Each block has a name, an optional type, an optional description, and optional validation rules. Here’s the general shape:

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

Once declared, you reference the variable in your resources using var.instance_type. If no default is provided, Terraform will prompt you for a value when you run terraform plan or terraform apply — but that's not ideal for automation. Instead, you'll provide values via a terraform.tfvars file, environment variables, or command-line flags, which we'll cover shortly.

How it works step by step

Now that you have the mental model, let's see exactly how variables are processed and applied in Terraform's workflow.

  1. Declaration — You define the variable's metadata (type, default, description) in a variables.tf file. This is your contract.
  2. Reference — Inside your resource blocks, you use var.<name> wherever you need the value. For example, instance_type = var.instance_type.
  3. Input — When you run Terraform commands, it collects values for all variables that don't have a default. It searches for these values in a specific order (we'll detail the precedence later).
  4. Validation — Terraform checks that the provided value matches the declared type and passes any custom validation rules.
  5. Use — During terraform plan and terraform apply, Terraform substitutes the variable references with the actual values, producing the final configuration.

This sequence ensures your code always functions with predictable inputs, and it gives you a clear place to look when something goes wrong.

Hands-on walkthrough

Let's put this into practice. We'll start with a typical hardcoded AWS EC2 instance and then refactor it to use variables. We'll also show how to override values for different environments.

Step 1: The hardcoded version (painful)

# main.tf (before)
provider "aws" {
  region = "us-east-1"
}

resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"  # ubuntu 20.04 in us-east-1
  instance_type = "t3.micro"
  tags = {
    Name = "web-server"
  }
}

To deploy this to another region or environment, you'd have to copy this file and change values manually. That’s error-prone and duplicates logic.

Step 2: Introduce variables

Create a variables.tf file:

# variables.tf
variable "aws_region" {
  description = "AWS region to deploy resources"
  type        = string
  default     = "us-east-1"
}

variable "ami_id" {
  description = "AMI ID for the EC2 instance"
  type        = string
}

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

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

Notice that ami_id has no default — it must be provided. This is a good practice because AMI IDs are region-specific and change often.

Now update main.tf to reference the variables:

# main.tf (after)
provider "aws" {
  region = var.aws_region
}

resource "aws_instance" "web" {
  ami           = var.ami_id
  instance_type = var.instance_type
  tags = {
    Name = "${var.environment}-web-server"
  }
}

Step 3: Provide values

Create a terraform.tfvars file to set the values for your current environment:

# terraform.tfvars
aws_region   = "us-east-1"
ami_id       = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
environment  = "dev"

Now when you run terraform apply, Terraform automatically loads terraform.tfvars and fills the variables. If you omit a variable that has no default, Terraform will ask you interactively.

Step 4: Override for different environments

The beauty of this pattern is that you can create separate .tfvars files per environment and apply them with the -var-file flag:

terraform apply -var-file="prod.tfvars"

A prod.tfvars might look like:

# prod.tfvars
aws_region   = "eu-west-1"
ami_id       = "ami-0a1b2c3d4e5f6a7b8"
instance_type = "m5.large"
environment  = "prod"

Expected output of terraform apply (truncated):

aws_instance.web: Creating...
aws_instance.web: Still creating... [10s elapsed]
aws_instance.web: Creation complete after 12s [id=i-0a1b2c3d4e5f6a7b8]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

The same configuration now produces a completely different infrastructure footprint based on which .tfvars file you pass. That's the power of using Terraform variables for reusability.

Compare options / when to choose what

Terraform provides several input methods, each best suited for a particular scenario. Here’s a comparison:

Method Example When to use Precedence (lowest to highest)
Default value default = "t3.micro" Safe fallback for non-critical settings 1
Environment variable TF_VAR_instance_type Automating in CI/CD, keeping secrets out of files (but use a secrets manager for sensitive data) 2
terraform.tfvars file instance_type = "t3.micro" Default values for a project; shared among the team 3
*.auto.tfvars file prod.auto.tfvars Automatically loaded per environment; convenient but less explicit 4 (after above)
-var command line -var='instance_type=t3.micro' Quick overrides during testing, or one-off runs 5
-var-file command line -var-file="prod.tfvars" Explicit selection of environment-specific files; recommended for multiple environments 6

A quick note on precedence: Terraform merges values from all sources, and later sources override earlier ones. Understanding this order saves you from puzzling “why is my variable different?” moments.

For most projects, we recommend: - Use variables.tf to define types and default values (the contract). - Use terraform.tfvars for project-wide defaults (or a common file). - Use -var-file per environment for environment-specific overrides. - Use -var sparingly for quick manual tests or CI secret injections (but prefer environment variables for secrets).

Troubleshooting & edge cases

Even with a solid variable setup, you’ll inevitably run into issues. Here are some common problems and their fixes.

1. Type mismatch errors

If you declare type = number but pass "t3.micro", Terraform will throw an error like:

Error: Invalid value for variable
│
│   on variables.tf line 4:
│    4: variable "count" {
│
│ The given value is not suitable for child module variable
│ "count" defined at variables.tf:4,6-13: string required.

Fix: Ensure the value in your .tfvars matches the declared type. Use quotes for strings, numbers for numbers, and true/false for booleans.

2. Variable not declared

If you use var.foo but never declare variable "foo", you'll see:

Error: Reference to undeclared input variable

Fix: Declare it in variables.tf and make sure the name matches exactly.

3. Confidential values leaking into plan output

With terraform plan, the state file records all values, including secrets, in plaintext. If you pass a password as a variable, it will appear in the state.

Fix: Use sensitive = true in your variable declaration to hide it from plan/apply output, and use a secrets manager (like AWS Secrets Manager) to store actual credentials, not plaintext in .tfvars.

4. Variable precedence confusion

You set a default, a value in terraform.tfvars, and a -var flag, and you’re not sure which wins.

Fix: Remember the precedence table above. If something unexpected happens, check whether an *.auto.tfvars file is present — those are silently loaded and can override your defaults.

What you learned & what's next

Congratulations! You've learned how to use Terraform variables for reusability in practice. You can now: - Explain the core idea behind parameterized infrastructure and why it matters for maintainability. - Complete a practical exercise that transforms a hardcoded configuration into a flexible, variable-driven one - Choose the right input method (default, tfvars, var-file, env var) based on your use case - Troubleshoot the most common variable-related errors

You’ve also connected this concept to the broader Terraform philosophy: separate the what from the how. Now that you have variables, you’re ready to take reusability a step further with modules. In the next lesson, you’ll learn how to package your variable-driven configurations into reusable components that can be shared across teams and projects. That’s where the true power of IaC shines — write once, use everywhere. Get ready to level up!

Practice recap

Create a variables.tf file that covers at least three different types (string, number, bool) and refactor a simple resource (e.g., an AWS security group) to use them. Then create two .tfvars files (dev and prod) and apply with -var-file to see the effect. Finally, simulate a type error to get comfortable with the error message format.

Common mistakes

  • Hardcoding values directly in resource blocks, making the configuration environment-specific and duplicating code.
  • Declaring variables with no default and then forgetting to provide a value, leading to interactive prompts that break automation.
  • Using string type for values that should be numbers or bools, causing type mismatch errors that are confusing to debug.
  • Keeping secrets like passwords in plaintext .tfvars files or variable defaults, risking exposure in state files.
  • Not understanding variable precedence, leading to unexpected values when multiple sources (defaults, tfvars, -var) are used.

Variations

  1. Using Terraform Cloud Variables to set values through the UI or API for remote runs.
  2. Leveraging environment variables (TF_VAR_) for CI/CD pipelines to inject values without storing them in files.
  3. Using locals for derived values that shouldn't be exposed as input variables (e.g., computed names or tags).

Real-world use cases

  • Deploying the same EC2 instance across dev, staging, and prod by switching -var-file per environment.
  • A CI pipeline injecting AMI IDs or secrets via environment variables without committing them to the repository.
  • Sharing a Terraform module publicly where users pass their own region, instance type, and tagging conventions.

Key takeaways

  • Variables turn static Terraform config into a reusable blueprint, just like function parameters in code.
  • Always declare types and defaults for every variable; use defaults as safe fallbacks.
  • Provide environment-specific values using .tfvars files and -var-file flags; remember the precedence order.
  • Mark sensitive variables with sensitive = true and store actual secrets in a secure backing store.
  • Type mismatches and undeclared variable references are the top errors — validate early and keep names consistent.
  • Mastering variables is the stepping stone to modules, where reusability reaches its full potential.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.