Environment Types #
Not every team needs the same three environments. A small startup might get by with dev and production. A mature team with hundreds of engineers might need more — including environments that live only while a PR is open and disappear after the merge. Understanding the types of environments that exist, their purposes, and when each is needed helps you decide the right environment architecture for your team’s context.
flowchart TD
A["Environment\nTypes"] --> B["Development"]
A --> C["Staging"]
A --> D["Production"]
A --> E["Sandbox"]
B --> F["Experimentation\nFast iteration"]
C --> G["Validation\nProd mirror"]
D --> H["Stability\nMonitoring"]
E --> I["Isolation\nTesting"]
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
style E fill:#8b5cf6,stroke:#6d28d9,color:#fffStandard Environments #
DEV (Development)
Purpose : Fast iteration and experimentation
Managed by: Shared by all developers or per-developer
Stability : Low — downtime allowed, data can be deleted
Cost : Minimal — small instances, off outside working hours
Lifecycle : Persistent, but resources can be destroyed anytime
STAGING / UAT (User Acceptance Testing)
Purpose : Final validation before production
Managed by: QA team + senior developers
Stability : High — must be as close to production as possible
Cost : Medium — representative but not full scale
Lifecycle : Persistent
PRODUCTION
Purpose : Serving real users
Managed by: Ops / SRE team with very restricted access
Stability : Very high — SLA, on-call, strict monitoring
Cost : Full scale according to traffic needs
Lifecycle : Persistent, with change freezes during critical periods
Ephemeral Environments #
An ephemeral environment is one created for a specific purpose and removed once that purpose is done. This isn’t a new concept, but Terraform makes it far more practical to implement.
WHEN EPHEMERAL ENVIRONMENTS ARE USED:
Per-PR Environment
Created : When a PR is opened
Contents: Configuration from that PR's branch
Purpose : Developers can test changes in a real environment
Removed : When the PR is merged or closed
Example workflow:
PR opened → CI triggers → terraform apply (create the environment)
Testing done → PR merged → CI triggers → terraform destroy
Per-Feature Environment
Created : When a sprint feature starts
Removed : When the feature is done and ready to move to staging
Useful : For large features needing cross-team testing
Performance Testing Environment
Created : Before a load test runs
Contents: Scaled-down production with representative data
Removed : After the load test finishes
Useful : Expensive if persistent, no need to run continuously
# Configuration for an ephemeral environment
# Usually called from CI/CD with the environment name from the branch name
variable "environment" {
type = string
# For ephemeral: "pr-123", "feature-payment", "loadtest-q4"
}
locals {
# Resource naming reflecting the ephemeral nature
resource_prefix = var.environment
# All resources are prefixed for easy identification and cleanup
}
resource "aws_instance" "app" {
instance_type = "t3.micro" # Always minimal for ephemeral
tags = {
Name = "${local.resource_prefix}-app"
Environment = var.environment
Ephemeral = "true" # Special tag for auditing and automatic cleanup
TTL = "72h" # Time-to-live — readable by cleanup scripts
}
}
flowchart TD
A["Dev"] -->|"Promote"| B["Staging"]
B -->|"Promote"| C["Production"]
A -.->|"Confidence\nValidation"| B
B -.->|"Confidence\nValidation"| C
style A fill:#10b981,stroke:#059669,color:#fff
style B fill:#f59e0b,stroke:#d97706,color:#fff
style C fill:#ef4444,stroke:#dc2626,color:#fffAWS Multi-Account Strategy #
For teams serious about security and isolation, every environment should live in a separate AWS account — not just separate state.
SINGLE ACCOUNT (not recommended for production):
Account: 123456789
├── Dev Resources
├── Staging Resources
└── Production Resources
Problems:
- One credential can access all environments
- Resource quotas (EC2, VPC, etc.) shared across all environments
- Billing isn't separated per environment
- Large blast radius — a mistake in dev can affect production
MULTI-ACCOUNT (recommended):
Management Account (root)
├── Dev Account (111111111)
│ └── All dev resources
├── Staging Account (222222222)
│ └── All staging resources
└── Production Account (333333333)
└── All production resources
Benefits:
- Full isolation — dev credentials can't touch production
- Separate billing per environment
- Small blast radius — mistakes in dev are isolated
- Separate service quotas per account
- Easier compliance — separate audit trails
# Terraform configuration for multi-account
# Each environment has a provider with a different assume_role
provider "aws" {
region = "ap-southeast-1"
assume_role {
role_arn = "arn:aws:iam::${var.account_id}:role/TerraformDeployRole"
# This role only exists in the specific account
# Terraform runs with management account credentials
# then assumes the role into the target account
}
}
# tfvars per environment:
# dev.tfvars: account_id = "111111111"
# staging.tfvars: account_id = "222222222"
# production.tfvars: account_id = "333333333"
Determining the Right Number of Environments #
QUESTIONS TO DETERMINE ENVIRONMENTS:
1. How often do you deploy to production?
- Several times a day → you need a strong staging
- Once a week → minimal staging is enough
2. How big is the team and how often do conflicts happen?
- Small team (< 5 devs) → shared dev is enough
- Large team (> 10 devs) → consider per-developer environments
or ephemeral per-PR
3. Are there compliance or audit requirements?
- Yes (PCI-DSS, HIPAA, SOC2) → multi-account is mandatory
- No → single account with IAM boundaries is enough
4. Have there been "bug slipped to production" incidents?
- Often → add a testing environment layer
- Rarely → the current environments may already be enough
RULE OF THUMB:
Early-stage startup → Dev + Production (2 environments)
Growing team → Dev + Staging + Production (3 environments)
Mature team → Dev + Staging + Production + Ephemeral (4+)
Enterprise → Multi-account, per-region, per-compliance scope
Environment Naming Conventions #
Consistent naming helps a lot when resources from various environments appear in a single view (billing console, monitoring dashboard).
# Common naming pattern
locals {
# Format: <project>-<environment>-<component>
# Example: myapp-production-api
# myapp-dev-database
# myapp-pr123-web
name_prefix = "${var.project}-${var.environment}"
common_tags = {
Project = var.project
Environment = var.environment
ManagedBy = "terraform"
# These tags make filtering easy in billing and monitoring
}
}
resource "aws_instance" "api" {
# ...
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-api"
Component = "api"
})
}
Environment Configuration Patterns #
# Pattern 1: Workspace-based (one codebase, different state)
terraform {
required_providers {
aws = { source = "hashicorp/aws" }
}
}
locals {
env_config = {
dev = {
instance_type = "t3.micro"
instance_count = 1
}
staging = {
instance_type = "t3.small"
instance_count = 2
}
production = {
instance_type = "t3.medium"
instance_count = 3
}
}
config = local.env_config[terraform.workspace]
}
resource "aws_instance" "web" {
count = local.config.instance_count
instance_type = local.config.instance_type
}
Environment Promotion Strategy #
# Deployment pipeline per environment
# DEV: Auto-deploy on every push to the develop branch
# Staging: Auto-deploy on every merge to main
# Production: Manual approval after staging UAT passes
# GitHub Actions example
on:
push:
branches: [develop] # → deploy to dev
pull_request:
branches: [main] # → plan for review
Environment Variables Best Practices #
# BEST PRACTICE for environment variables:
# 1. Prefix with TF_VAR_
export TF_VAR_environment="production"
export TF_VAR_instance_type="m5.large"
# 2. For secrets, use CI/CD secret storage
# GitHub Actions: secrets.TF_VAR_db_password
# GitLab CI: $TF_VAR_db_password (masked variable)
# 3. DON'T set them in .bashrc/.zshrc
# Because they can execute in the wrong environment
# 4. Use a .env file with .gitignore
echo "TF_VAR_db_password=secret" >> .env
echo ".env" >> .gitignore
# 5. Or use direnv for auto-loading
# .envrc:
# export TF_VAR_environment=dev
Environment Isolation Patterns #
ISOLATION LEVELS:
1. Workspace (least isolated)
- Same state backend
- Same code branch
- Easy to accidentally apply the wrong env
2. Directory (medium isolation)
- Separate directories per env
- Shared modules
- Clear separation
3. Repository (most isolated)
- Separate repos per env
- Independent lifecycles
- More overhead
RECOMMENDATION: Directory-based isolation
for most use cases
# Directory-based implementation
# environments/dev/main.tf
terraform {
backend "s3" {
bucket = "terraform-state-dev"
key = "infrastructure/terraform.tfstate"
region = "ap-southeast-1"
}
}
module "infrastructure" {
source = "../../modules/infrastructure"
environment = "dev"
instance_type = "t3.micro"
}
Environment Promotion Strategy #
ENVIRONMENT PROMOTION:
Code → Development → Staging → Production
Development:
- Auto-apply on merge to main
- Latest code, frequent changes
- No approval needed
Staging:
- Auto-apply on tag (v1.x.x)
- Mirrors the production config
- Integration testing
Production:
- Manual apply only
- Requires approval
- Change window restrictions
# GitHub Actions environment promotion
name: Deploy
on:
push:
tags: ['v*']
jobs:
deploy-staging:
environment: staging
runs-on: ubuntu-latest
steps:
- run: terraform apply -auto-approve
deploy-production:
needs: deploy-staging
environment: production
runs-on: ubuntu-latest
steps:
- run: terraform apply
Summary #
- Three standard environments: dev (fast iteration), staging (final validation), production (serving users) — each has different purposes and characteristics.
- Ephemeral environments are created for a specific purpose (per-PR, per-feature, load tests) then removed — Terraform makes this practical with
terraform applyandterraform destroy.- AWS multi-account is a best practice for serious teams — full isolation, separate billing, small blast radius, easier compliance.
- The number of environments isn’t “the more the better” — determine it based on deployment frequency, team size, compliance needs, and incident history.
- Consistent naming conventions (
<project>-<env>-<component>) make navigation easier in the billing console, monitoring, and audits.- An
Environmenttag on all resources is a minimal practice that should always exist — it makes cost allocation and filtering easier.