
Terraform workspaces provide a mechanism to manage multiple distinct state files for the same configuration. By default, Terraform operates in a workspace named default. When you create a new workspace (e.g., terraform workspace new dev), Terraform creates a separate state file for that workspace (e.g., terraform.tfstate.d/dev/terraform.tfstate). All operations within that workspace read from and write to its dedicated state file.
This allows you to deploy the same infrastructure code to different environments—development, staging, production—without altering the code itself, by simply switching workspaces.
Key Terraform CLI workspace commands include:
terraform workspace new <workspace-name>: Creates a new workspaceterraform workspace list: Lists existing workspacesterraform workspace select <workspace-name>: Switches to a different workspaceterraform workspace show: Displays the current workspace nameterraform workspace delete <workspace-name>: Removes a workspaceThe terraform.workspace interpolation allows configurations to dynamically reference the current workspace name, enabling slight variations based on the active environment:
resource "aws_instance" "example" {
ami = "ami-0c55b31ad20f0c502"
instance_type = terraform.workspace == "prod" ? "t2.medium" : "t2.micro"
tags = {
Name = "Server-${terraform.workspace}"
Environment = terraform.workspace
}
}The term "workspace" carries different meanings across Terraform's ecosystem. Both depend on a properly-configured backend — see our Terraform state and remote backends guide for the underlying state model:
CLI workspaces are a local feature for managing multiple state files from a single Terraform configuration. They enable state isolation for different environments or parallel development scenarios. However, they have inherent limitations:
terraform.workspace becomes unwieldy at scaleCLI workspaces are best suited for:

Terraform Automation and Collaboration Software (TACOs) like Terraform Cloud and Scalr evolved workspace concepts into comprehensive management units. These remote workspaces serve as distinct deployment targets with:

Remote workspaces are essential for:
Consistency and control are paramount when managing configurations across development, staging, and production environments. Most environment differences live in input variables and tfvars files — keep environment-specific values there, not hardcoded across configurations.
A widely-accepted best practice in the Terraform community is using separate directories for distinct, long-lived environments. This approach provides maximum isolation:
.
├── environments
│ ├── development
│ │ ├── backend.tf
│ │ ├── main.tf
│ │ └── dev.tfvars
│ ├── staging
│ │ ├── backend.tf
│ │ ├── main.tf
│ │ └── staging.tfvars
│ └── production
│ ├── backend.tf
│ ├── main.tf
│ └── prod.tfvars
├── modules
│ ├── vpc
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── ec2_instance
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
Benefits of directory-based separation:
When to use workspaces instead:
Use CLI workspaces for short-term, temporary environments built from the exact same configuration—such as an isolated sandbox for testing a feature branch or a parallel environment for code review. Workspaces are not recommended for distinct prod/staging/dev environments due to shared backend configurations and the potential for complex conditional logic.
Define all variables in variables.tf and use .tfvars files for environment-specific values:
# variables.tf
variable "aws_region" {
description = "AWS region"
type = string
}
variable "instance_count" {
description = "Number of instances"
type = number
}
variable "instance_type" {
description = "EC2 instance type"
type = string
}Then create environment-specific files:
# environments/production/prod.tfvars
aws_region = "us-east-1"
instance_count = 5
instance_type = "m5.large"# environments/development/dev.tfvars
aws_region = "us-west-2"
instance_count = 1
instance_type = "t2.micro"Critical rule: Never store sensitive data in .tfvars files. Use dedicated secret managers (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault) and integrate them with your CI/CD pipeline or Terraform provider.
Strive for staging environments that closely mirror production:
Establish a clear progression path:
develop, staging, main)terraform plan output in PRs for visibilityOverreliance on workspaces for environment separation leads to hidden risks:
# ❌ Problematic: Workspace-based environment separation
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "terraform.tfstate"
region = "us-west-2"
}
}
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = terraform.workspace == "prod" ? "m5.large" : "t2.micro"
tags = {
Environment = terraform.workspace
}
}Problems with this approach:
# ✅ Recommended: Directory-based separation
# ./environments/prod/main.tf
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "environments/prod/terraform.tfstate"
region = "us-west-2"
}
}
module "application" {
source = "../../modules/application"
environment = "prod"
instance_type = "m5.large"
}
# ./environments/dev/main.tf
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "environments/dev/terraform.tfstate"
region = "us-west-2"
}
}
module "application" {
source = "../../modules/application"
environment = "dev"
instance_type = "t2.micro"
}Advantages:
The choice between centralized (monorepo) and distributed (polyrepo) repository structures fundamentally affects scalability, collaboration, and operational complexity. The repo-structure decision is tightly coupled to your pipeline design — see CI/CD and GitOps for Terraform & OpenTofu for the trade-offs.
Architecture:
my-infrastructure-monorepo/
├── environments/
│ ├── dev/
│ ├── staging/
│ └── prod/
├── modules/
│ ├── vpc/
│ ├── database/
│ └── app-tier/
├── policies/
└── docs/
Advantages:
Disadvantages:
Best for:
Architecture:
my-org/
├── infra-networking/ (VPC, subnets, routing)
├── infra-database/ (RDS, data warehouse)
├── infra-app/ (Application tier resources)
├── terraform-modules/ (Shared module library)
└── platform-policies/ (OPA policies)
Advantages:
Disadvantages:
Best for:
Choose based on:
Pro tip: Regardless of choice, use centralized platforms like Scalr that provide unified management across both monorepo and polyrepo structures.

How you organize Terraform code determines your ability to scale, maintain security, and collaborate effectively. For a 4-part deep dive, see our Structuring Terraform and OpenTofu series, Part 1/4. For module fundamentals, Terraform Modules Explained.
Standard File Layout:
Every module should follow a consistent structure:
# versions.tf
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}Naming Conventions:
aws_instance.web_server)ram_size_gb, enable_monitoring)vpc_id, database_endpoint)network.tf, compute.tf, security.tf)Modules are your foundation for DRY (Don't Repeat Yourself) infrastructure. Poor module design leads to code duplication and maintenance nightmares.
Module Design Principles:
Example well-designed module:
# modules/vpc/main.tf
resource "aws_vpc" "main" {
cidr_block = var.cidr_block
enable_dns_hostnames = var.enable_dns_hostnames
tags = {
Name = "${var.project_name}-vpc"
Project = var.project_name
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-igw"
}
}
# modules/vpc/variables.tf
variable "project_name" {
description = "The name of the project"
type = string
}
variable "cidr_block" {
description = "The CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
}
variable "enable_dns_hostnames" {
description = "Enable DNS hostnames in the VPC"
type = bool
default = true
}
# modules/vpc/outputs.tf
output "vpc_id" {
description = "The ID of the VPC"
value = aws_vpc.main.id
}
output "internet_gateway_id" {
description = "The ID of the internet gateway"
value = aws_internet_gateway.main.id
}State is critical. Protect and manage it meticulously.
State Splitting:
Avoid monolithic state files. Break down state by environment, region, and component:
production/
networking/terraform.tfstate
app-main/terraform.tfstate
databases/terraform.tfstate
staging/
networking/terraform.tfstate
app-main/terraform.tfstate
Benefits:
Remote Backends:
Always use remote backends (AWS S3, Azure Blob Storage, Google Cloud Storage) with:
terraform {
backend "s3" {
bucket = "my-tf-state-bucket-prod"
key = "production/networking/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "my-tf-state-lock-prod"
encrypt = true
}
}Essential remote backend features:
Logical Backend Keys:
Structure paths consistently for easy management:
env:/<environment>/<region>/<component>/terraform.tfstate
Example:
env:/production/us-east-1/networking/terraform.tfstate
env:/production/us-east-1/app-tier/terraform.tfstate
env:/staging/us-east-1/networking/terraform.tfstate
Managing Dependencies:
Use terraform_remote_state to read outputs from other isolated state files:
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "my-tf-state-bucket"
key = "production/networking/terraform.tfstate"
region = "us-east-1"
}
}
resource "aws_instance" "app" {
subnet_id = data.terraform_remote_state.networking.outputs.subnet_id
# ...
}Design dependencies carefully to avoid overly complex or circular relationships.
As infrastructure grows, basic workspace management becomes insufficient. Sophisticated approaches are required.
terraform.workspace makes configurations complex and error-prone
3. Minimize blast radius: Keep state files small and focused
- One per environment per region per component
- Avoid "kitchen sink" configurations
- Use separate state for shared services
Use Terragrunt for orchestration: Manage dependencies and reduce boilerplate:
# environments/prod/vpc/terragrunt.hcl
terraform {
source = "../../../modules/vpc"
}
inputs = {
cidr_block = "10.0.0.0/16"
environment = "prod"
}Implement hierarchical configuration: Use directory structures and configuration inheritance:
global/terraform.tfvars (common to all envs)
environments/prod/terraform.tfvars (prod-specific)
environments/prod/backend.tf (prod backend)
Terraform Cloud / Scalr:
Remote workspaces support structured dependencies through:
tfe_outputs (TFC) or terraform_remote_stateExample dependency flow:
Networking Workspace (creates VPC, subnets)
↓ (Run Trigger)
Application Workspace (references networking outputs)
↓ (Run Trigger)
Database Workspace (references app outputs)
Modern infrastructure requires evolved approaches to environment management. Layer in Policy as Code so policies apply per-environment, drift detection to catch out-of-band changes, and audit logs for compliance evidence.
Secret Management:
sensitive = trueAccess Control:
Network Security as Code:
CI/CD Pipeline Stages:
terraform fmt)Mandatory Approval Gates:
terraform plan in PR for visibilityUse Open Policy Agent (OPA) with Rego or HashiCorp Sentinel to automatically enforce policies:
# Enforce S3 encryption
package terraform.analysis
deny[msg] {
bucket := input.resource_changes[_]
bucket.type == "aws_s3_bucket"
bucket.mode == "managed"
not bucket.change.after.server_side_encryption_configuration
msg := sprintf("S3 Bucket '%s' must have server-side encryption", [bucket.name])
}Policies can enforce:
Terragrunt: Wrapper for Terraform that:
run-all commands for bulk operationsAtlantis: Enables GitOps workflows:
terraform plan on PR creationTACO Platforms (Scalr, Terraform Cloud, Spacelift, Env0):
Key Metrics:
Feedback Loops:
Managing Terraform environments effectively requires moving beyond basic workspace commands to adopt a comprehensive, disciplined approach. The evolution from CLI workspaces to sophisticated remote platforms reflects the growing operational complexity of infrastructure at scale.
Key Takeaways:
By implementing these practices, you can build robust, secure, and scalable Terraform environments that support your team's growth while maintaining stability, security, and compliance across your entire infrastructure-as-code landscape.
