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:#fffWhy 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:#fffThe 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.