# Deployment Environment Isolation Best Practices

## Environment Hierarchy

### Standard Environment Structure

```
Production        ← Live users, real data, maximum stability
    ↑
Staging           ← Pre-production testing, production-like
    ↑
Development       ← Shared development environment
    ↑
Preview/PR        ← Per-PR ephemeral environments
    ↑
Local             ← Developer machines
```

### Environment Purposes

| Environment | Purpose | Data | Access | Lifetime |
|-------------|---------|------|--------|----------|
| **Production** | Live application | Real user data | Restricted | Permanent |
| **Staging** | Pre-release testing | Synthetic/sanitized | Team-wide | Permanent |
| **Sandbox** | Experiments, spikes | Synthetic | Developers | Semi-permanent |
| **Development** | Shared dev testing | Synthetic | Developers | Permanent |
| **Preview** | PR-specific testing | Synthetic/seeded | PR author + reviewers | Ephemeral |
| **Local** | Individual development | Local/mocked | Individual | Session |

## Environment Configuration

### Pulumi Stack Structure

```
infrastructure/
├── Pulumi.yaml              # Project definition
├── Pulumi.dev.yaml          # Development stack
├── Pulumi.staging.yaml      # Staging stack
├── Pulumi.production.yaml   # Production stack
└── __main__.py
```

### Stack Configuration Example

```yaml
# Pulumi.production.yaml
config:
  aws:region: us-east-1
  myapp:environment: production
  myapp:instanceType: t3.medium
  myapp:minInstances: "2"
  myapp:maxInstances: "10"
  myapp:enableMultiAz: "true"
  myapp:backupRetention: "30"
  myapp:monthlyBudget: "500"
```

```yaml
# Pulumi.dev.yaml
config:
  aws:region: us-east-1
  myapp:environment: development
  myapp:instanceType: t3.micro
  myapp:minInstances: "1"
  myapp:maxInstances: "2"
  myapp:enableMultiAz: "false"
  myapp:backupRetention: "7"
  myapp:monthlyBudget: "50"
```

### Environment-Aware Infrastructure

```python
"""Environment-aware infrastructure configuration."""

import pulumi
from dataclasses import dataclass


@dataclass
class EnvironmentConfig:
    """Per-environment configuration."""

    name: str
    instance_type: str
    min_instances: int
    max_instances: int
    database_class: str
    multi_az: bool
    backup_days: int
    monthly_budget: int


# Environment presets
ENVIRONMENTS: dict[str, EnvironmentConfig] = {
    "dev": EnvironmentConfig(
        name="development",
        instance_type="t3.micro",
        min_instances=1,
        max_instances=2,
        database_class="db.t3.micro",
        multi_az=False,
        backup_days=7,
        monthly_budget=50,
    ),
    "staging": EnvironmentConfig(
        name="staging",
        instance_type="t3.small",
        min_instances=1,
        max_instances=3,
        database_class="db.t3.small",
        multi_az=False,
        backup_days=14,
        monthly_budget=150,
    ),
    "production": EnvironmentConfig(
        name="production",
        instance_type="t3.medium",
        min_instances=2,
        max_instances=10,
        database_class="db.t3.medium",
        multi_az=True,
        backup_days=30,
        monthly_budget=500,
    ),
}


def get_environment_config() -> EnvironmentConfig:
    """Get configuration for current Pulumi stack."""
    stack = pulumi.get_stack()
    if stack not in ENVIRONMENTS:
        raise ValueError(f"Unknown environment: {stack}. Expected one of: {list(ENVIRONMENTS.keys())}")
    return ENVIRONMENTS[stack]


# Usage
env = get_environment_config()
print(f"Deploying to {env.name} with {env.instance_type} instances")
```

## Platform-Specific Patterns

### Vercel Preview Deployments

Vercel automatically creates preview deployments for every PR:

```
main branch      → production.myapp.com
staging branch   → staging.myapp.com
PR #123          → myapp-git-feature-xyz-team.vercel.app (auto-generated)
```

**Configuration:**

```python
import pulumi_vercel as vercel

# Production deployment
project = vercel.Project(
    "myapp",
    name="myapp",
    framework="nextjs",
    git_repository=vercel.ProjectGitRepositoryArgs(
        type="github",
        repo="org/myapp",
        production_branch="main",
    ),
)

# Environment-specific variables
vercel.ProjectEnvironmentVariable(
    "api-url-prod",
    project_id=project.id,
    key="NEXT_PUBLIC_API_URL",
    value="https://api.myapp.com",
    target=["production"],
)

vercel.ProjectEnvironmentVariable(
    "api-url-preview",
    project_id=project.id,
    key="NEXT_PUBLIC_API_URL",
    value="https://api-staging.myapp.com",
    target=["preview"],
)
```

### Supabase Environment Isolation

**Option 1: Separate Projects (Recommended)**

```bash
# Create per-environment projects
supabase projects create myapp-dev --region us-east-1
supabase projects create myapp-staging --region us-east-1
supabase projects create myapp-production --region us-east-1
```

**Option 2: Branch-based (Supabase Branching)**

```bash
# Enable branching (Pro plan required)
supabase branches create staging
supabase branches create feature-xyz
```

### Cloudflare Environment Isolation

```javascript
// wrangler.toml
name = "myapp-worker"

[env.staging]
name = "myapp-worker-staging"
route = "staging.myapp.com/*"

[env.production]
name = "myapp-worker-production"
route = "myapp.com/*"
```

```bash
# Deploy to specific environment
wrangler deploy --env staging
wrangler deploy --env production
```

## Data Isolation

### Database Isolation Strategies

| Strategy | Isolation | Cost | Complexity |
|----------|-----------|------|------------|
| Separate databases | Complete | High | Low |
| Separate schemas | Good | Medium | Medium |
| Row-level security | Logical | Low | High |

**Recommended: Separate databases per environment**

```python
# Each environment has its own database
DATABASE_URLS = {
    "dev": "postgresql://...@dev-db.example.com/myapp",
    "staging": "postgresql://...@staging-db.example.com/myapp",
    "production": "postgresql://...@prod-db.example.com/myapp",
}
```

### Data Seeding for Non-Production

```python
"""Seed data for development/staging environments."""

import os


def should_seed_data() -> bool:
    """Check if data seeding should run."""
    env = os.getenv("ENVIRONMENT", "development")
    return env in ("development", "staging", "preview")


def seed_development_data():
    """Seed realistic test data.

    Only runs in non-production environments.
    """
    if not should_seed_data():
        raise RuntimeError("Cannot seed data in production!")

    # Create test users
    users = [
        {"email": "test@example.com", "name": "Test User"},
        {"email": "admin@example.com", "name": "Admin User"},
    ]
    for user in users:
        create_user(**user)

    # Create sample data
    create_sample_orders(count=100)
    create_sample_products(count=50)
```

## Preview Environment Patterns

### Ephemeral Preview Environments

```yaml
# .github/workflows/preview.yml
name: Preview Deployment

on:
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  deploy-preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Deploy Preview
        run: |
          # Vercel handles this automatically
          # Or use custom preview deployment
          pulumi stack init preview-${{ github.event.pull_request.number }}
          pulumi up --yes

      - name: Comment PR with URL
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: '🚀 Preview deployed: https://preview-${{ github.event.pull_request.number }}.myapp.com'
            })

  cleanup-preview:
    runs-on: ubuntu-latest
    if: github.event.action == 'closed'
    steps:
      - name: Destroy Preview
        run: |
          pulumi stack select preview-${{ github.event.pull_request.number }}
          pulumi destroy --yes
          pulumi stack rm --yes
```

### Preview Database Strategy

```python
"""Preview environment database strategy."""

import os


def get_database_for_preview() -> str:
    """Get database URL for preview environment.

    Options:
    1. Shared staging database (cheapest, less isolation)
    2. Fresh database per PR (most isolation, expensive)
    3. Database branching (Supabase/PlanetScale)
    """
    pr_number = os.getenv("PR_NUMBER")
    strategy = os.getenv("PREVIEW_DB_STRATEGY", "shared")

    if strategy == "shared":
        # Use staging database
        return os.getenv("STAGING_DATABASE_URL")

    elif strategy == "branch":
        # Use database branching (Supabase/PlanetScale)
        return f"postgresql://...@preview-{pr_number}.db.example.com/myapp"

    elif strategy == "isolated":
        # Spin up fresh database per PR (expensive!)
        return create_preview_database(pr_number)

    raise ValueError(f"Unknown preview DB strategy: {strategy}")
```

## Environment Variables per Environment

### Complete Example

```bash
# .env.development
ENVIRONMENT=development
DEBUG=true
LOG_LEVEL=DEBUG
DATABASE_URL=postgresql://localhost/myapp_dev
API_URL=http://localhost:3001
FEATURE_NEW_CHECKOUT=true

# .env.staging
ENVIRONMENT=staging
DEBUG=false
LOG_LEVEL=INFO
DATABASE_URL=postgresql://staging-db.example.com/myapp
API_URL=https://api-staging.myapp.com
FEATURE_NEW_CHECKOUT=true

# .env.production
ENVIRONMENT=production
DEBUG=false
LOG_LEVEL=WARNING
DATABASE_URL=postgresql://prod-db.example.com/myapp
API_URL=https://api.myapp.com
FEATURE_NEW_CHECKOUT=false  # Not ready for prod yet
```

## Cost Considerations

### Environment Cost Scaling

| Environment | % of Prod Cost | Justification |
|-------------|---------------|---------------|
| Production | 100% | Full resources required |
| Staging | 30-50% | Reduced scale, same architecture |
| Development | 10-20% | Minimal resources |
| Preview | 5-10% per instance | Ephemeral, auto-cleanup |
| Local | $0 | Developer machine |

### Cost Control by Environment

```python
"""Environment-specific cost controls."""

import pulumi

stack = pulumi.get_stack()

# Auto-shutdown for non-production
if stack in ("dev", "staging"):
    # Scale to zero overnight (cron-based)
    enable_night_shutdown = True
    shutdown_cron = "0 20 * * *"  # 8 PM
    startup_cron = "0 8 * * 1-5"  # 8 AM weekdays

# Resource limits by environment
RESOURCE_LIMITS = {
    "dev": {"max_instances": 2, "max_db_size_gb": 10},
    "staging": {"max_instances": 5, "max_db_size_gb": 50},
    "production": {"max_instances": 50, "max_db_size_gb": 500},
}
```

## Security Considerations

### Access Control by Environment

| Environment | Access Level |
|-------------|--------------|
| Production | Restricted (ops team, on-call) |
| Staging | Team-wide read, restricted write |
| Development | Team-wide full access |
| Preview | PR participants only |
| Local | Individual developer |

### Secret Management per Environment

```python
"""Environment-specific secret handling."""

import pulumi

stack = pulumi.get_stack()
config = pulumi.Config()

# Different secret sources per environment
if stack == "production":
    # Production: AWS Secrets Manager
    db_password = aws_secretsmanager_get("prod/db-password")
elif stack == "staging":
    # Staging: Pulumi secrets
    db_password = config.require_secret("dbPassword")
else:
    # Development: Local .env or default
    db_password = config.get_secret("dbPassword") or pulumi.Output.secret("dev-password")
```

## Related Documentation

- **Configuration as Code**: [configuration-as-code.md](./configuration-as-code.md) - CaC principles
- **Environment Variables**: [environment-variables.md](./environment-variables.md) - Env var patterns
- **Secrets Management**: [secrets-management.md](./secrets-management.md) - Secret handling
- **Vercel**: [vercel.md](./vercel.md) - Preview deployments
- **Cost Estimation**: [cost-estimation.md](./cost-estimation.md) - Environment costs

---

**Key Takeaway**: Environment isolation protects production data and stability while enabling rapid development. Use separate resources per environment, automate preview deployments for PRs, and implement strict access controls. Cost scales with isolation—balance isolation needs against budget.
