feat(stage): полная изоляция стенда от прод-ресурсов на prod-vm
Третья утечка stage↔prod: stage-compose разделял с прод-compose /opt/food-market-data/uploads И названные тома (food-market-postgres-data, food-market-api-data, food-market-api-logs). Теперь всё разведено. - Новый deploy/docker-compose.stage.yml — шаблон стендовского стека с food-market-stage-* волюмами, bind /opt/food-market-stage-data/uploads, container_name food-market-stage-*, OpenIddict Issuer по дефолту смотрит на test.admin.food-market.kz (а не на admin.food-market.kz). - На prod-vm сделаны: pg_dump стенда → down → переименованы stage-*-pre- isolation-20261005 → up с новыми волюмами → pg_restore. Прод-контейнеры, прод-том food-market-postgres-data и прод-папка /opt/food-market-data/ не тронуты (read-only inspect). - ~/deploy-stage.sh получил guard до билда: проверяет шаблон репо и удалённый stage-compose на запрещённые паттерны прод-путей/томов/имён/ issuer-URL. - ~/quality-watchdog.sh +step 10 (06:XX ежесуточно) — тот же набор паттернов + docker inspect реального стенда. - docs/stage-prod-isolation-progress.md: таблица до/после, что оставили, как разводили, follow-ups. Smoke на test.admin.food-market.kz: /health/ready Healthy, 31233 товара, invoice upload → parsing (OCR Hangfire job), GDPR org-export zip упал в /opt/food-market-stage-data/uploads/exports/. Прод-папка /opt/food-market- data/uploads/ до/после миграции: 13 файлов, 0 изменений. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
7745d018af
commit
7e7eb8ef1d
92
deploy/docker-compose.stage.yml
Normal file
92
deploy/docker-compose.stage.yml
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
# Stage-stack compose (test.admin.food-market.kz).
|
||||||
|
#
|
||||||
|
# Деплоится на prod-vm в ~/food-market-stage/deploy/docker-compose.yml.
|
||||||
|
# Запускается через ~/deploy-stage.sh на dev-vm (билдит из main → push :stage →
|
||||||
|
# ssh prod-vm `docker compose -p food-market-stage pull/up`).
|
||||||
|
#
|
||||||
|
# ВАЖНО: все stage-ресурсы НАЗВАНЫ отдельно от прод-стека (`food-market-stage-*`
|
||||||
|
# volume'ы, `/opt/food-market-stage-data` для uploads). Guard в ~/deploy-stage.sh
|
||||||
|
# и в quality-watchdog падает, если здесь появится прод-путь или прод-том — это
|
||||||
|
# третья утечка stage↔prod и мы её закрыли (см. docs/stage-prod-isolation-progress.md).
|
||||||
|
#
|
||||||
|
# Если нужно добавить сервис — делайте это тут и в прод-compose СИММЕТРИЧНО,
|
||||||
|
# но тома/бинды называйте RAW отдельно, без автодефолта от compose-project'а.
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: ${REGISTRY:-127.0.0.1:5001}/mirror/postgres:16-alpine
|
||||||
|
container_name: food-market-stage-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: food_market
|
||||||
|
POSTGRES_USER: food_market
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-food_market_dev}
|
||||||
|
PGDATA: /var/lib/postgresql/data/pgdata
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:5435:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U food_market -d food_market"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
api:
|
||||||
|
image: ${REGISTRY:-127.0.0.1:5001}/food-market-api:${API_TAG:-stage}
|
||||||
|
container_name: food-market-stage-api
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
ASPNETCORE_ENVIRONMENT: Production
|
||||||
|
ConnectionStrings__Default: Host=postgres;Port=5432;Database=food_market;Username=food_market;Password=${POSTGRES_PASSWORD:-food_market_dev}
|
||||||
|
# На стенде issuer должен указывать на тестовый домен. Если здесь
|
||||||
|
# внезапно появится admin.food-market.kz — guard в deploy-stage.sh упадёт.
|
||||||
|
OpenIddict__Issuer: ${OPENIDDICT_ISSUER:-https://test.admin.food-market.kz/}
|
||||||
|
OpenIddict__CertPassword: ${OPENIDDICT_CERT_PASSWORD:-}
|
||||||
|
# Rate-limits на стенде всегда выше (e2e и smoke генерят много запросов).
|
||||||
|
RateLimiting__SignupPerIpPerHour: ${RATE_SIGNUP_HOUR:-5000}
|
||||||
|
RateLimiting__SignupPerIpPerDay: ${RATE_SIGNUP_DAY:-50000}
|
||||||
|
RateLimiting__PerIpPerMinute: ${RATE_PER_IP_MIN:-5000}
|
||||||
|
RateLimiting__PerIpPerHour: ${RATE_PER_IP_HOUR:-20000}
|
||||||
|
# Quick-login доступен ТОЛЬКО на стенде; на prod — False.
|
||||||
|
QuickLogin__Enabled: ${QUICK_LOGIN_ENABLED:-false}
|
||||||
|
QuickLogin__MasterPassword: ${QUICK_LOGIN_MASTER_PASSWORD:-123456}
|
||||||
|
# OCR invoice: Claude CLI via ssh на dev-vm.
|
||||||
|
Invoices__ClaudeCli__RemoteHost: ${INVOICE_REMOTE_HOST:-nns@192.168.1.192}
|
||||||
|
Invoices__ClaudeCli__Executable: /home/nns/.local/bin/claude
|
||||||
|
Invoices__ClaudeCli__SshKey: /app/.ssh/invoice-ocr-key
|
||||||
|
Invoices__ClaudeCli__SshKnownHosts: /tmp/.fm-ssh/known_hosts
|
||||||
|
ports:
|
||||||
|
- "8085:8080"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/health/ready || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
volumes:
|
||||||
|
- api-data:/app/App_Data
|
||||||
|
- api-logs:/app/logs
|
||||||
|
- /opt/food-market-stage-data/uploads:/app/uploads
|
||||||
|
- ./secrets/invoice-ocr-key:/app/.ssh/invoice-ocr-key:ro
|
||||||
|
|
||||||
|
web:
|
||||||
|
image: ${REGISTRY:-127.0.0.1:5001}/food-market-web:${WEB_TAG:-stage}
|
||||||
|
container_name: food-market-stage-web
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
api:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "8086:80"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
name: food-market-stage-postgres-data
|
||||||
|
api-data:
|
||||||
|
name: food-market-stage-api-data
|
||||||
|
api-logs:
|
||||||
|
name: food-market-stage-api-logs
|
||||||
118
docs/stage-prod-isolation-progress.md
Normal file
118
docs/stage-prod-isolation-progress.md
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
# stage ↔ prod isolation — progress
|
||||||
|
|
||||||
|
**Задача:** stage-compose (`~/food-market-stage/deploy/docker-compose.yml` на prod-vm) делит с прод-compose
|
||||||
|
`/opt/food-market-data/uploads` и названные тома (`food-market-postgres-data`, `food-market-api-data`,
|
||||||
|
`food-market-api-logs`). Это третья утечка stage↔prod. Прод сейчас лежит — чиним до того, как его поднимут.
|
||||||
|
|
||||||
|
Prod-vm: `nns@192.168.1.190`. Dev-vm: 192.168.1.193 (где этот репо и registry :5001).
|
||||||
|
|
||||||
|
## Хронология утечек
|
||||||
|
|
||||||
|
1. **До 22.09.2026** — stage писал в прод-БД (fixed, разнесли на отдельные контейнеры).
|
||||||
|
2. **Инцидент compose-проекта «deploy»** — одинаковое `-p deploy` снесло прод-контейнеры при stage-up (fixed, stage теперь `-p food-market-stage`).
|
||||||
|
3. **Сегодня (05.10.2026)** — общий bind-mount `/opt/food-market-data/uploads` + общие named volumes.
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [x] **1. uploads: стенд → /opt/food-market-stage-data/uploads.** Перенесена пустая `invoices/2026-10/`
|
||||||
|
(единственное «после августа»); остальное в прод-папке (13 export-zip Июн'26 + старые products)
|
||||||
|
оставлено как есть. Stage-compose на prod-vm переписан, в репозитории добавлен
|
||||||
|
`deploy/docker-compose.stage.yml`.
|
||||||
|
- [x] **2. Полный аудит общих ресурсов.** Таблица ниже. Разведены: uploads bind, 3 named volume,
|
||||||
|
container_name, project name. Issuer стенда теперь дефолтится на `test.admin.food-market.kz`.
|
||||||
|
- [x] **3. Guard в `~/deploy-stage.sh` + quality-watchdog.** `deploy-stage.sh` падает до билда,
|
||||||
|
если local/remote stage-compose ссылается на прод-ресурсы (uploads/volumes/container names/
|
||||||
|
issuer). В `quality-watchdog.sh` добавлен step 10 (суточная каденция, 06:XX) — тот же набор
|
||||||
|
паттернов + `docker inspect` реального запущенного стенда.
|
||||||
|
- [x] **4. Smoke.** `/health/ready` Healthy, демо-admin логин, GET каталога 31 233 товара, invoice
|
||||||
|
upload → `parsing` → `failed` (OCR вернул не-JSON на 1×1 jpg — ожидаемо; исторические invoice
|
||||||
|
в restore'ом DB: `needs_review` с распарсенными `supplierName/invoiceNumber/totalSum`),
|
||||||
|
GDPR org-export → zip упал в `/opt/food-market-stage-data/uploads/exports/<orgId>/`.
|
||||||
|
Прод-папка `/opt/food-market-data/uploads/` до/после миграции: 13 файлов, без изменений.
|
||||||
|
|
||||||
|
## Audit — before (snapshot 05.10.2026 11:xx MSK)
|
||||||
|
|
||||||
|
| Ресурс | STAGE (project `food-market-stage`) | PROD (project `deploy`) | Общий? |
|
||||||
|
|-------------------------------------|-----------------------------------------------------------|-----------------------------------------------------|:------:|
|
||||||
|
| uploads bind | `/opt/food-market-data/uploads` → `/app/uploads` | `/opt/food-market-data/uploads` → `/app/uploads` | **ДА** |
|
||||||
|
| postgres volume | `food-market-postgres-data` (shared pgdata) | `food-market-postgres-data` | **ДА** |
|
||||||
|
| api App_Data (OpenIddict certs) | `food-market-api-data` | `food-market-api-data` | **ДА** |
|
||||||
|
| api logs | `food-market-api-logs` | `food-market-api-logs` | **ДА** |
|
||||||
|
| api OCR ssh-key (bind) | `./secrets/invoice-ocr-key:/app/.ssh/invoice-ocr-key:ro` | нет | нет |
|
||||||
|
| networks | `food-market-stage_default` (172.19/16) | host-net (prod web + api — порты наружу) | нет |
|
||||||
|
| api host port | `8085:8080` | `8080:8080` | нет |
|
||||||
|
| web host port | `8086:80` | `8081:80` | нет |
|
||||||
|
| postgres host port | `127.0.0.1:5435:5432` | `127.0.0.1:5434:5432` | нет |
|
||||||
|
| image tag | `food-market-api:stage`, `food-market-web:stage` | `food-market-api:latest`, `food-market-web:latest` | нет |
|
||||||
|
| OpenIddict Issuer | `https://admin.food-market.kz/` *(продовый домен!)* | не задано (default) | **ДА** |
|
||||||
|
| container name | дефолтные (`food-market-stage-api-1` и т.п.) | явные (`food-market-api`, `food-market-postgres`) | нет |
|
||||||
|
|
||||||
|
**Красные ячейки:** uploads, postgres volume, api-data, api-logs, OpenIddict Issuer.
|
||||||
|
|
||||||
|
(Issuer стенда равен прод-URL — токены, выпущенные стендом, валидируются как прод-токены. Это пятая общая точка, не входит в прямой scope задачи «тома/папки», но фиксируем в прогресс-доке, чтобы не потерять.)
|
||||||
|
|
||||||
|
## Audit — target state
|
||||||
|
|
||||||
|
| Ресурс | STAGE | PROD |
|
||||||
|
|-----------------------|------------------------------------------------|----------------------------------------|
|
||||||
|
| uploads bind | `/opt/food-market-stage-data/uploads` | `/opt/food-market-data/uploads` |
|
||||||
|
| postgres volume | `food-market-stage-postgres-data` | `food-market-postgres-data` |
|
||||||
|
| api App_Data | `food-market-stage-api-data` | `food-market-api-data` |
|
||||||
|
| api logs | `food-market-stage-api-logs` | `food-market-api-logs` |
|
||||||
|
| OpenIddict Issuer | `https://test.admin.food-market.kz/` | `https://admin.food-market.kz/` |
|
||||||
|
|
||||||
|
## Audit — after (snapshot 05.10.2026 12:xx MSK)
|
||||||
|
|
||||||
|
| Ресурс | STAGE | PROD | Общий? |
|
||||||
|
|-----------------------------|-------------------------------------------------------------|-------------------------------------------------------------|:------:|
|
||||||
|
| uploads bind | `/opt/food-market-stage-data/uploads` → `/app/uploads` | `/opt/food-market-data/uploads` → `/app/uploads` *(не трогали)* | нет |
|
||||||
|
| postgres volume | `food-market-stage-postgres-data` (fresh initdb + pg_restore)| `food-market-postgres-data` *(не трогали)* | нет |
|
||||||
|
| api App_Data | `food-market-stage-api-data` | `food-market-api-data` | нет |
|
||||||
|
| api logs | `food-market-stage-api-logs` | `food-market-api-logs` | нет |
|
||||||
|
| container_name | `food-market-stage-{postgres,api,web}` | `food-market-{postgres,api,web}` | нет |
|
||||||
|
| compose project | `food-market-stage` | `deploy` | нет |
|
||||||
|
| OpenIddict Issuer | `https://test.admin.food-market.kz/` (default + .env) | `https://admin.food-market.kz/` | нет |
|
||||||
|
| сеть | `food-market-stage_default` (bridge, 172.19/16) | host-net (прод на bare ports) | нет |
|
||||||
|
| порты | 8085 api / 8086 web / 5435 pg | 8080 api / 8081 web / 5434 pg | нет |
|
||||||
|
| image tag default | `:stage` | `:latest` | нет |
|
||||||
|
|
||||||
|
## Как разводили
|
||||||
|
|
||||||
|
1. `pg_dump -F c` стенда (16.6 MB из общего pgdata).
|
||||||
|
2. `docker compose -p food-market-stage down` (прод-postgres автореcтартовался — он
|
||||||
|
фактически всё время был zombie на залоченном общим pgdata, теперь живой).
|
||||||
|
3. Старые stage-named volumes (`food-market-stage-{postgres-data,api-data,api-logs}`) переименованы
|
||||||
|
в `*-pre-isolation-20261005` (June stale data, в итоге не нужны).
|
||||||
|
4. `docker compose -p food-market-stage up -d` создал новые волюмы, API пробежал migrations
|
||||||
|
по пустой БД (EFCore).
|
||||||
|
5. `docker stop food-market-stage-api` → `DROP DATABASE food_market` → `CREATE DATABASE` →
|
||||||
|
`pg_restore --no-owner --no-privileges` → `docker start food-market-stage-api`.
|
||||||
|
6. Единственный FK-warning: `hangfire.state → hangfire.job` для одного «висящего» job'а в середине
|
||||||
|
OCR — не критично (новые job'ы пишутся с нуля).
|
||||||
|
|
||||||
|
## Что оставили read-only (per инструкции)
|
||||||
|
|
||||||
|
- Прод-контейнеры `food-market-{postgres,web}`, прод-том `food-market-postgres-data`,
|
||||||
|
прод-папка `/opt/food-market-data/` — только read-only inspect.
|
||||||
|
- `global.json`, gateway nginx — не касались.
|
||||||
|
- `/opt/food-market-data/uploads/invoices/2026-10/` оставлена пустая (туда и был `cp -a`, но файлов в
|
||||||
|
исходнике не было).
|
||||||
|
|
||||||
|
## Защитные swapchain'ы
|
||||||
|
|
||||||
|
- `~/deploy-stage.sh`: guard в начале (проверяет шаблон репо и stage-compose на prod-vm).
|
||||||
|
- `~/quality-watchdog.sh` step 10 (06:XX ежесуточно) — тот же набор паттернов + `docker inspect`
|
||||||
|
реального стенда. Падение → красный шаг + инцидент в очередь Server-Claude.
|
||||||
|
|
||||||
|
## Что НЕ в scope этой задачи
|
||||||
|
|
||||||
|
- Общий postgres-инстанс `food-market-postgres-data` всё ещё хостит DB соседей (`calcman_staging`,
|
||||||
|
`purchase`, `sklad`) — это их прод, не наш. Прод food-market, когда его поднимут, получит свою
|
||||||
|
чистую пустую БД в этом же волюме; восстанавливать его будем из backup'а.
|
||||||
|
- Legacy волюм `food-market-stage-minio-data` остался (не в compose) — snapshotнутый кусок старого
|
||||||
|
аборт-рефакторинга, не удаляю (`остаться как есть`).
|
||||||
|
|
||||||
|
## Follow-ups
|
||||||
|
|
||||||
|
- Поднять прод (отдельная задача, не этот тикет).
|
||||||
|
- Решить, что делать с pre-isolation-20261005 волюмами — через ~30 дней можно удалить.
|
||||||
Loading…
Reference in a new issue