Definition
Terraform modules and workspaces serve different purposes for code reuse and environment separation.
- Module – a reusable, parameterized container of resources with typed input variables, resources, and output values.
- Workspace – a named instance of a single configuration's state, letting the same code hold multiple independent state copies.
- Multi-environment pattern – reusable modules composed from thin per-environment root configurations, each with its own backend state and variable values.
- Workspace caution – all workspaces share the same backend and code, so a wrong selection can hit production.
At the National Bank of India, a module is the standard blueprint for a branch: counters, vault, and ATMs designed once, then stamped out with different inputs for a small town branch and a large city branch. A workspace is keeping separate inventory ledgers for the same blueprint. The strongest setup gives each real environment its own blueprint instantiation and its own locked ledger, so a renovation in the test branch can never touch the main branch.
Understanding the Concepts
What is a Terraform module?
A module is simply a directory of Terraform files with input variables, resources, and output values. The root configuration is itself a module. Child modules are called with a module block that passes inputs.
# modules/network/variables.tf
variable "vpc_cidr" {
type = string
description = "CIDR block for the banking VPC"
}
variable "environment" {
type = string
description = "Environment name: dev, staging, or prod"
}
# modules/network/main.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
tags = {
Name = "banking-vpc-${var.environment}"
Environment = var.environment
}
}
resource "aws_subnet" "private" {
count = 3
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 4, count.index)
tags = {
Name = "private-${var.environment}-${count.index}"
}
}
# modules/network/outputs.tf
output "vpc_id" {
value = aws_vpc.main.id
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id
}
How do you call a module?
The root configuration calls child modules and wires one module's outputs into another's inputs.
# environments/prod/main.tf
module "network" {
source = "../../modules/network"
vpc_cidr = "10.0.0.0/16"
environment = "prod"
}
module "eks" {
source = "../../modules/eks"
vpc_id = module.network.vpc_id
subnet_ids = module.network.private_subnet_ids
cluster_name = "banking-cluster-prod"
node_instance_type = "m5.2xlarge"
min_nodes = 5
max_nodes = 20
}
What makes a good module?
Good modules follow the same principles as well-encapsulated functions: clear parameters, a single responsibility, and a clear return value.
# Good module design principles:
# 1. Typed variables with descriptions and defaults
variable "node_instance_type" {
type = string
default = "m5.xlarge"
description = "EC2 instance type for EKS worker nodes"
}
# 2. Single responsibility: this module does ONLY networking
# Not networking + compute + database
# 3. Outputs expose exactly what callers need
output "vpc_id" {
value = aws_vpc.main.id
description = "The VPC ID for other modules to reference"
}
# 4. Versioned when published to a registry
module "network" {
source = "nbi-registry/banking-network/aws"
version = "2.1.0" # pinned version for reproducibility
}
What is a Terraform workspace?
A workspace is a named state within a single backend. It lets the same configuration manage multiple independent state copies.
# Create and switch workspaces
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
terraform workspace select dev
terraform apply -var-file=dev.tfvars
terraform workspace select prod
terraform apply -var-file=prod.tfvars
terraform workspace list
# default
# dev
# * staging
# prod
# Branch behaviour on workspace name
locals {
instance_type = terraform.workspace == "prod" ? "m5.2xlarge" : "t3.medium"
node_count = terraform.workspace == "prod" ? 10 : 2
}
Why are workspaces risky for production?
All workspaces share the same backend and configuration code. A change to the code affects every workspace, and it is easy to run a command against the wrong workspace.
# DANGER: you think you are in dev, but you are in prod
terraform workspace show
# prod <- oops
terraform destroy -auto-approve
# Just destroyed production infrastructure!
How do you structure multiple environments safely?
The recommended pattern is per-environment root configurations that compose shared modules.
terraform/
modules/
network/ # reusable VPC, subnets, routing
eks/ # reusable EKS cluster
database/ # reusable RDS instance
environments/
dev/
main.tf # calls modules with dev inputs
backend.tf # separate state key: dev/terraform.tfstate
dev.tfvars # dev variable values
staging/
main.tf
backend.tf # staging/terraform.tfstate
staging.tfvars
prod/
main.tf
backend.tf # prod/terraform.tfstate
prod.tfvars
# environments/dev/backend.tf
terraform {
backend "s3" {
bucket = "nbi-terraform-state"
key = "dev/banking/terraform.tfstate" # separate state
region = "ap-south-1"
dynamodb_table = "nbi-terraform-locks"
}
}
# environments/dev/main.tf
module "network" {
source = "../../modules/network"
vpc_cidr = "10.1.0.0/16" # dev CIDR
environment = "dev"
}
module "eks" {
source = "../../modules/eks"
vpc_id = module.network.vpc_id
subnet_ids = module.network.private_subnet_ids
node_instance_type = "t3.medium" # small for dev
min_nodes = 1
max_nodes = 3
}
# environments/prod/main.tf
module "network" {
source = "../../modules/network"
vpc_cidr = "10.0.0.0/16" # prod CIDR
environment = "prod"
}
module "eks" {
source = "../../modules/eks"
vpc_id = module.network.vpc_id
subnet_ids = module.network.private_subnet_ids
node_instance_type = "m5.2xlarge" # large for prod
min_nodes = 5
max_nodes = 20
}
How do workspaces and per-environment roots compare?
| Aspect | Workspaces | Per-environment roots |
|---|---|---|
| State isolation | Same backend, different keys | Completely separate backends |
| Code sharing | Same code for all | Shared modules, thin roots |
| Accidental cross-env | Easy (wrong workspace) | Hard (separate directory) |
| Blast radius | All envs share one config | Each env is independent |
| Best for | Ephemeral sandboxes | Dev/staging/prod separation |
What are common mistakes?
The most common mistake is copying entire configurations into three folders instead of using shared modules. The copies drift, and staging no longer matches production.
# WRONG - copy-paste configuration per environment
terraform/
dev/main.tf # full copy of all resources
staging/main.tf # diverges over time
prod/main.tf # different from staging -> bugs slip past
# CORRECT - shared modules, thin per-environment roots
terraform/
modules/ # shared logic, single source of truth
environments/
dev/main.tf # calls modules with dev values
prod/main.tf # calls modules with prod values
What should a senior engineer know?
Modules should be versioned when published to a registry. Pin the version in the caller so that an upstream module change does not silently affect production.
The composition pattern gives strong isolation: each environment has its own locked, versioned state and its own blast radius, while the shared modules guarantee the environments stay structurally consistent.
# Apply only the prod environment
cd environments/prod
terraform init
terraform plan -var-file=prod.tfvars
terraform apply -var-file=prod.tfvars
# This can NEVER touch dev or staging state
Visual Overview
Module Composition Pattern
===========================
modules/ (shared, reusable)
├── network/ VPC, subnets, routing
│ variables.tf, main.tf, outputs.tf
├── eks/ EKS cluster, node groups
└── database/ RDS instance
environments/
├── dev/ (thin root, own state)
│ main.tf ─→ calls modules with dev values
│ backend.tf ─→ state: dev/terraform.tfstate
│
├── staging/ (thin root, own state)
│ main.tf ─→ calls modules with staging values
│ backend.tf ─→ state: staging/terraform.tfstate
│
└── prod/ (thin root, own state)
main.tf ─→ calls modules with prod values
backend.tf ─→ state: prod/terraform.tfstate
Data Flow
==========
modules/network
│ outputs: vpc_id, subnet_ids
▼
modules/eks
│ inputs: vpc_id, subnet_ids
│ outputs: cluster_endpoint
▼
modules/database
│ inputs: subnet_ids
│ outputs: db_endpoint
▼
Application connects to cluster_endpoint and db_endpoint
Key Takeaway
A Terraform module is a reusable, parameterized container of resources that you define once and instantiate many times with different inputs. Good modules have typed variables, clear outputs, a single responsibility, and are versioned when published. They keep infrastructure code DRY and structurally consistent across environments.
A workspace is a named state copy of one configuration. Workspaces are useful for ephemeral sandboxes, but for separating dev, staging, and production the safer pattern is per-environment root configurations that compose shared modules, each with its own isolated backend state.
The composition pattern gives strong isolation: each environment has its own locked, versioned state and its own blast radius, while the shared modules guarantee the environments stay structurally identical. A mistake in dev can never touch production state.
Remember one sentence: share logic in modules, isolate environments in per-environment roots with separate state, and reserve workspaces for lightweight sandboxes.
---