Modern QA2026Terraform Workspaces for Ephemeral Environments — tiles
Log inJoin
65 / 75 · 08 Infrastructure as Code Testing · Ephemeral Environments← prev⊞ allnext →☰ Read as one page

12.3Terraform Workspaces for Ephemeral Environments

Terraform workspaces provide lightweight isolation. Each workspace maintains its own state file, so resources created in one workspace do not affect another.

Workspace-Aware Configuration

# main.tf -- workspace-aware naming
locals {
  env_name = terraform.workspace == "default" ? "production" : terraform.workspace
  prefix   = "myapp-${local.env_name}"

  # Use smaller instances for PR environments
  is_ephemeral = terraform.workspace != "default"
}

resource "aws_s3_bucket" "data" {
  bucket = "${local.prefix}-data"
  # Each PR gets its own bucket: myapp-pr-142-data
}

resource "aws_db_instance" "main" {
  identifier     = "${local.prefix}-db"
  instance_class = local.is_ephemeral ? "db.t4g.micro" : "db.r6g.xlarge"
  allocated_storage = local.is_ephemeral ? 20 : 500
  # PR environments use minimal instance sizes to control cost

  # Skip final snapshot for ephemeral environments
  skip_final_snapshot = local.is_ephemeral

  # Use a smaller backup retention for ephemeral
  backup_retention_period = local.is_ephemeral ? 0 : 7
}

resource "aws_ecs_service" "app" {
  name            = "${local.prefix}-app"
  desired_count   = local.is_ephemeral ? 1 : 3
  # PR environments need only one replica
}

# DNS entry for the preview URL
resource "aws_route53_record" "preview" {
  count   = local.is_ephemeral ? 1 : 0
  zone_id = data.aws_route53_zone.preview.zone_id
  name    = "${local.env_name}.preview.example.com"
  type    = "CNAME"
  records = [aws_lb.app.dns_name]
  ttl     = 60
}

output "preview_url" {
  value = local.is_ephemeral ? "https://${local.env_name}.preview.example.com" : ""
}

CI Pipeline for Ephemeral Environments

#!/bin/bash
# scripts/ephemeral-env.sh
set -euo pipefail

ACTION="$1"  # "create" or "destroy"
PR_NUM=$(echo "$GITHUB_REF" | grep -oP '\d+')
WORKSPACE="pr-${PR_NUM}"

case "$ACTION" in
  create)
    cd terraform/
    terraform workspace new "$WORKSPACE" 2>/dev/null || \
      terraform workspace select "$WORKSPACE"

    terraform apply -auto-approve \
      -var="pr_number=${PR_NUM}" \
      -var="git_sha=${GITHUB_SHA:0:8}"

    PREVIEW_URL=$(terraform output -raw preview_url)
    echo "Preview environment ready: $PREVIEW_URL"

    # Wait for health check
    for i in $(seq 1 30); do
      if curl -sf "$PREVIEW_URL/healthz" > /dev/null; then
        echo "Environment is healthy"
        break
      fi
      echo "Waiting for environment to be ready... ($i/30)"
      sleep 10
    done

    # Run tests
    cd ..
    npx playwright test --base-url "$PREVIEW_URL"
    TEST_RESULT=$?

    # Post results to PR
    if [ $TEST_RESULT -eq 0 ]; then
      gh pr comment "$PR_NUM" --body \
        "Preview: $PREVIEW_URL | Tests: PASSED"
    else
      gh pr comment "$PR_NUM" --body \
        "Preview: $PREVIEW_URL | Tests: FAILED (see CI logs)"
    fi
    ;;

  destroy)
    cd terraform/
    terraform workspace select "$WORKSPACE"
    terraform destroy -auto-approve
    terraform workspace select default
    terraform workspace delete "$WORKSPACE"
    echo "Environment pr-${PR_NUM} destroyed"
    ;;
esac