DEV Community

Cover image for Terraform Workspaces vs Folders: How to Manage Dev, Staging, and Prod Environments
ADITYA RAJ
ADITYA RAJ

Posted on AI-assisted

Terraform Workspaces vs Folders: How to Manage Dev, Staging, and Prod Environments

When you first start with Terraform, one question shows up almost immediately: how do I manage dev, staging, and prod without breaking something?

Terraform gives you two built-in ways to do it: folder-based environments and workspaces. I've used both on real Azure projects, so here's what each one looks like, where each one hurts, and which one I'd pick.

Table of Contents

The Problem: Why Environment Isolation Matters

Picture this: you're cleaning up dev, you run terraform destroy, and a minute later you realize the terminal was pointed at prod.

That's the nightmare. You want your environments isolated, so a mistake in one can't touch another. Both methods below solve this, but with very different levels of safety.

Method 1: Folder-Based Environments

This is what most teams use in production, and it's my current setup. Every environment gets its own folder.

Project structure

terraform/
├── bootstrap/
│   └── backend/          # Creates the resource group, storage account + container for state
├── environments/
│   ├── dev/
│   │   ├── backend.tf    # key = "dev.terraform.tfstate"
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── terraform.tfvars
│   └── prod/
│       ├── backend.tf    # key = "prod.terraform.tfstate"
│       ├── main.tf
│       ├── variables.tf
│       └── terraform.tfvars
└── modules/
    └── network/          # Reusable code shared by every environment
Enter fullscreen mode Exit fullscreen mode

How it works

Each environment is its own Terraform root. That means a separate state file, a separate backend key, and its own terraform.tfvars. Dev and prod don't know the other exists.

A typical backend file looks like this:

# environments/dev/backend.tf
terraform {
  backend "azurerm" {
    resource_group_name  = "rg-terraform-state"
    storage_account_name = "mycompanytfstate"
    container_name       = "tfstate"
    key                  = "dev.terraform.tfstate"
  }
}
Enter fullscreen mode Exit fullscreen mode

Azure Blob Storage handles state locking for you using blob leases, so there's no separate lock table to create (unlike DynamoDB on AWS).

To deploy, you move into the folder you want:

az login
cd environments/dev
terraform init
terraform apply

cd ../prod
terraform init
terraform apply
Enter fullscreen mode Exit fullscreen mode

Inside each environment's main.tf, you call shared modules:

module "network" {
  source        = "../../modules/network"
  address_space = ["10.0.0.0/16"]
  env           = "dev"
}
Enter fullscreen mode Exit fullscreen mode

The module holds the logic. The environment folder only decides the values.

Pros

  • Strong isolation. Prod state can't be touched from the dev folder. You can even give prod its own storage account and subscription.
  • Different infrastructure per environment. For example, Standard_B1s VMs in dev and Standard_B2s in prod, with no hacks.
  • Safe for CI/CD and teams. Each pipeline points at one folder.

Cons

  • Some duplication. You repeat a few files per environment. Pulling shared code into modules/ keeps this small.

Use folders when: you have real, long-lived environments like dev, QA, and prod.

Method 2: Workspace-Based Environments

With workspaces, you keep one folder, and Terraform manages multiple state files for you behind the scenes.

Project structure

terraform/
├── bootstrap/
│   └── backend/          # Same as before
└── environments/         # Only ONE folder now
    ├── backend.tf        # key = "app.terraform.tfstate" (shared by all)
    ├── main.tf
    ├── locals.tf
    └── dev.tfvars / prod.tfvars
Enter fullscreen mode Exit fullscreen mode

There are no more dev/ and prod/ folders.

How it works

Terraform gives you a built-in value called terraform.workspace. You can use it to change behavior per environment:

# locals.tf
locals {
  env = terraform.workspace # "dev" or "prod"

  vm_size = {
    dev  = "Standard_B1s"
    prod = "Standard_B2s"
  }
}

# main.tf
resource "azurerm_linux_virtual_machine" "app" {
  name                = "vm-app-${local.env}"
  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location
  size                = lookup(local.vm_size, local.env, "Standard_B1s")

  # admin_username, admin_ssh_key, os_disk, source_image_reference,
  # and network_interface_ids omitted for brevity

  tags = {
    Environment = local.env
  }
}
Enter fullscreen mode Exit fullscreen mode

Workspace commands

terraform init
terraform workspace new dev
terraform workspace new prod
terraform workspace select dev
terraform workspace show     # always check this before you apply!
Enter fullscreen mode Exit fullscreen mode

With the Azure Storage backend, Terraform keeps each workspace's state as its own blob in the same container. The workspace name is added to the end of your key:

tfstate/app.terraform.tfstate          # default workspace
tfstate/app.terraform.tfstateenv:dev   # dev workspace
tfstate/app.terraform.tfstateenv:prod  # prod workspace
Enter fullscreen mode Exit fullscreen mode

Pros

  • No code duplication. One set of files for everything.
  • Great for temporary environments like feature-login or a personal dev stack.

Cons

  • Weak isolation. Every workspace shares the same backend configuration, storage account, and credentials.
  • Easy to make a costly mistake. You run terraform apply thinking you're in dev, but you're in prod.
  • It gets messy fast. If your environments differ, you end up with lookup() and conditionals everywhere.

Use workspaces when: you need short-lived, identical clones, like PR previews or developer sandboxes.

Folder vs Workspace: Quick Comparison

Feature Folder Method Workspace Method
Isolation Very high Low
Risk of wrong-env mistakes Low Higher, easy to forget your workspace
Different infra per environment Easy Hard, needs lots of conditionals
Code duplication Some (reduced with modules) None
Best for dev, staging, prod dev-rahul, pr-123, temp tests

My Rule of Thumb (and a Hybrid Setup)

  • Folders for permanent environments (dev, staging, prod).
  • Workspaces for temporary environments inside dev.

That's how I run it now. environments/dev/ is a folder, but inside it each developer gets a workspace for a personal stack. Prod stays fully separate, so a workspace mix-up can never reach it.

Quick Tip: What to Put in .gitignore

Keep local Terraform files out of git:

.terraform/
*.tfstate
*.tfstate.backup
Enter fullscreen mode Exit fullscreen mode

Commit your .tf files, your .tfvars files (as long as they contain no secrets), and .terraform.lock.hcl. The lock file pins provider versions so everyone on your team and in CI uses the same ones.

Conclusion

If you only remember one thing: folders give you safety, workspaces give you speed.

  • Real environments that live for months or years → folders
  • Throwaway copies that live for days → workspaces
  • Not sure → start with folders. It's easier to relax isolation later than to add it after an incident.

Over to You

Which method are you using on your Azure projects: folders, workspaces, or a mix? Have you ever run apply in the wrong environment? Share your story in the comments, and follow me for more Terraform and DevOps posts.

Top comments (0)