Skip to main content

Migrating from Terraform Workspaces

Terraform workspaces solve a simple problem: deploy the same code to multiple environments using one state backend. Atmos can adopt that workspace-backed state while moving environment configuration into explicit stack files.

How Workspaces Fit With Atmos

Workspaces are a valid way to organize Terraform state. When migrating to Atmos, the important shift is making configuration explicit in stacks while preserving the state layout that already works for your team.

1. Shared Backend Configuration

Terraform workspaces share backend configuration and differentiate state by workspace name. Atmos can keep that model, or configure separate backends per stack when a team explicitly wants that operating model.

# All workspaces use this backend configuration
terraform {
backend "s3" {
bucket = "my-terraform-state" # Same bucket for dev, staging, prod
key = "terraform.tfstate" # Workspace adds the environment-specific path
region = "us-east-1"
}
}

If your team wants independent backend configuration per environment, use the optional separate-backend migration path below.

2. Configuration Often Lives in Code

Workspace-based repos often put environment-specific choices in Terraform expressions. Atmos moves those choices into stack YAML:

variables.tf
variable "instance_type" {
default = "t3.micro"
}

locals {
# Workspace-specific logic
instance_type = terraform.workspace == "prod" ? "m5.large" :
terraform.workspace == "staging" ? "t3.medium" :
"t3.micro"

enable_monitoring = terraform.workspace == "prod" ? true : false

# This can grow as environments diverge
}

3. Hidden State

Which workspaces exist? What resources do they contain? You have to inspect the state backend or track it manually.

$ terraform workspace list
default
dev
staging
prod
old-test-workspace # Is this safe to delete?
johns-experiment # What is this?

4. Component Versioning

Workspaces usually run the same root module version for every environment. Atmos stacks can point environments at different component versions when a rollout needs that control.

5. Optional Backend Changes

Teams that want separate backend configuration can migrate workspaces into per-stack backends. This is optional and should be planned carefully because it moves state.

How Atmos Helps

Atmos separates code (Terraform components) from configuration (stack YAML files). This gives you:

Atmos Advantages

  • Workspace-compatible state - Keep existing workspace-backed state, or choose separate backend configuration
  • Environment-specific config - No giant case statements
  • Visible configuration - Stacks are files you can search, version, review
  • Component reuse - Share code, customize config via inheritance
  • Optional backend changes - Move state backends only when your team chooses that model

Migration Strategy

Before: Workspace-Based Setup

main.tf
terraform {
backend "s3" {
bucket = "terraform-state"
key = "vpc/terraform.tfstate"
region = "us-east-1"
}
}

variable "environment" {
default = "dev"
}

variable "cidr_block" {
default = "10.0.0.0/16"
}

locals {
# Workspace-specific logic
cidr_block = terraform.workspace == "prod" ? "10.100.0.0/16" :
terraform.workspace == "staging" ? "10.50.0.0/16" :
"10.0.0.0/16"
}

resource "aws_vpc" "main" {
cidr_block = local.cidr_block

tags = {
Environment = terraform.workspace
}
}

Deploy:

terraform workspace select prod
terraform apply # Which VPC am I deploying? Not obvious!

After: Atmos Stacks

Step 1: Create reusable component (generic code)

components/terraform/vpc/main.tf
# No workspace logic!
variable "cidr_block" {
description = "VPC CIDR block"
type = string
}

variable "environment" {
description = "Environment name"
type = string
}

resource "aws_vpc" "main" {
cidr_block = var.cidr_block

tags = {
Environment = var.environment
}
}

Step 2: Create environment-specific stacks (config)

stacks/prod.yaml
terraform:
backend_type: s3
backend:
s3:
bucket: terraform-state-prod # Isolated backend
key: terraform.tfstate
region: us-east-1

components:
terraform:
vpc:
backend:
s3:
workspace_key_prefix: vpc
vars:
cidr_block: "10.100.0.0/16"
environment: prod
stacks/dev.yaml
terraform:
backend_type: s3
backend:
s3:
bucket: terraform-state-dev # Separate backend!
key: terraform.tfstate
region: us-east-1

components:
terraform:
vpc:
backend:
s3:
workspace_key_prefix: vpc
vars:
cidr_block: "10.0.0.0/16"
environment: dev

Deploy:

atmos terraform apply vpc -s prod # Crystal clear: VPC in prod
atmos terraform apply vpc -s dev # VPC in dev

No workspace selection. No hidden state. Just explicit, declarative configuration.

Learn more: atmos terraform apply | atmos terraform plan

Step-by-Step Migration

1. Extract Workspace Logic

Identify all workspace-specific logic in your Terraform code:

Before:

locals {
instance_type = terraform.workspace == "prod" ? "m5.large" : "t3.small"
enable_monitoring = terraform.workspace == "prod" ? true : false
backup_retention = terraform.workspace == "prod" ? 30 : 7
}

After (convert to variables):

components/terraform/app/variables.tf
variable "instance_type" {
description = "EC2 instance type"
type = string
}

variable "enable_monitoring" {
description = "Enable CloudWatch monitoring"
type = bool
}

variable "backup_retention" {
description = "Backup retention in days"
type = number
}

2. Create Stack Configurations

For each workspace, create a stack file:

stacks/prod.yaml
components:
terraform:
app:
vars:
instance_type: m5.large
enable_monitoring: true
backup_retention: 30
stacks/dev.yaml
components:
terraform:
app:
vars:
instance_type: t3.small
enable_monitoring: false
backup_retention: 7

3. Choose a State Backend Strategy

Atmos can use your existing workspace-backed state. Moving state to separate backends is optional and requires careful planning to avoid state loss.

Option A: Keep Workspace State (Easiest)

You can keep using workspace-based state with Atmos:

stacks/prod.yaml
terraform:
backend_type: s3
backend:
s3:
bucket: terraform-state # Same bucket
key: vpc/terraform.tfstate
region: us-east-1
workspace_key_prefix: env # Uses workspace structure

components:
terraform:
vpc:
settings:
terraform:
workspace: prod # Selects workspace "prod"

This lets you migrate incrementally without touching state.

Option B: Move to Separate Backends (Optional)

If your team explicitly wants independent backend configuration per environment, migrate each workspace to its own backend:

  1. Export state from workspace:

    terraform workspace select prod
    terraform state pull > prod.tfstate
  2. Configure new backend:

    stacks/prod.yaml
    terraform:
    backend_type: s3
    backend:
    s3:
    bucket: terraform-state-prod # New bucket
    key: vpc.tfstate
    region: us-east-1
  3. Initialize and push state:

    atmos terraform init vpc -s prod
    terraform state push prod.tfstate
  4. Verify:

    atmos terraform plan vpc -s prod # Should show no changes

4. Remove Workspace Logic

Clean up your Terraform code:

Remove:

# DELETE workspace references
locals {
env = terraform.workspace # Remove
}

# DELETE workspace conditionals
count = terraform.workspace == "prod" ? 1 : 0 # Remove

Replace with variables:

variable "environment" {
description = "Environment name"
type = string
}

variable "create_feature" {
description = "Whether to create optional feature"
type = bool
default = false
}

5. Update CI/CD

Before:

# Old CI/CD
terraform workspace select $ENV
terraform plan
terraform apply -auto-approve

After:

# New CI/CD
atmos terraform plan $COMPONENT -s $STACK
atmos terraform apply $COMPONENT -s $STACK -auto-approve

GitHub Actions example:

- name: Deploy VPC
run: |
atmos terraform apply vpc -s ${{ matrix.stack }}
strategy:
matrix:
stack: [dev, staging, prod]

Handling Common Patterns

Pattern 1: Workspace-Specific Resources

Before:

resource "aws_instance" "monitoring" {
count = terraform.workspace == "prod" ? 1 : 0
# ...
}

After:

components/terraform/monitoring/main.tf
variable "enabled" {
type = bool
default = false
}

resource "aws_instance" "this" {
count = var.enabled ? 1 : 0
# ...
}
stacks/prod.yaml
components:
terraform:
monitoring:
vars:
enabled: true
stacks/dev.yaml
components:
terraform:
monitoring:
vars:
enabled: false

Pattern 2: Workspace in Tags

Before:

tags = {
Environment = terraform.workspace
}

After:

components/terraform/vpc/main.tf
variable "environment" {
type = string
}

tags = {
Environment = var.environment
}
stacks/prod.yaml
vars:
environment: production

components:
terraform:
vpc:
vars:
environment: '{{ .vars.environment }}'

Pattern 3: Workspace-Specific Data Sources

Before:

data "aws_ami" "app" {
most_recent = true

filter {
name = "name"
values = [terraform.workspace == "prod" ? "prod-ami-*" : "dev-ami-*"]
}
}

After:

components/terraform/app/main.tf
variable "ami_prefix" {
type = string
}

data "aws_ami" "app" {
most_recent = true

filter {
name = "name"
values = ["${var.ami_prefix}-*"]
}
}
stacks/prod.yaml
components:
terraform:
app:
vars:
ami_prefix: prod-ami

Migration Checklist

  • Audit all terraform.workspace references in code
  • Convert workspace conditionals to variables
  • Create Atmos atmos.yaml configuration
  • Create stack files for each workspace
  • Keep workspace-backed state, unless your team explicitly chooses separate backends
  • If moving state, test the migration in dev first
  • Verify atmos terraform plan shows no changes
  • Update CI/CD pipelines
  • Update team documentation
  • Update workspace commands to stack-based Atmos commands where appropriate

Benefits After Migration

  • Explicit configuration - Environment settings live in stack files
  • Workspace-compatible state - Keep existing workspace-backed state
  • Environment-specific settings - No complex conditionals
  • Better code review - Stack changes visible in YAML diffs
  • Reusable components - Same code, different configs
  • Easier testing - Deploy different component versions per stack

Common Questions

Get Help

Migrating from workspaces? We're here to help:

Next Steps

Ready to get started?