docs(deploy): test-deploy Yandex Cloud spec + plan (single VM, nginx/php/pg/redis, real RLS roles)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-21 11:02:17 +03:00
parent e6752b5e4c
commit 815f0a2dcd
4 changed files with 781 additions and 1 deletions
+8
View File
@@ -1,6 +1,14 @@
# Глоссарий проекта Лидерра
# Формат: одно слово на строке. Кириллица в нижнем регистре.
# Test-deploy Yandex Cloud (2026-05-21)
hba
htpasswd
lsb
nslookup
scp
хостить
# A4 design-tooling integration (v2.8 / v3.8 / v1.22)
iconify
+1 -1
View File
@@ -1,6 +1,6 @@
# Brain Status (auto-generated)
Last updated: 2026-05-21T07:59:50.686Z
Last updated: 2026-05-21T08:00:35.867Z
| Контролёр | Состояние | Детали |
|---|---|---|
@@ -0,0 +1,631 @@
# Тестовый деплой портала Лидерра в Yandex Cloud — план
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task (inline — план содержит интерактивные шаги заказчика: создание VM, DNS, deploy-key). Steps use checkbox (`- [ ]`) syntax.
**Goal:** Поднять рабочую копию портала в интернете на одной Linux-VM в Yandex Cloud по адресу `https://<поддомен>` с HTTPS, доступом только для заказчика+Claude, для ручного теста.
**Architecture:** Одна Ubuntu 24.04 VM: nginx (HTTPS + Basic Auth) → PHP-FPM 8.3 → портал (Laravel 13 + собранный Vue) → PostgreSQL 16 + Redis 7 на той же машине; queue worker + scheduler как systemd-службы. Фронтенд собирается на dev-машине и заливается. Настоящие роли БД (RLS включён). Спека: `docs/superpowers/specs/2026-05-21-test-deploy-yandex-cloud-design.md`.
**Tech Stack:** Yandex Cloud Compute, Ubuntu 24.04 LTS, nginx, PHP 8.3-FPM, PostgreSQL 16, Redis 7, Certbot/Let's Encrypt, systemd, OpenSSH.
**Условные обозначения:** 🧑 = шаг заказчика (веб-интерфейс/решение), 🤖 = шаг Claude (Bash/SSH). Плейсхолдеры: `<SERVER_IP>`, `<DOMAIN>` (например `test.example.ru`), `<BASIC_USER>`/`<BASIC_PASS>` (дверь сайта) — заполняются по ходу.
---
## Фаза 0 — Подготовка на dev-машине (🤖, до создания сервера)
### Task 0.1: Проверить SSH-клиент и сгенерировать ключ деплоя
**Files:** `~/.ssh/liderra_deploy`, `~/.ssh/liderra_deploy.pub` (на dev-машине)
- [ ] **Step 1: Проверить наличие OpenSSH**
Run: `ssh -V; ssh-keygen --help 2>&1 | Select-Object -First 1`
Expected: версия OpenSSH (например `OpenSSH_for_Windows_9.x`). Если нет — поставить «OpenSSH Client» через Settings → Optional Features.
- [ ] **Step 2: Сгенерировать ключ-пару (без пароля, ed25519)**
Run (PowerShell):
```powershell
ssh-keygen -t ed25519 -f "$env:USERPROFILE\.ssh\liderra_deploy" -C "liderra-test-deploy" -N '""'
```
Expected: созданы `liderra_deploy` (приватный) и `liderra_deploy.pub` (публичный).
- [ ] **Step 3: Показать публичный ключ заказчику**
Run: `Get-Content "$env:USERPROFILE\.ssh\liderra_deploy.pub"`
Expected: строка `ssh-ed25519 AAAA... liderra-test-deploy`. Отдать заказчику для вставки при создании VM (Task 1.2).
### Task 0.2: Код-правка — временный флаг доступа к админке (TDD)
**Files:**
- Modify: `app/config/app.php` (добавить ключ `saas_admin_test_bypass`)
- Modify: `app/app/Http/Middleware/EnsureSaasAdmin.php`
- Test: `app/tests/Feature/Middleware/EnsureSaasAdminTest.php` (создать или дополнить)
- [ ] **Step 1: Написать падающий тест**
Создать `app/tests/Feature/Middleware/EnsureSaasAdminTest.php`:
```php
<?php
declare(strict_types=1);
use function Pest\Laravel\get;
it('blocks admin area in production by default', function () {
app()->detectEnvironment(fn () => 'production');
config(['app.saas_admin_test_bypass' => false]);
// любой admin-маршрут под EnsureSaasAdmin; подставить реальный из routes
$response = get('/api/admin/tenants');
expect($response->status())->toBe(503);
});
it('allows admin area in production when test bypass flag is on', function () {
app()->detectEnvironment(fn () => 'production');
config(['app.saas_admin_test_bypass' => true]);
$response = get('/api/admin/tenants');
expect($response->status())->not->toBe(503);
});
```
- [ ] **Step 2: Запустить — убедиться, что падает**
Run: `cd app; C:\tools\php83\php.exe artisan test --filter=EnsureSaasAdmin`
Expected: второй тест FAIL (сейчас middleware всегда 503 вне local/testing).
- [ ] **Step 3: Добавить ключ конфига**
В `app/config/app.php` добавить (рядом с другими ключами):
```php
'saas_admin_test_bypass' => (bool) env('SAAS_ADMIN_TEST_BYPASS', false),
```
- [ ] **Step 4: Поправить middleware**
В `app/app/Http/Middleware/EnsureSaasAdmin.php` заменить тело `handle`:
```php
public function handle(Request $request, Closure $next): Response
{
if (app()->environment('local', 'testing')) {
return $next($request);
}
// ВРЕМЕННО (тест-деплой): пропускаем при включённом флаге.
// TODO: убрать после внедрения Yandex 360 SSO (Б-1 + DO-4).
if (config('app.saas_admin_test_bypass') === true) {
return $next($request);
}
abort(503, 'SaaS-admin авторизация не настроена (ожидает Б-1 + DO-4).');
}
```
- [ ] **Step 5: Запустить тест — зелёный**
Run: `cd app; C:\tools\php83\php.exe artisan test --filter=EnsureSaasAdmin`
Expected: оба PASS.
- [ ] **Step 6: Линт + commit**
Run: `cd app; composer pint; composer stan`
Expected: 0 ошибок.
```bash
git add app/config/app.php app/app/Http/Middleware/EnsureSaasAdmin.php app/tests/Feature/Middleware/EnsureSaasAdminTest.php
git commit -m "feat(deploy): temporary SAAS_ADMIN_TEST_BYPASS flag for test server (off by default)"
```
> NB: маршрут `/api/admin/tenants` в тесте — подставить реальный admin-маршрут из `app/routes/`. Уточнить на Step 1 (grep по `EnsureSaasAdmin`).
### Task 0.3: Собрать фронтенд для прода
- [ ] **Step 1: Прод-сборка**
Run: `npm --prefix app run build`
Expected: создан `app/public/build/` с манифестом и ассетами, ошибок нет.
- [ ] **Step 2: Зафиксировать факт сборки**
Сборка не коммитится (build в .gitignore) — будет залита на сервер в Task 3.3 через scp. Проверить: `Test-Path app/public/build/manifest.json` → True.
---
## Фаза 1 — Создание сервера (🧑 заказчик в консоли YC, по инструкции Claude)
### Task 1.1: Зарезервировать статический публичный IP
- [ ] **Step 1:** YC Console → Virtual Private Cloud → IP-адреса → «Зарезервировать адрес» → зона `ru-central1-a`.
- [ ] **Step 2:** Записать выданный IP → это `<SERVER_IP>` (нужен для DNS; статический, чтобы адрес не менялся при перезагрузке).
### Task 1.2: Создать виртуальную машину
- [ ] **Step 1:** Compute Cloud → «Создать ВМ».
- [ ] **Step 2:** Параметры:
- Имя: `liderra-test`; зона `ru-central1-a`.
- Образ: **Ubuntu 24.04 LTS**.
- vCPU 2, RAM 2 ГБ, **гарантированная доля vCPU 20%** (дёшево; сборки идут на dev-машине).
- Диск: SSD 20 ГБ.
- Публичный адрес: выбрать **зарезервированный** из Task 1.1.
- Доступ: логин `deploy`; SSH-ключ — вставить публичный ключ из Task 0.1 Step 3.
- [ ] **Step 3:** Создать. Дождаться статуса RUNNING.
### Task 1.3: Открыть порты (группа безопасности)
- [ ] **Step 1:** VPC → Группы безопасности → группа сети ВМ → правила входящего трафика.
- [ ] **Step 2:** Разрешить TCP **22, 80, 443** (источник `0.0.0.0/0`; 22 можно сузить до IP заказчика/dev — но для простоты теста оставить открытым).
- [ ] **Step 3:** Сообщить Claude `<SERVER_IP>` → переходим к Фазе 2.
---
## Фаза 2 — Базовая настройка сервера (🤖 по SSH)
### Task 2.1: Первое подключение
- [ ] **Step 1: Подключиться**
Run: `ssh -i "$env:USERPROFILE\.ssh\liderra_deploy" -o StrictHostKeyChecking=accept-new deploy@<SERVER_IP> "echo OK; lsb_release -d"`
Expected: `OK` + `Ubuntu 24.04`.
- [ ] **Step 2: Обновить пакеты**
Run: `ssh ... deploy@<SERVER_IP> "sudo apt-get update && sudo apt-get -y upgrade"`
Expected: завершается без ошибок.
### Task 2.2: Установить стек
- [ ] **Step 1: Установить пакеты**
Run одной командой по SSH:
```bash
sudo apt-get install -y nginx \
php8.3-fpm php8.3-cli php8.3-pgsql php8.3-redis php8.3-mbstring \
php8.3-xml php8.3-curl php8.3-bcmath php8.3-zip php8.3-gd php8.3-intl \
postgresql postgresql-contrib redis-server git unzip certbot python3-certbot-nginx \
apache2-utils
```
Expected: установлено без ошибок (`apache2-utils` даёт `htpasswd`).
- [ ] **Step 2: Установить Composer**
```bash
php -r "copy('https://getcomposer.org/installer','/tmp/ci.php');" \
&& sudo php /tmp/ci.php --install-dir=/usr/local/bin --filename=composer
```
Run: `ssh ... "composer --version; php -v | head -1"`
Expected: Composer 2.x; PHP 8.3.
- [ ] **Step 3: Проверить службы**
Run: `ssh ... "systemctl is-active nginx php8.3-fpm postgresql redis-server"`
Expected: `active` × 4.
---
## Фаза 3 — База, код, конфиг (🤖 по SSH)
> **Порядок исполнения внутри фазы:** 3.2 (код на сервере — db/-скрипты приезжают с репо) → 3.1 (БД и роли) → 3.3 (фронтенд) → 3.4 (.env) → 3.5 (схема через migrate + grants + seed). Здесь нумерация по смыслу, но db-скрипты есть только после clone.
>
> **DB-роли (из `db/00_create_roles.sql` v1.1 + `app/config/database.php`):** пароли передаются psql через `-v` (НЕ `ALTER ROLE`). Схема грузится миграцией `load_initial_schema` (она делает `DB::unprepared(schema.sql)`) под ролью `crm_migrator` (BYPASSRLS+CREATEDB). Гранты — `db/02_grants.sql`. Рантайм — `crm_app_user` (RLS). Supplier-джобы — `crm_supplier_worker` (BYPASSRLS) через connection `pgsql_supplier`. Connection `pgsql_migrator` в конфиге НЕТ → для миграций временно подменяем `DB_USERNAME` на `crm_migrator` (default-connection `pgsql`), потом возвращаем на `crm_app_user`.
### Task 3.1: Создать БД и роли
**Files (на сервере):** `db/00_create_roles.sql` (после clone в 3.2).
- [ ] **Step 1: Сгенерировать пароли ролей (на dev или сервере)**
Run: `ssh ... "for r in app admin migrator audit supplier; do echo \$r=\$(openssl rand -hex 16); done"`
Expected: 5 строк вида `app=...`. Сохранить как `<APP_DB_PASS>` / `<ADMIN_DB_PASS>` / `<MIGRATOR_DB_PASS>` / `<AUDIT_DB_PASS>` / `<WORKER_DB_PASS>` (в безопасное место, не в git).
- [ ] **Step 2: Создать БД**
```bash
ssh ... "sudo -u postgres createdb liderra"
```
Expected: без ошибок.
- [ ] **Step 3: Создать роли с паролями (через -v)**
```bash
ssh ... "sudo -u postgres psql -d liderra \
-v crm_app_password='<APP_DB_PASS>' \
-v crm_admin_password='<ADMIN_DB_PASS>' \
-v crm_migrator_password='<MIGRATOR_DB_PASS>' \
-v crm_audit_writer_password='<AUDIT_DB_PASS>' \
-v crm_supplier_worker_password='<WORKER_DB_PASS>' \
-f /var/www/liderra/db/00_create_roles.sql"
```
Run: `ssh ... "sudo -u postgres psql -d liderra -c '\du' | grep -E 'crm_(app|migrator|supplier)'"`
Expected: 5 ролей созданы (`crm_app_user`, `crm_admin_user`, `crm_migrator`, `crm_audit_writer`, `crm_supplier_worker`).
- [ ] **Step 4: Разрешить TCP-вход ролям (pg_hba)**
> Роли ходят через 127.0.0.1 (scram). Убедиться, что `pg_hba.conf` имеет строку `host all all 127.0.0.1/32 scram-sha-256` (на Ubuntu по умолчанию есть). Если нет — добавить и `sudo systemctl reload postgresql`.
Run: `ssh ... "sudo grep -E '127.0.0.1/32' /etc/postgresql/16/main/pg_hba.conf"`
Expected: строка с `scram-sha-256` (или `md5`).
### Task 3.2: Выложить код (deploy-key + clone)
- [ ] **Step 1: Сгенерировать deploy-key на сервере**
```bash
ssh ... "ssh-keygen -t ed25519 -f ~/.ssh/github_deploy -N '' -C 'liderra-server'; cat ~/.ssh/github_deploy.pub"
```
Expected: публичный ключ сервера.
- [ ] **Step 2 (🧑): Добавить ключ в GitHub**
Заказчик: GitHub → репо `CoralMinister/lidpotok` → Settings → Deploy keys → Add → вставить ключ (read-only, без write).
- [ ] **Step 3: Настроить SSH для GitHub + clone**
```bash
ssh ... 'cat >> ~/.ssh/config <<EOF
Host github.com
IdentityFile ~/.ssh/github_deploy
StrictHostKeyChecking accept-new
EOF
sudo mkdir -p /var/www && sudo chown deploy:deploy /var/www
git clone git@github.com:CoralMinister/lidpotok.git /var/www/liderra
cd /var/www/liderra && git checkout main && git log -1 --oneline'
```
Expected: репозиторий склонирован, HEAD на нужном коммите (с флагом из Task 0.2 — убедиться, что коммит влит в `main`; иначе `git checkout <ветка>`).
- [ ] **Step 4: composer install**
```bash
ssh ... "cd /var/www/liderra/app && composer install --no-dev --optimize-autoloader --no-interaction"
```
Expected: зависимости установлены, 0 ошибок.
### Task 3.3: Залить собранный фронтенд
- [ ] **Step 1: Скопировать build на сервер**
Run (с dev-машины):
```powershell
scp -i "$env:USERPROFILE\.ssh\liderra_deploy" -r app/public/build deploy@<SERVER_IP>:/var/www/liderra/app/public/
```
Expected: `manifest.json` + ассеты на сервере.
### Task 3.4: Production .env
- [ ] **Step 1: Создать .env на сервере**
```bash
ssh ... 'cat > /var/www/liderra/app/.env <<EOF
APP_NAME=Liderra
APP_ENV=production
APP_DEBUG=false
APP_URL=https://<DOMAIN>
APP_LOCALE=ru
APP_FALLBACK_LOCALE=ru
APP_TIMEZONE=Europe/Moscow
LOG_CHANNEL=stack
LOG_LEVEL=warning
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=liderra
DB_USERNAME=crm_app_user
DB_PASSWORD=<APP_DB_PASS>
DB_SUPPLIER_USERNAME=crm_supplier_worker
DB_SUPPLIER_PASSWORD=<WORKER_DB_PASS>
SESSION_DRIVER=redis
SESSION_LIFETIME=120
QUEUE_CONNECTION=redis
CACHE_STORE=redis
REDIS_CLIENT=predis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_FROM_ADDRESS="hello@<DOMAIN>"
MAIL_FROM_NAME=Liderra
SAAS_ADMIN_TEST_BYPASS=true
AUTH_PASSWORD_RESET_TOKEN_TABLE=password_resets
EOF'
```
- [ ] **Step 2: APP_KEY**
```bash
ssh ... "cd /var/www/liderra/app && php artisan key:generate --force && php artisan about | head -20"
```
Expected: ключ сгенерирован; `Environment: production`, `Debug Mode: OFF`.
### Task 3.5: Схема (migrate), гранты, демо-данные, кэши
> Схему и сиды грузим под BYPASSRLS-ролью `crm_migrator`, потом возвращаем рантайм на `crm_app_user`. Подмена — временно правим `DB_USERNAME`/`DB_PASSWORD` в `.env` (это значения для default-connection `pgsql`, через которую идёт migrate/seed).
- [ ] **Step 1: Временно переключить .env на crm_migrator**
```bash
ssh ... "cd /var/www/liderra/app && \
sed -i 's/^DB_USERNAME=.*/DB_USERNAME=crm_migrator/; s/^DB_PASSWORD=.*/DB_PASSWORD=<MIGRATOR_DB_PASS>/' .env && \
grep -E '^DB_(USERNAME|PASSWORD)=' .env"
```
Expected: `DB_USERNAME=crm_migrator`.
- [ ] **Step 2: Накатить схему (миграция load_initial_schema грузит schema.sql)**
```bash
ssh ... "cd /var/www/liderra/app && php artisan migrate --force"
```
Run: `ssh ... "sudo -u postgres psql -d liderra -c '\dt' | tail -3"`
Expected: миграция `load_initial_schema` отработала; десятки таблиц (схема v8.27).
- [ ] **Step 3: Создать партиции (как на dev — ручной cron вместо pg_partman)**
```bash
ssh ... "cd /var/www/liderra/app && php artisan partitions:create-months"
```
Expected: партиции созданы (команда из ЭТАЛОН/project_phase1_strategy; если имя иное — `php artisan list | grep partition`).
- [ ] **Step 4: Применить гранты**
```bash
ssh ... "sudo -u postgres psql -d liderra -f /var/www/liderra/db/02_grants.sql"
```
Expected: гранты применены без ошибок (запуск под postgres-суперюзером — владелец/superuser, см. 00_create_roles doc вариант с crm_admin_user тоже подходит).
- [ ] **Step 5: Демо-данные (под crm_migrator, BYPASSRLS — cross-tenant сид проходит)**
```bash
# залить нужные демо-скрипты на сервер
scp -i "$env:USERPROFILE\.ssh\liderra_deploy" app/storage/_demo_5users.php app/storage/_demo_split_tenants.php deploy@<SERVER_IP>:/var/www/liderra/app/storage/
ssh ... "cd /var/www/liderra/app && php artisan db:seed --force && php artisan tinker storage/_demo_5users.php && php artisan tinker storage/_demo_split_tenants.php"
```
Expected: 5 компаний + учётки `admin@demo.local` / `manager1..4@demo.local` (пароль `password`).
> NB: точный набор демо-скриптов сверить с ЭТАЛОН §4 (там же команда восстановления). Залить только нужные `_demo_*.php`.
- [ ] **Step 6: Вернуть рантайм-роль crm_app_user**
```bash
ssh ... "cd /var/www/liderra/app && \
sed -i 's/^DB_USERNAME=.*/DB_USERNAME=crm_app_user/; s/^DB_PASSWORD=.*/DB_PASSWORD=<APP_DB_PASS>/' .env && \
grep -E '^DB_USERNAME=' .env"
```
Expected: `DB_USERNAME=crm_app_user` (RLS будет enforce'иться в рантайме).
- [ ] **Step 7: Права и кэши**
```bash
ssh ... 'cd /var/www/liderra/app \
&& sudo chown -R deploy:www-data storage bootstrap/cache \
&& sudo chmod -R 775 storage bootstrap/cache \
&& php artisan config:cache && php artisan route:cache && php artisan view:cache'
```
Expected: кэши собраны, прав хватает.
---
## Фаза 4 — Веб, HTTPS, дверь (🤖 + 🧑 DNS)
### Task 4.1: DNS A-запись (🧑)
- [ ] **Step 1:** В панели домена создать запись `A` для `<DOMAIN>``<SERVER_IP>`.
- [ ] **Step 2 (🤖): Проверить распространение**
Run: `ssh ... "getent hosts <DOMAIN> || nslookup <DOMAIN>"`
Expected: резолвится в `<SERVER_IP>` (может занять до 30–60 мин).
### Task 4.2: nginx vhost (HTTP)
- [ ] **Step 1: Конфиг сайта**
```bash
ssh ... 'sudo tee /etc/nginx/sites-available/liderra <<EOF
server {
listen 80;
server_name <DOMAIN>;
root /var/www/liderra/app/public;
index index.php;
# дверь на весь сайт (Basic Auth), кроме webhook поставщика
location / {
auth_basic "Liderra test";
auth_basic_user_file /etc/nginx/.htpasswd;
try_files \$uri \$uri/ /index.php?\$query_string;
}
location ^~ /api/webhook/ {
auth_basic off;
try_files \$uri \$uri/ /index.php?\$query_string;
}
location ~ \.php\$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
}
EOF
sudo ln -sf /etc/nginx/sites-available/liderra /etc/nginx/sites-enabled/liderra
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx'
```
Expected: `nginx -t` syntax ok; reload без ошибок.
> NB: точный префикс webhook (`/api/webhook/`) сверить с `app/routes/api.php` (grep `webhook`). Если иной — поправить `location ^~`.
- [ ] **Step 2: Создать пароль двери**
```bash
ssh ... "sudo htpasswd -bc /etc/nginx/.htpasswd <BASIC_USER> <BASIC_PASS>"
```
Expected: `.htpasswd` создан.
- [ ] **Step 3: Проверка по HTTP**
Run: `ssh ... "curl -s -o /dev/null -w '%{http_code}' -u <BASIC_USER>:<BASIC_PASS> http://<DOMAIN>/"`
Expected: `200` (или `302` на /login). Без креда → `401`.
### Task 4.3: HTTPS (Let's Encrypt)
- [ ] **Step 1: Выпустить сертификат**
```bash
ssh ... "sudo certbot --nginx -d <DOMAIN> --non-interactive --agree-tos -m <EMAIL> --redirect"
```
Expected: сертификат выпущен, nginx переписан на 443 + редирект с 80.
- [ ] **Step 2: Проверить HTTPS + авто-продление**
Run: `ssh ... "curl -sI -u <BASIC_USER>:<BASIC_PASS> https://<DOMAIN>/ | head -1; sudo certbot renew --dry-run 2>&1 | tail -1"`
Expected: `HTTP/2 200|302`; dry-run `Congratulations` / success.
---
## Фаза 5 — Фоновые службы (🤖)
### Task 5.1: queue worker как systemd-служба
- [ ] **Step 1: Юнит**
```bash
ssh ... 'sudo tee /etc/systemd/system/liderra-queue.service <<EOF
[Unit]
Description=Liderra queue worker
After=redis-server.service postgresql.service
[Service]
User=deploy
Restart=always
WorkingDirectory=/var/www/liderra/app
ExecStart=/usr/bin/php artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload && sudo systemctl enable --now liderra-queue'
```
Run: `ssh ... "systemctl is-active liderra-queue"`
Expected: `active`.
### Task 5.2: scheduler (cron)
- [ ] **Step 1: Cron-запись**
```bash
ssh ... '( crontab -l 2>/dev/null; echo "* * * * * cd /var/www/liderra/app && /usr/bin/php artisan schedule:run >> /dev/null 2>&1" ) | crontab -'
```
Run: `ssh ... "crontab -l | grep schedule:run"`
Expected: строка присутствует.
---
## Фаза 6 — Приёмка и сопровождение (🤖)
### Task 6.1: Проверка критериев готовности (DoD)
- [ ] **Step 1: HTTPS + замочек**
Открыть `https://<DOMAIN>` в браузере (с логином двери) → валидный сертификат, портал грузится.
- [ ] **Step 2: Дверь работает**
Run: `ssh ... "curl -s -o /dev/null -w '%{http_code}' https://<DOMAIN>/"``401` (без креда).
- [ ] **Step 3: Вход + данные**
В браузере: `admin@demo.local` / `password` → видно 4 демо-проекта.
- [ ] **Step 4: Изоляция компаний (RLS)**
Войти `manager1@demo.local` / `password` → видна только своя компания (чужих проектов нет). Если падает SQL — зафиксировать, чинить (риск из спеки §5.4).
- [ ] **Step 5: Админка**
Открыть `/admin/...` под админом → не 503 (флаг bypass работает).
- [ ] **Step 6: Службы переживают перезагрузку**
```bash
ssh ... "sudo reboot" # подождать ~40с
ssh ... "systemctl is-active nginx php8.3-fpm postgresql redis-server liderra-queue"
```
Expected: все `active`; сайт снова открывается.
### Task 6.2: Скрипт обновления + инструкция
**Files:** `/var/www/liderra/deploy.sh` (на сервере), `docs/deploy/test-server-runbook.md` (в репо)
- [ ] **Step 1: deploy.sh**
```bash
ssh ... 'cat > /var/www/liderra/deploy.sh <<EOF
#!/usr/bin/env bash
set -euo pipefail
cd /var/www/liderra
git pull
cd app
composer install --no-dev --optimize-autoloader --no-interaction
php artisan migrate --force
php artisan config:cache && php artisan route:cache && php artisan view:cache
sudo systemctl restart php8.3-fpm liderra-queue
echo "Deployed: \$(git -C /var/www/liderra log -1 --oneline)"
EOF
chmod +x /var/www/liderra/deploy.sh'
```
> Фронтенд при обновлении: пересобрать на dev (`npm --prefix app run build`) и `scp` build на сервер ПЕРЕД запуском deploy.sh.
- [ ] **Step 2: Runbook**
Создать `docs/deploy/test-server-runbook.md`: адрес, доступы (где лежат пароли), команда обновления, как остановить/удалить VM (прекратить оплату), напоминание убрать `SAAS_ADMIN_TEST_BYPASS` при переходе к настоящему SSO.
- [ ] **Step 3: Commit runbook**
```bash
git add docs/deploy/test-server-runbook.md
git commit -m "docs(deploy): test-server runbook"
```
---
## Открытые вопросы (заполнить при исполнении)
- `<DOMAIN>` и панель управления доменом — от заказчика.
- Точный admin-маршрут для теста (Task 0.2) и префикс webhook (Task 4.2) — grep по коду.
- Точные seed-шаги демо-учёток (Task 3.5) — по ЭТАЛОН §4.
- Пароли БД-ролей (`<APP_DB_PASS>`, `<ADMIN_DB_PASS>`, `<MIGRATOR_DB_PASS>`, `<AUDIT_DB_PASS>`, `<WORKER_DB_PASS>`) + дверь сайта (`<BASIC_PASS>`) — сгенерировать (Task 3.1 Step 1), сохранить в безопасном месте (не в git; занести в runbook-ссылку на хранилище).
- `pg_hba.conf` путь зависит от версии PG (`/etc/postgresql/16/main/`) — сверить на сервере.
@@ -0,0 +1,141 @@
# Тестовый деплой портала Лидерра в Yandex Cloud — дизайн
**Дата:** 2026-05-21
**Статус:** черновик дизайна (brainstorming) → ожидает вычитки заказчиком → writing-plans
**Автор:** Claude + Дмитрий
**Тип:** инфраструктура / деплой (не фича приложения)
## 1. Цель
Поднять рабочую копию портала Лидерры в интернете по стабильному адресу с настоящим
HTTPS, чтобы её могли открывать **только заказчик (Дмитрий) и Claude** для сквозного
ручного тестирования. Это **тестовое/staging-окружение**, не продакшен: без юр.лица,
без реальной почты, без SSO, под снос в любой момент.
## 2. Что НЕ входит (YAGNI / границы)
- ❌ Yandex 360 SSO (корпоративный вход админов) — ждёт Б-1 (ООО).
- ❌ Реальный landing, реальная почта (Unisender Go), Sentry-мониторинг, бэкапы,
автодеплой из GitHub (CI/CD).
- ❌ Управляемые БД/Redis Yandex (Managed PostgreSQL/Redis) — это для будущего прода.
- ❌ Перенос текущей dev-базы — на сервере свежие демо-данные.
- ❌ Публичный доступ для чужих тестеров (для этого понадобились бы реальная почта,
закрытие админки, реальная изоляция — отдельный этап).
## 3. Решения, принятые в brainstorming
| Развилка | Выбор |
|---|---|
| Где хостить | Отдельный Linux-сервер в **Yandex Cloud** (вариант A — всё на одной VM) |
| Аккаунт YC | Заводится с нуля заказчиком (создан 21.05.2026: облако `cloud-sasha261185`, каталог `default`); ожидает привязки платёжного аккаунта + грант 60 дней |
| Адрес | **Свой домен** (поддомен вида `test.<домен>`) + настоящий HTTPS (Let's Encrypt) |
| Кто настраивает сервер | **Claude по SSH** с dev-машины; заказчик даёт доступ (вставляет публичный ключ при создании VM) |
| Архитектура | Вариант A — один сервер, нативная установка (nginx + PHP-FPM + PostgreSQL + Redis), без Docker, без управляемых сервисов |
## 4. Архитектура сервера
Одна VM (Ubuntu LTS, ~2 vCPU / 24 ГБ, диск 15–20 ГБ SSD, зона `ru-central1-a`):
```
интернет
ваш домен (test.…) ──DNS A-запись──► публичный IP VM
┌───────┴─ nginx (HTTPS, Let's Encrypt авто-продление) ──────────┐
│ • HTTP Basic Auth на весь сайт (пускает только нас двоих) │
│ — кроме пути webhook поставщика (защищён HMAC-подписью) │
└───────┬─────────────────────────────────────────────────────────┘
PHP-FPM 8.3 ← код портала + собранный фронтенд (public/build)
┌───────┼─────────┐
PostgreSQL 16 Redis 7 (на этой же машине)
systemd-службы: queue worker (queue:work redis) + scheduler
(php artisan schedule:run по cron) — переживают перезагрузку
```
**Поток выкладки кода:**
1. Сервер тянет код из приватного репо `CoralMinister/lidpotok` по **read-only deploy-key**
(генерируется на сервере, заказчик добавляет в GitHub → Deploy keys).
2. `composer install --no-dev --optimize-autoloader`.
3. **Фронтенд собирается на dev-машине** (`npm --prefix app run build`) и заливается
(`app/public/build`) на сервер — чтобы не держать Node и не упираться в RAM при сборке.
4. Накат схемы БД (`db/schema.sql` v8.27) + демо-данные (seed + 5 учёток).
5. `php artisan config:cache route:cache view:cache`.
**Обновление новой версии** (когда понадобится) — одна идемпотентная команда/скрипт:
`git pull` → composer → залить новый build → migrate → пересобрать кэши → перезапустить
php-fpm + queue. Оформлю как `deploy.sh` на сервере + короткую инструкцию.
## 5. Безопасность теста
1. **Edge-дверь:** nginx HTTP Basic Auth на весь сайт (один общий логин/пароль для нас
двоих; хранится в `/etc/nginx/.htpasswd`). Посторонние и поисковики сайт не видят.
Исключение — путь приёма лидов от поставщика (webhook, защищён HMAC), чтобы при
желании протестировать живой приём от `crm.bp-gr.ru`.
2. **Админка:** middleware `EnsureSaasAdmin` в проде отдаёт 503 (ждёт Yandex SSO).
Добавляется **минимальный временный флаг** `SAAS_ADMIN_TEST_BYPASS` (config
`app.saas_admin_test_bypass`, default `false`): когда `true` — middleware пропускает.
Включается только на тест-сервере, помечен «убрать после внедрения реального SSO».
Правка в коде — небольшая, закоммичена, по умолчанию выключена → прод не затронут.
3. **Боевой режим без утечек:** `APP_ENV=production`, `APP_DEBUG=false`.
4. **Реальная изоляция компаний (RLS):** на сервере подключаются настоящие роли БД
(`db/00_create_roles.sql` + `db/02_grants.sql`; приложение ходит как `crm_app_user`,
джобы — как `crm_supplier_worker` BYPASSRLS). В отличие от dev (postgres-суперюзер,
RLS обходится) — изоляция реально работает.
- ⚠️ **Риск:** RLS включается «вживую» впервые. Возможен запрос, работавший под
суперюзером и падающий под RLS. Реакция: чиню точечно либо временно ослабляю роль.
Считается полезным для теста.
5. **SSH:** доступ по ключу (пароли отключены); порт 22 в группе безопасности по
возможности ограничить IP dev-машины + заказчика. Открыты порты 80/443/22.
6. **Почта:** `MAIL_MAILER=log` (письма в лог, не на ящик) — не нужны, заходим под
готовыми демо-учётками.
## 6. Данные
Демо-набор как на dev: 5 изолированных компаний, входы `admin@demo.local` +
`manager1..4@demo.local`, пароль у всех `password`. Демо-данные — стираемые.
## 7. Разделение работ
**Заказчик (через веб-интерфейсы, по инструкции Claude):**
1. Завершить регистрацию YC + привязать карту + забрать грант 60 дней.
2. Создать VM (Ubuntu), вставить публичный SSH-ключ Claude.
3. Сообщить публичный IP машины.
4. Прописать у домена A-запись `test.<домен>` → IP.
5. Добавить read-only deploy-key в GitHub-репо.
6. Придумать общий логин/пароль «двери» сайта.
**Claude (по SSH, сам):** вся установка/настройка сервера, выкладка кода, сборка-загрузка
фронтенда, схема БД + демо-данные, HTTPS, systemd-службы, проверка (портал открывается,
логин работает, изоляция компаний работает), `deploy.sh` + инструкция обновления.
**Доступ Claude:** только IP сервера + SSH по ключу, который Claude генерирует сам.
Паролей/карт заказчика Claude не получает.
## 8. Стоимость и жизненный цикл
- ~10001500 ₽/мес за VM (2 vCPU / 2–4 ГБ); грант 60 дней + до 10 000 ₽ — вероятно,
первый период бесплатно.
- Домен — ~200–1500 ₽/год (если ещё нет).
- Тест не нужен → VM остановить/удалить → оплата прекращается.
## 9. Критерии готовности (Definition of Done)
- По адресу `https://test.<домен>` открывается портал с валидным HTTPS-замочком.
- Сайт под Basic Auth (посторонний без логина не входит).
- Вход `admin@demo.local` / `password` работает; видны 4 демо-проекта.
- `manager1@demo.local` видит только свою компанию (RLS работает).
- Админка `/admin/*` доступна (через временный флаг).
- queue worker + scheduler работают как службы, переживают перезагрузку VM.
- Есть `deploy.sh` + инструкция «как выложить новую версию».
## 10. Открытые мелочи (решим в плане)
- Точный размер VM (2 ГБ vs 4 ГБ) — зависит от того, собираем ли фронт на сервере
(план: собираем на dev → 2 ГБ хватит).
- Точный путь webhook-исключения в nginx — уточнить по `routes/`.
- Имя поддомена и сам домен — от заказчика.