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:

Hcl
# 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>:

Hcl
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:

Bash
terraform apply -var="environment=staging"

or via a .tfvars file, which is the more common approach for anything beyond a quick test:

Hcl
# staging.tfvars
environment   = "staging"
instance_type = "t3.small"
Bash
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:

Hcl
# outputs.tf
output "app_instance_public_ip" {
  description = "Public IP address of the app server"
  value       = aws_instance.app.public_ip
}
Bash
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):

Hcl
# modules/web-server/variables.tf
variable "instance_type" {
  type = string
}

variable "environment" {
  type = string
}
Hcl
# 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}"
  }
}
Hcl
# modules/web-server/outputs.tf
output "public_ip" {
  value = aws_instance.this.public_ip
}

Calling that module from the root configuration, once per environment:

Hcl
# 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:

Bash
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:

Hcl
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 default and being surprised when terraform plan interactively 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.