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:#fff

Standard 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:#fff

AWS 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 apply and terraform 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 Environment tag on all resources is a minimal practice that should always exist — it makes cost allocation and filtering easier.

← Previous: What is an Environment?   Next: Workspace →

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