Variables, Modules & Workspaces
Parameterizing configuration with variables and outputs, building a reusable module, and managing environments with workspaces.
Variables
A hardcoded value like region = "us-east-1" scattered across every .tf file becomes painful to change consistently. variable blocks parameterize a configuration:
# variables.tf
variable "region" {
description = "AWS region to deploy into"
type = string
default = "us-east-1"
}
variable "instance_type" {
description = "EC2 instance type for the app server"
type = string
default = "t3.micro"
}
variable "environment" {
description = "Deployment environment name"
type = string
}
Reference a variable anywhere in your config with var.<name>:
provider "aws" {
region = var.region
}
resource "aws_instance" "app" {
ami = "ami-0abcdef1234567890"
instance_type = var.instance_type
tags = {
Environment = var.environment
}
}
A variable with no default (like environment above) must be supplied at plan/apply time:
terraform apply -var="environment=staging"
or via a .tfvars file, which is the more common approach for anything beyond a quick test:
# staging.tfvars
environment = "staging"
instance_type = "t3.small"
terraform apply -var-file="staging.tfvars"
Outputs
An output block surfaces a value from your configuration after apply — useful for values you need to hand to another system, another Terraform configuration, or just to read directly:
# outputs.tf
output "app_instance_public_ip" {
description = "Public IP address of the app server"
value = aws_instance.app.public_ip
}
terraform apply
# ...
Outputs:
app_instance_public_ip = "54.210.12.44"
Modules
A module is a reusable, self-contained package of Terraform configuration — the same idea as a function in a programming language: define it once, parameterize it with variables, and call it from multiple places instead of copy-pasting the same resource blocks repeatedly.
A minimal module that provisions a web server (its own small directory with its own variables.tf, main.tf, outputs.tf):
# modules/web-server/variables.tf
variable "instance_type" {
type = string
}
variable "environment" {
type = string
}
# modules/web-server/main.tf
resource "aws_instance" "this" {
ami = "ami-0abcdef1234567890"
instance_type = var.instance_type
tags = {
Environment = var.environment
Name = "web-server-${var.environment}"
}
}
# modules/web-server/outputs.tf
output "public_ip" {
value = aws_instance.this.public_ip
}
Calling that module from the root configuration, once per environment:
# main.tf
module "web_staging" {
source = "./modules/web-server"
instance_type = "t3.micro"
environment = "staging"
}
module "web_production" {
source = "./modules/web-server"
instance_type = "t3.medium"
environment = "production"
}
output "staging_ip" {
value = module.web_staging.public_ip
}
Modules can also be published and pulled from the public Terraform Registry (source = "terraform-aws-modules/vpc/aws") — for common infrastructure patterns (a VPC, an EKS cluster), reusing a well-maintained community module is often preferable to writing the same resources from scratch.
Workspaces
A workspace lets one Terraform configuration manage multiple, independent instances of the same infrastructure — each with its own separate state — without duplicating the .tf files:
terraform workspace new staging
terraform workspace new production
terraform workspace list
# default
# * staging
# production
terraform workspace select production
Inside your configuration, terraform.workspace returns the currently selected workspace name, letting you vary behavior per environment without separate directories:
resource "aws_instance" "app" {
instance_type = terraform.workspace == "production" ? "t3.medium" : "t3.micro"
tags = {
Environment = terraform.workspace
}
}
Workspaces are a lightweight tool best suited to environments that are structurally identical and differ only in a few parameters (like size). For environments with substantially different infrastructure (e.g., production has a Multi-AZ database and staging doesn't), separate root configurations — or separate module calls with different variables, as in the modules example above — are usually a clearer, less error-prone approach than trying to branch all that difference inside terraform.workspace == conditionals.
Common mistakes
- Hardcoding environment-specific values (instance sizes, domain names) directly in resource blocks instead of variables — this is what makes reusing the same config across environments painful.
- Forgetting a variable has no
defaultand being surprised whenterraform planinteractively prompts for it in a non-interactive CI run. - Using workspaces for environments that are actually structurally different, then fighting increasingly complex conditionals to express that difference.
- Publishing a module without clear variable descriptions and sensible defaults — the whole point of a module is that someone else can use it without reading its internals.