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
- Method 1: Folder-Based Environments
- Method 2: Workspace-Based Environments
- Folder vs Workspace: Quick Comparison
- My Rule of Thumb (and a Hybrid Setup)
- Quick Tip: What to Put in .gitignore
- Conclusion
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
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"
}
}
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
Inside each environment's main.tf, you call shared modules:
module "network" {
source = "../../modules/network"
address_space = ["10.0.0.0/16"]
env = "dev"
}
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_B1sVMs in dev andStandard_B2sin 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
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
}
}
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!
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
Pros
- No code duplication. One set of files for everything.
-
Great for temporary environments like
feature-loginor 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 applythinking you're indev, but you're inprod. -
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
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)