141 lines
5.7 KiB
Markdown
141 lines
5.7 KiB
Markdown
# 20 — Deployment
|
|
|
|
## Current state
|
|
|
|
A single EC2 host running Docker Compose, with Caddy terminating TLS for
|
|
`realvuxbaro.me`. Images are built **on the host** at deploy time; there is no
|
|
container registry and no orchestrator.
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
subgraph internet["Internet"]
|
|
USER[Clinician]
|
|
OPS[Operator]
|
|
LE[Let's Encrypt]
|
|
end
|
|
|
|
subgraph host["EC2 instance — docker compose project 'docker'"]
|
|
CADDY["caddy:2-alpine<br/>:80 :443<br/>volumes: Caddyfile, caddy-data, caddy-config"]
|
|
WEB["web<br/>build apps/web/Dockerfile<br/>AI_SERVICE_URL=http://ai-service:8000"]
|
|
AI["ai-service<br/>build apps/ai-service/Dockerfile<br/>env_file .env.prod (not in repo)"]
|
|
PG[("postgres:16-alpine<br/>vol postgres-data")]
|
|
QD[("qdrant/qdrant:latest<br/>vol qdrant-data")]
|
|
PROM["prometheus<br/>127.0.0.1:9090"]
|
|
TEMPO["tempo"]
|
|
OTEL["otel-collector"]
|
|
GRAF["grafana<br/>127.0.0.1:3002"]
|
|
end
|
|
|
|
BR["AWS Bedrock<br/>via instance IAM role"]
|
|
|
|
USER -->|https| CADDY
|
|
OPS -->|https .../grafana/| CADDY
|
|
LE <-->|ACME| CADDY
|
|
CADDY --> WEB --> AI
|
|
AI --> PG
|
|
AI --> QD
|
|
AI --> BR
|
|
AI -.OTLP.-> OTEL --> TEMPO
|
|
PROM -.scrape.-> AI
|
|
GRAF --> PROM
|
|
GRAF --> TEMPO
|
|
CADDY --> GRAF
|
|
```
|
|
|
|
## Files that define it
|
|
|
|
| File | Role |
|
|
|---|---|
|
|
| `infra/docker/docker-compose.prod.yml` | Base topology: postgres, qdrant, ai-service, web, caddy |
|
|
| `infra/docker/docker-compose.observability.yml` | Overlay: turns on OTel in `ai-service`, adds prometheus/tempo/otel-collector/grafana |
|
|
| `infra/docker/Caddyfile` | `realvuxbaro.me` → `web:3000`, `/grafana/*` → `grafana:3000`, `/grafana` → 308 redirect |
|
|
| `apps/ai-service/Dockerfile` | `python:3.12-slim`, deps pinned inline, `uvicorn main:app --host 0.0.0.0 --port 8000` |
|
|
| `apps/web/Dockerfile` | 3-stage node:20-slim, `next start -p 3000 -H 0.0.0.0` |
|
|
| `.github/workflows/deploy.yml` | The deploy itself, over SSH |
|
|
|
|
## Port and exposure map
|
|
|
|
| Service | Host port | Reachable from |
|
|
|---|---|---|
|
|
| caddy | 80, 443 | Internet |
|
|
| web | none | Compose network + Caddy |
|
|
| ai-service | **none** | Compose network only |
|
|
| postgres | none | Compose network only |
|
|
| qdrant | none | Compose network only |
|
|
| prometheus | `127.0.0.1:9090` | The host only (SSH tunnel) |
|
|
| grafana | `127.0.0.1:3002` | The host, plus the internet via Caddy `/grafana/` |
|
|
| tempo, otel-collector | none | Compose network only |
|
|
|
|
## Deploy sequence
|
|
|
|
Triggered by a push to `master` or a manual `workflow_dispatch`.
|
|
`appleboy/ssh-action` runs a `set -e` script on the host as `ubuntu`:
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant GH as GitHub Actions
|
|
participant EC2 as EC2 host
|
|
participant DC as docker compose
|
|
participant SVC as running stack
|
|
|
|
GH->>EC2: ssh (EC2_HOST, EC2_SSH_KEY), env GRAFANA_ADMIN_PASSWORD
|
|
EC2->>EC2: test -n "$GRAFANA_ADMIN_PASSWORD" (fail fast)
|
|
EC2->>EC2: cd ~/app && git fetch origin master && git reset --hard origin/master
|
|
EC2->>DC: compose -f prod -f observability up -d --build<br/>ai-service web prometheus tempo otel-collector grafana caddy
|
|
DC-->>SVC: rebuilt + restarted
|
|
EC2->>SVC: caddy validate && caddy reload
|
|
EC2->>SVC: docker exec ai-service python -m migrate
|
|
EC2->>EC2: sleep 10
|
|
EC2->>SVC: GET /health, GET /ready, GET web:3000
|
|
EC2->>SVC: POST /v1/rag/query (gout) — assert answerable + chi_dinh
|
|
EC2->>SVC: prometheus /-/ready, tempo /ready (retry 12x5s), grafana /api/health
|
|
EC2->>SVC: assert both Grafana datasources + the dashboard exist
|
|
EC2->>SVC: GET https://realvuxbaro.me/grafana/login
|
|
EC2->>SVC: POST /v1/rag/query with X-Correlation-ID; assert X-Trace-ID matches ^[0-9a-f]{32}$
|
|
EC2->>EC2: sleep 20
|
|
EC2->>SVC: assert duocthu_requests_total in Prometheus
|
|
EC2->>SVC: assert the exact trace id retrievable from Tempo (retry 12x5s)
|
|
```
|
|
|
|
Note what the `up -d` line does **not** include: `postgres` and `qdrant`. They
|
|
are left running from a previous deploy (both carry `restart: unless-stopped`),
|
|
so the stateful services are never restarted by a code deploy. That is
|
|
deliberate-looking and safe for uptime, but it also means a change to the
|
|
postgres/qdrant service definitions in the compose file will not take effect
|
|
until someone restarts them by hand.
|
|
|
|
## Migrations
|
|
|
|
`docker exec docker-ai-service-1 python -m migrate` runs after the containers
|
|
are up. `migrate.py` applies every `migrations/*.sql` in sorted order, each
|
|
idempotent. There is no version table, no ordering guard beyond the filename,
|
|
and no rollback.
|
|
|
|
## Rollback
|
|
|
|
There is no rollback command. The recovery path is `git revert` (or reset) on
|
|
`master` followed by another deploy, because the deploy script does
|
|
`git reset --hard origin/master` and rebuilds. Since images are built on the
|
|
host and not tagged, there is **no previously-built image to roll back to**.
|
|
|
|
## What the repository does not contain
|
|
|
|
- Any container registry configuration (ECR, GHCR, Docker Hub).
|
|
- Any image tagging or versioning scheme — `web` and `ai-service` are rebuilt
|
|
from `latest` source each time.
|
|
- Terraform for the EC2 host: `infra/terraform/` holds only empty module and
|
|
environment directories plus a README.
|
|
- Blue/green, canary, or any staged rollout — the deploy is in-place.
|
|
- A database backup or restore procedure.
|
|
- A staging environment. `infra/helm/values-staging.yaml` and
|
|
`infra/argocd/applications/staging/` exist but were never applied.
|
|
|
|
## Target deployment (not applied)
|
|
|
|
The Helm chart and ArgoCD manifests describe a Kubernetes deployment. See
|
|
[21-kubernetes-and-argocd.md](21-kubernetes-and-argocd.md). They are current
|
|
intent, not current state — ADR 0002's status line says exactly that:
|
|
|
|
> **Accepted — still the target, not yet implemented.** Not superseded by the
|
|
> current production setup.
|