# Containerize Schema Audit (Frontend + Backend) for AWS ECS

## Context

Two containers needed:

1. **Frontend** (Next.js 16) — Long-running ECS service serving the dashboard. Currently loads `audit-data.json` via a symlink to the backend dir; needs to fetch from S3 instead. Has an API route that shells out to Apollo Rover CLI at runtime.

2. **Backend** (Python 3.13, uv) — ECS scheduled task that runs the audit script (`python -m src.main`), produces `audit-data.json`, and uploads it to S3. Also uses Rover CLI (via subprocess) and needs `APOLLO_KEY`. Runs on a schedule (e.g., daily via EventBridge), then exits.

### Org Patterns (from `graphql-rover-utility`)

- **Base images**: Org ECR parent images at `086679231553.dkr.ecr.us-east-1.amazonaws.com/docker-parent-images:<tag>`
- **Rover install**: Pinned version via `curl -sSL https://rover.apollo.dev/nix/${ROVER_VERSION} | sh`, add `~/.rover/bin` to PATH
- **CI/CD**: Jenkins with `dockerToEcr` shared library step, pushes to ECR account `086679231553`
- **ECR naming**: image name matches repo name

---

## Part 1: Frontend Container

### Files to Create/Modify

| # | File | Action | Purpose |
|---|------|--------|---------|
| 1 | `frontend/next.config.ts` | Modify | Add `output: 'standalone'` |
| 2 | `frontend/package.json` | Modify | Add `@aws-sdk/client-s3` |
| 3 | `frontend/src/app/api/audit-data/route.ts` | **Create** | API route: fetch `audit-data.json` from S3 |
| 4 | `frontend/src/context/AppContext.tsx` | Modify (line 79) | Change fetch URL to `/api/audit-data` |
| 5 | `frontend/public/data/` | **Delete** | Remove symlink (data now from S3) |
| 6 | `frontend/.dockerignore` | **Create** | Exclude `node_modules`, `.next`, `.env*`, etc. |
| 7 | `frontend/Dockerfile` | **Create** | Multi-stage build with Rover CLI |

### Dockerfile Structure (3 stages)

```dockerfile
# Base: org ECR parent image (verify node22 tag exists, else use node20)
FROM 086679231553.dkr.ecr.us-east-1.amazonaws.com/docker-parent-images:node22 AS deps
# pnpm install --frozen-lockfile

FROM <same base> AS builder
# Copy deps, copy source, pnpm build
# Install Rover (pinned version, same pattern as graphql-rover-utility)

FROM <same base> AS runner
# Copy .next/standalone + static + public
# Install Rover (needed at runtime for /api/schema-check)
# ENV ROVER_VERSION='v0.22.0'
# RUN curl -sSL https://rover.apollo.dev/nix/${ROVER_VERSION} | sh
# ENV PATH=~/.rover/bin:${PATH}
# Non-root user, EXPOSE 3000, CMD ["node", "server.js"]
```

### Key Details — Frontend

- `output: 'standalone'` produces self-contained `server.js` (minimal image size)
- Rover CLI needed at runtime for `/api/schema-check` route (`rover subgraph fetch/check`)
- New `/api/audit-data` route: `S3Client` with default credential chain, env vars `AUDIT_DATA_S3_BUCKET` + `AUDIT_DATA_S3_KEY`, 5-min cache header
- Pinned Rover version `v0.22.0` (matching `graphql-rover-utility`)

---

## Part 2: Backend Container

### Files to Create/Modify

| # | File | Action | Purpose |
|---|------|--------|---------|
| 8 | `backend/src/main.py` | Modify | Add `--s3-upload` flag: after writing local JSON, upload to S3 |
| 9 | `backend/pyproject.toml` | Modify | Add `boto3` to dependencies |
| 10 | `backend/.dockerignore` | **Create** | Exclude `.venv`, `*.json` data files, etc. |
| 11 | `backend/Dockerfile` | **Create** | Python 3.13 + uv + Rover CLI |

### Dockerfile Structure (2 stages)

```dockerfile
# Builder: python:3.13-slim (or org python parent if available)
FROM python:3.13-slim AS builder
# Install uv, copy pyproject.toml + uv.lock, uv sync --frozen

FROM python:3.13-slim AS runner
# Copy venv from builder
# Install Rover CLI (pinned version, same pattern)
# ENV ROVER_VERSION='v0.22.0'
# RUN curl -sSL https://rover.apollo.dev/nix/${ROVER_VERSION} | sh
# ENV PATH=~/.rover/bin:${PATH}
# Non-root user
# ENTRYPOINT ["python", "-m", "src.main", "--s3-upload"]
```

### Key Details — Backend

- Uses `python:3.13-slim` (Debian-based, glibc guaranteed for Rover)
- `uv sync --frozen` for reproducible installs
- Add `boto3` to dependencies for S3 upload
- New `--s3-upload` CLI flag: after `write_report()`, upload `audit-data.json` to S3
- Env vars: `APOLLO_KEY`, `AUDIT_DATA_S3_BUCKET`, `AUDIT_DATA_S3_KEY`
- **Run-once task**: runs audit, uploads to S3, exits with code 0

### S3 Upload Addition to `main.py`

New arg: `--s3-upload` (store_true). After `write_report()` (~line 235):
```python
if args.s3_upload:
    import boto3
    s3 = boto3.client('s3')
    bucket = os.environ['AUDIT_DATA_S3_BUCKET']
    key = os.environ.get('AUDIT_DATA_S3_KEY', 'audit-data.json')
    s3.upload_file(output_path, bucket, key, ExtraArgs={'ContentType': 'application/json'})
    print(f'Uploaded to s3://{bucket}/{key}')
```

---

## Part 3: Shared Infrastructure

### Files to Create

| # | File | Action | Purpose |
|---|------|--------|---------|
| 12 | `docker-compose.yml` (root) | **Create** | Orchestrate both services locally |
| 13 | `infra/ecs-task-frontend.json` | **Create** | Fargate service (long-running, port 3000) |
| 14 | `infra/ecs-task-backend.json` | **Create** | Fargate scheduled task (run-once) |

### docker-compose.yml (root level)

```yaml
services:
  frontend:
    build: ./frontend
    ports: ["3000:3000"]
    environment:
      - APOLLO_KEY
      - AUDIT_DATA_S3_BUCKET
      - AUDIT_DATA_S3_KEY
    volumes:
      - ${HOME}/.aws:/home/nextjs/.aws:ro

  backend:
    build: ./backend
    environment:
      - APOLLO_KEY
      - AUDIT_DATA_S3_BUCKET
      - AUDIT_DATA_S3_KEY
    volumes:
      - ${HOME}/.aws:/root/.aws:ro
    profiles: ["audit"]  # Only runs on demand: docker compose run backend
```

### ECS Architecture

- **Frontend**: Fargate service, 0.5 vCPU / 1GB, always-on, `APOLLO_KEY` from Secrets Manager, S3 read via IAM task role
- **Backend**: Fargate scheduled task (EventBridge rule, e.g., daily), 1 vCPU / 2GB (audit is CPU/network-heavy), `APOLLO_KEY` from Secrets Manager, S3 write via IAM task role, exits after completion
- **ECR**: Push to `086679231553.dkr.ecr.us-east-1.amazonaws.com/schema-audit-frontend` and `schema-audit-backend`

### IAM Permissions

| Role | Permissions |
|------|-------------|
| Frontend task role | `s3:GetObject` on audit-data bucket |
| Backend task role | `s3:PutObject` on audit-data bucket |
| Execution role (shared) | ECR pull + Secrets Manager read + CloudWatch Logs |

### Jenkins CI/CD (follows graphql-rover-utility pattern)

Both containers get a `Jenkinsfile` using the shared library:
```groovy
stage('Create a Release') {
    steps {
        dockerToEcr awsRegions: ['us-east-1'],
            ecrAccountId: '086679231553',
            imageName: 'schema-audit-frontend', // or schema-audit-backend
            imageTag: env.GIT_COMMIT,
            pushLatest: (env.BRANCH_NAME == 'master')
    }
}
```

---

## Verification

1. **Frontend standalone build**: `cd frontend && pnpm build` produces `.next/standalone/server.js`
2. **Frontend Docker**: `docker build -t schema-audit-frontend ./frontend` succeeds
3. **Backend Docker**: `docker build -t schema-audit-backend ./backend` succeeds
4. **Compose up frontend**: `docker compose up frontend` → `http://localhost:3000` loads
5. **Compose run backend**: `docker compose run backend` → runs audit, uploads to S3
6. **Frontend reads S3 data**: `/api/audit-data` returns JSON (needs AWS creds + env vars)
7. **Schema check works**: `/api/schema-check` POST works (Rover CLI in frontend container)
8. **Backend Rover works**: `rover subgraph list` works inside backend container
