close

Terraform Modules and Workspaces: How MyCoCo Scaled from Copy-Paste to DRY Infrastructure

How a growing SaaS company leveraged Terraform workspaces and modules to eliminate 1,500+ lines of duplicate code while maintaining environment isolation

Elevator Pitch

When teams manage multiple environments in Terraform, they often duplicate entire configurations, creating a maintenance nightmare. Terraform workspaces combined with modules provide an elegant solution—one codebase serves all environments through workspace-aware configurations. Read how MyCoCo transformed their infrastructure management using workspaces to achieve true DRY (Don't Repeat Yourself) principles.

TL;DR

The Problem: Copy-pasted Terraform configurations across dev/staging/production led to environment drift, deployment failures, and 20+ hours weekly maintenance overhead.

The Solution: Terraform workspaces for environment separation combined with modules for code reuse and workspace-specific variable files.

The Impact: MyCoCo reduced configuration management time by 70%, eliminated environment drift, and deployed new environments in hours instead of days.

Key Implementation: Use terraform.workspace with modular design patterns and structured terraform.workspace.tfvars files for environment-specific configurations.

Bottom Line: If you're maintaining separate Terraform directories per environment, workspaces + modules will transform your infrastructure management.

Click to enlarge
Terraform Modules and Workspaces Architecture Diagram

From copy-paste chaos to modular design: The transformation of MyCoCo's infrastructure management

The Challenge: MyCoCo's Environment Sprawl

"Why do we have three different versions of our RDS configuration?" Jordan asked during his first week as MyCoCo's platform engineer. The answer revealed a deeper problem.

Sam, the senior DevOps engineer, explained their evolution: "We started with one environment, then copied everything for staging, then copied again for production. Now we maintain three separate Terraform directories."

The structure told the story:

infrastructure/
├── dev/
│   ├── main.tf      # 500 lines
│   ├── rds.tf       # 200 lines
│   └── variables.tf # 150 lines
├── staging/
│   ├── main.tf      # 497 lines (slightly different)
│   ├── rds.tf       # 198 lines (minor variations)
│   └── variables.tf # 148 lines (different values)
└── production/
    ├── main.tf      # 502 lines (more differences)
    ├── rds.tf       # 205 lines (critical variations)
    └── variables.tf # 152 lines (production values)

Alex, VP of Engineering, quantified the impact: "Every infrastructure change requires three pull requests, three code reviews, and three separate terraform apply commands. We're burning 20 hours per week just keeping these in sync."

The breaking point came when a critical security patch needed to be applied across all environments. What should have been a 30-minute task turned into a two-day project of carefully updating each environment, testing separately, and hoping nothing was missed.

The Solution: Workspaces + Modules Architecture

MyCoCo's transformation centered on two key Terraform features: workspaces for environment isolation and modules for code reuse.

Before: Duplicate Directories

# production/rds.tf - Copied and modified for each environment
resource "random_password" "db_password" {
  length  = 16
  special = true
}

# Note: Consider using password_wo instead of password in Terraform 1.11+
# for better security (write-only attribute that doesn't show in plans)
resource "aws_db_instance" "main" {
  identifier     = "mycoco-prod"
  engine         = "postgres"
  engine_version = "15.3"
  instance_class = "db.t3.large"  # Hard-coded per environment

  allocated_storage = 100
  storage_encrypted = true

  username = "dbadmin"
  password = random_password.db_password.result

  # 50+ more lines of mostly identical configuration...
}

After: Workspace-Aware Module

# main.tf - Single configuration for all environments
locals {
  workspace_config = {
    dev = {
      instance_class    = "db.t3.micro"
      allocated_storage = 20
      backup_retention  = 1
    }
    staging = {
      instance_class    = "db.t3.medium"
      allocated_storage = 50
      backup_retention  = 7
    }
    production = {
      instance_class    = "db.t3.large"
      allocated_storage = 100
      backup_retention  = 30
    }
  }

  env_config = local.workspace_config[terraform.workspace]
}

module "database" {
  source = "./modules/rds"

  environment       = terraform.workspace
  instance_class    = local.env_config.instance_class
  allocated_storage = local.env_config.allocated_storage
  backup_retention  = local.env_config.backup_retention

  # Common configuration for all environments
  engine         = "postgres"
  engine_version = "15.3"

  # Workspace-aware naming
  identifier = "mycoco-${terraform.workspace}"
}

The Module Structure

# modules/rds/variables.tf
variable "identifier" {
  description = "The name of the RDS instance"
  type        = string
}

variable "engine" {
  description = "The database engine"
  type        = string
}

variable "engine_version" {
  description = "The engine version to use"
  type        = string
}

variable "instance_class" {
  description = "The instance type of the RDS instance"
  type        = string
}

variable "allocated_storage" {
  description = "The allocated storage in gibibytes"
  type        = number
}

variable "backup_retention" {
  description = "The days to retain backups for"
  type        = number
  default     = 7
}

variable "environment" {
  description = "The deployment environment"
  type        = string
}
# modules/rds/main.tf
resource "random_password" "db_password" {
  length  = 16
  special = true
}

resource "aws_db_instance" "main" {
  identifier     = var.identifier
  engine         = var.engine
  engine_version = var.engine_version
  instance_class = var.instance_class

  allocated_storage       = var.allocated_storage
  max_allocated_storage   = var.allocated_storage * 2
  storage_encrypted       = true

  db_name  = "mycoco"
  username = "dbadmin"
  password = random_password.db_password.result

  backup_retention_period = var.backup_retention
  backup_window          = "03:00-04:00"

  # Production-only features
  deletion_protection = var.environment == "production" ? true : false
  skip_final_snapshot = var.environment != "production" ? true : false

  tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}
# modules/rds/outputs.tf
output "endpoint" {
  description = "The connection endpoint"
  value       = aws_db_instance.main.endpoint
}

output "database_name" {
  description = "The database name"
  value       = aws_db_instance.main.db_name
}

output "instance_id" {
  description = "The RDS instance ID"
  value       = aws_db_instance.main.id
}

Environment-Specific Variables

# envs/production.tfvars
region               = "ca-central-1"
enable_monitoring    = true
enable_backups       = true
multi_az            = true
performance_insights = true

# envs/staging.tfvars
region               = "ca-central-1"
enable_monitoring    = true
enable_backups       = true
multi_az            = false
performance_insights = false

# envs/dev.tfvars
region               = "ca-central-1"
enable_monitoring    = false
enable_backups       = false
multi_az            = false
performance_insights = false

Workspace Management Workflow

# List available workspaces
$ terraform workspace list
  default
  dev
* staging
  production

# Switch to production
$ terraform workspace select production

# Deploy with workspace-specific config
$ terraform apply -var-file="envs/$(terraform workspace show).tfvars"

Results: MyCoCo's Workspace Transformation

The impact of adopting workspaces with modules was immediate:

70% Reduction in Maintenance Time: Changes that previously required updating three separate codebases now happened in one place. Sam noted, "I make one change, test it in dev workspace, promote through staging, and apply to production—all with the same code."

Zero Environment Drift: With all environments using the same modules, configuration drift became impossible. The only differences were intentional, defined in workspace-specific variables.

Rapid Environment Provisioning: When MyCoCo needed a disaster recovery environment, Jordan created it in two hours: terraform workspace new dr-east && terraform apply. Previously, this would have taken days of copying and modifying configurations.

Clear Environment Isolation: Maya, the security engineer, appreciated the built-in separation: "Each workspace has its own state file. There's no risk of accidentally modifying production while working in dev."

The real validation came during their next security audit. Instead of reviewing three sets of configurations, auditors examined one modular codebase with clear environment separation through workspaces.

Key Takeaways

Start with Workspaces: Before diving into complex module structures, implement workspaces to separate your environments while using the same code.

Use terraform.workspace: Reference the current workspace in your configurations for dynamic resource naming and conditional logic.

Combine with Modules: Workspaces handle environment separation; modules handle code reuse. Together, they eliminate duplication.

Workspace-Specific Variables: Store environment-specific values in separate tfvars files, loaded based on the current workspace.

State Isolation: Each workspace maintains its own state file, providing natural environment isolation without complex backend configurations.

Documentation is Critical: Each module needs clear documentation of required and optional variables, making adoption straightforward. Leverage terraform-docs (a utility to generate documentation from Terraform modules in various output formats) to automate this process.

Consider Community Modules: For complex RDS setups, consider using established modules like terraform-aws-modules/rds which provides workspace-aware configurations out of the box.

For teams drowning in duplicate Terraform configurations, workspaces combined with modules offer a path to sanity. The investment in refactoring pays immediate dividends in reduced maintenance, eliminated drift, and happier engineers.

← Back to Logs

Explore more articles and projects