What is an Environment? #

Almost every team managing serious infrastructure has more than one environment. There’s dev where developers experiment, staging that mirrors production for final testing, and production serving real users. The challenge isn’t creating each one — it’s making sure all three are managed from the same configuration, consistently, and avoiding situations where staging silently differs from production so bugs slip through undetected.

flowchart TD
    A["🌍 Environment"] --> B["Development"]
    A --> C["Staging"]
    A --> D["Production"]

    B --> E["Fast iteration\nLow risk"]
    C --> F["Validation\nBefore prod"]
    D --> G["Stability\nHigh availability"]

    style A fill:#3b82f6,stroke:#1e40af,color:#fff
    style B fill:#10b981,stroke:#059669,color:#fff
    style C fill:#f59e0b,stroke:#d97706,color:#fff
    style D fill:#ef4444,stroke:#dc2626,color:#fff

Why Environments Are Needed #

Environment differences aren’t just a naming convention. Each environment has a different purpose, and those different purposes require structured configuration differences.

DEV (Development)
  Purpose : Experimentation, fast iteration, debugging
  Nature  : May be unstable, data can be deleted anytime
  Size    : Small, minimal — cost efficient
  Access  : Developers have full open access
  Example : 1 t3.micro instance, single-AZ DB without backups

STAGING
  Purpose : Validation before release to production
  Nature  : Must be as close to production as possible
  Size    : Medium — enough for representative testing
  Access  : Restricted — QA team, senior developers
  Example : 2 t3.small instances, DB with backups enabled

PRODUCTION
  Purpose : Serving real users
  Nature  : Must be stable, highly available, secure
  Size    : Full scale according to traffic needs
  Access  : Very restricted — only authorized operators
  Example : 3+ t3.medium instances, multi-AZ DB with 30-day backups

What Differs Between Environments #

Different environments don’t mean totally different configurations — in fact, the structure must be the same, only the values differ.

# The SAME across all environments (configuration structure):
#   - What resources are created (VPC, subnets, EC2, RDS)
#   - How resources connect to each other
#   - Security rules and networking topology
#   - How deployment is done

# What DIFFERS between environments (configuration values):
#   - Instance sizes (t3.micro vs t3.medium vs t3.large)
#   - Instance counts (1 vs 2 vs 5)
#   - Multi-AZ (false vs false vs true)
#   - Backup retention (0 vs 7 vs 30 days)
#   - Deletion protection (false vs false vs true)
#   - Resource names and tags
#   - VPC CIDR blocks (10.0.0.0/16 vs 10.1.0.0/16 vs 10.2.0.0/16)

# This is reflected in variables:
variable "instance_type" {
  # dev: "t3.micro", staging: "t3.small", production: "t3.medium"
}

variable "multi_az" {
  # dev: false, staging: false, production: true
}

variable "backup_retention_days" {
  # dev: 0, staging: 7, production: 30
}

The Principle of Consistent Environments #

There’s one principle most frequently violated in multi-environment management: production drift — a situation where staging and production silently differ because they’re managed differently.

ANTI-PATTERN: Asymmetric environments

  Dev       → Managed with Terraform ✓
  Staging   → Managed with Terraform ✓
  Production → "It's already running, don't touch it" → managed manually ✗

  Consequences:
  - Production has "mysterious" resources that don't exist in staging
  - Bugs that slip through staging can appear in production
  - No way to reproduce the production environment elsewhere
  - During disaster recovery, nobody knows production's exact state

CORRECT: All environments managed from the same configuration

  Dev, Staging, Production → all from the same Terraform configuration
  Differences are only in variable values, not in configuration structure

Multi-Environment Management Challenges #

CHALLENGE 1: STATE ISOLATION
  Each environment must have its own state.
  Dev state must not mix with production state.
  → Solution: remote backend with a separate key per environment

CHALLENGE 2: CONFIGURATION ISOLATION
  Changes to the dev configuration must not automatically reach production.
  → Solution: explicit workflows (PR, review, promotion)

CHALLENGE 3: SEPARATE CREDENTIALS
  Dev and production must not use the same AWS account.
  → Solution: separate AWS accounts per environment (multi-account strategy)

CHALLENGE 4: CONFIGURATION CONSISTENCY
  The configuration structure must be the same across environments.
  → Solution: the same modules, different tfvars

CHALLENGE 5: PROMOTION WORKFLOW
  How does configuration "move up" from dev to staging to production?
  → Solution: GitOps with a branch or tag per environment

Two Main Approaches #

Terraform provides two main approaches for managing multi-environment setups — each with different trade-offs.

APPROACH 1: WORKSPACES
  One configuration directory, separate state per workspace.
  terraform workspace new dev
  terraform workspace new production

  Pros    : One set of code, easy to keep consistent
  Cons    : All environments share the provider and backend config,
            difficult if you need separate AWS accounts per environment

APPROACH 2: DIRECTORY BASED
  A separate directory per environment, each with its own
  configuration and state.

  environments/dev/        → dev state
  environments/staging/    → staging state
  environments/production/ → production state

  Pros    : Full isolation, flexible for large differences
  Cons    : Risk of configuration divergence between environments
            if not managed with discipline

Both approaches will be discussed in more detail in the following article.

flowchart LR
    A["Terraform\nWorkspace"] -->|"Per environment"| B["Different\nState File"]
    B --> C["Same Code\nDifferent Values"]

    style A fill:#3b82f6,stroke:#1e40af,color:#fff
    style B fill:#f59e0b,stroke:#d97706,color:#fff
    style C fill:#10b981,stroke:#059669,color:#fff


The Impact of Environments on Architecture #

The choice of environment strategy affects the entire architecture — from how you deploy to how you monitor.

flowchart TD
    subgraph DEV["Development"]
        D1["Single instance\nt3.micro"]
        D2["No HA"]
        D3["Self-signed cert"]
    end

    subgraph STG["Staging"]
        S1["2 instances\nt3.small"]
        S2["Simulated HA"]
        S3["Test certificate"]
    end

    subgraph PRD["Production"]
        P1["Auto-scaling\nm5.large"]
        P2["Multi-AZ HA"]
        P3["Real certificate"]
    end

    DEV -->|"Promote"| STG
    STG -->|"Promote"| PRD

    style DEV fill:#fff3e0,stroke:#e65100
    style STG fill:#e3f2fd,stroke:#1565c0
    style PRD fill:#e8f5e9,stroke:#2e7d32
# Configuration differences between environments
# DEV:
#   - instance_type: t3.micro (minimal cost)
#   - multi_az: false (no HA needed)
#   - backup_retention: 1 day (saves storage)
#   - deletion_protection: false (easy to clean up)

# PRODUCTION:
#   - instance_type: m5.large (performance)
#   - multi_az: true (high availability)
#   - backup_retention: 30 days (compliance)
#   - deletion_protection: true (safety)

Environment Promotion Workflow #

# Common workflow: develop → staging → production

# 1. Developer pushes code to a feature branch
git push origin feature/add-cache

# 2. The CI pipeline automatically deploys to the dev environment
terraform workspace select dev
terraform apply -auto-approve

# 3. QA testing in dev, then merge to main
git checkout main && git merge feature/add-cache

# 4. The CD pipeline deploys to staging
terraform workspace select staging
terraform plan -out=staging.tfplan
# Manual plan review
terraform apply staging.tfplan

# 5. Smoke tests and UAT in staging
# 6. Manual approval for the production deploy
# 7. Deploy to production with an approval gate
terraform workspace select production
terraform plan -out=prod.tfplan
# Manual approval in the CI/CD tool
terraform apply prod.tfplan

Environment Isolation Patterns #

Good environment isolation prevents cross-environment mistakes that can be fatal.

# Pattern 1: Completely separate state files
# Each environment = separate directory + state
# infrastructure/dev/main.tf    → state: dev.tfstate
# infrastructure/staging/main.tf → state: staging.tfstate
# infrastructure/prod/main.tf   → state: prod.tfstate

# Pattern 2: Workspace-based isolation
terraform workspace select dev
terraform apply
# State is stored in the "dev" workspace, separate from other workspaces

# Pattern 3: Account-level isolation (safest)
# Dev: AWS Account 111111111111
# Staging: AWS Account 222222222222
# Prod: AWS Account 333333333333
provider "aws" {
  region = "ap-southeast-1"
  assume_role {
    role_arn = local.account_roles[terraform.workspace]
  }
}

locals {
  account_roles = {
    dev        = "arn:aws:iam::111111111111:role/TerraformRole"
    staging    = "arn:aws:iam::222222222222:role/TerraformRole"
    production = "arn:aws:iam::333333333333:role/TerraformRole"
  }
}
ISOLATION LEVEL COMPARISON:

Level 1: Separate Directory + State
├── Blast radius: Limited (can still pick the wrong workspace)
├── Cost: Low
└── Complexity: Low

Level 2: Workspace
├── Blast radius: Medium (shared backend config)
├── Cost: Low
└── Complexity: Low

Level 3: Separate AWS Accounts
├── Blast radius: Smallest (full isolation)
├── Cost: Medium (multi-account management)
└── Complexity: High

RECOMMENDATION: Level 3 for production,
Levels 1-2 for development

Environment Configuration Matrix #

CONFIG MATRIX PER ENVIRONMENT:

                    dev         staging      production
────────────────────────────────────────────────────────
instance_type       t3.micro    t3.small     m5.large
instance_count      1           2            3 (auto-scale)
multi_az            false       true         true
backup_retention    1 day       7 days       30 days
deletion_protection false       false        true
monitoring          basic       detailed     detailed
ssl_certificate     self-signed ACM          ACM (renewed)
logging             stdout      CloudWatch   CloudWatch+SIEM
alerting            none        Slack        PagerDuty
cost/month          ~$50        ~$200        ~$2000+
# Implementation: a configuration map per environment
locals {
  env_config = {
    dev = {
      instance_type        = "t3.micro"
      instance_count       = 1
      multi_az             = false
      backup_retention     = 1
      deletion_protection  = false
      monitoring_level     = "basic"
    }
    staging = {
      instance_type        = "t3.small"
      instance_count       = 2
      multi_az             = true
      backup_retention     = 7
      deletion_protection  = false
      monitoring_level     = "detailed"
    }
    production = {
      instance_type        = "m5.large"
      instance_count       = 3
      multi_az             = true
      backup_retention     = 30
      deletion_protection  = true
      monitoring_level     = "detailed"
    }
  }
  cfg = local.env_config[var.environment]
}

Summary #

  • Environments aren’t just names — dev, staging, and production have different purposes that require structured configuration differences.
  • Same structure, different values — the basic principle of good environments. The same Terraform configuration is used across all environments, only the variable values differ.
  • Production drift is the main enemy — when staging doesn’t mirror production, bugs slip through undetected and disaster recovery becomes unreliable.
  • Five challenges: state isolation, configuration isolation, separate credentials, configuration consistency, and promotion workflows.
  • Two main approaches: workspaces (one configuration, separate state) and directory-based (a separate directory per environment).
  • Ideally use separate AWS accounts per environment for better cost isolation, security, and blast radius.

← Previous: Registry   Next: Environment Types →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact