Реализация на отдельных двух машинах с протестированным взаимодействием по проверке сообщений
This commit is contained in:
@@ -0,0 +1,405 @@
|
||||
# #1 SMS OTP deploy
|
||||
|
||||
Безопасный порядок развёртывания `sms-service`/worker на существующей ВМ и включения реальной OTP-доставки: сначала подготовить PostgreSQL и секреты, затем запустить новый контур при `mock=true`, проверить i-Digital и только после этого переключить Keycloak.
|
||||
|
||||
Старую сборку Keycloak после expand-миграции возвращать нельзя. Аварийный откат выполняется переключением новой сборки обратно в mock-режим.
|
||||
|
||||
## 0. До начала
|
||||
|
||||
- Получить у i-Digital:
|
||||
- `TOKEN_1`;
|
||||
- согласованное имя отправителя;
|
||||
- согласованный текст `auth_otp`;
|
||||
- подтверждённый source IP для callback;
|
||||
- регистрацию статического egress IP ВМ.
|
||||
- Создать PITR marker/backup managed PostgreSQL.
|
||||
- Скопировать `/opt/han-chat/backend/.env` в защищённое место вне каталога релиза.
|
||||
- Оставить `KEYCLOAK_OTP_MOCK_ENABLED=true` до последнего этапа.
|
||||
- На production-like при mock-режиме оставить `KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=true`.
|
||||
- Подтвердить у Direct актуальность IP `185.203.96.7`, указанного в `codebase/backend/nginx/templates/site-tls.conf.template`. Если IP другой — обновить allowlist до сборки nginx.
|
||||
|
||||
## 1. Создать пользователя и схему PostgreSQL
|
||||
|
||||
### 1.1. Создать пользователя
|
||||
|
||||
В интерфейсе Selectel создать отдельного пользователя:
|
||||
|
||||
```text
|
||||
sms_user
|
||||
```
|
||||
|
||||
Использовать случайный пароль не короче 32 символов.
|
||||
|
||||
### 1.2. Создать схему
|
||||
|
||||
Подключиться к `han_chat` под `dbAdmin` и выполнить:
|
||||
|
||||
```sql
|
||||
GRANT CONNECT ON DATABASE han_chat TO sms_user;
|
||||
GRANT CREATE ON DATABASE han_chat TO sms_user;
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS sms AUTHORIZATION sms_user;
|
||||
REVOKE ALL ON SCHEMA sms FROM PUBLIC;
|
||||
|
||||
ALTER ROLE sms_user IN DATABASE han_chat SET search_path TO sms, public;
|
||||
```
|
||||
|
||||
### 1.3. Проверить
|
||||
|
||||
Переподключиться к БД как `sms_user`:
|
||||
|
||||
```sql
|
||||
SELECT current_user;
|
||||
SHOW search_path;
|
||||
|
||||
SELECT
|
||||
nspname,
|
||||
pg_get_userbyid(nspowner) AS owner
|
||||
FROM pg_namespace
|
||||
WHERE nspname = 'sms';
|
||||
```
|
||||
|
||||
Ожидаемый результат:
|
||||
|
||||
- `current_user = sms_user`;
|
||||
- `search_path = sms, public`;
|
||||
- владелец схемы `sms` — `sms_user`.
|
||||
|
||||
Не выдавать `sms_user` права на схемы `han_app` и `keycloak`.
|
||||
|
||||
## 2. Заполнить `.env` на ВМ
|
||||
|
||||
Файл:
|
||||
|
||||
```text
|
||||
/opt/han-chat/backend/.env
|
||||
```
|
||||
|
||||
Добавить или обновить:
|
||||
|
||||
```dotenv
|
||||
SMS_SERVICE_IMAGE=han-chat-sms-service:local
|
||||
SMS_DATABASE_URL=postgresql+asyncpg://sms_user:<URL_ENCODED_PASSWORD>@<PG_HOST>:<PG_PORT>/han_chat?sslmode=verify-full&sslrootcert=/run/secrets/pg-ca.pem
|
||||
|
||||
KEYCLOAK_SMS_SERVICE_URL=http://sms-service:8080
|
||||
SMS_SERVICE_TOKEN=<openssl rand -hex 32>
|
||||
KEYCLOAK_SMS_SERVICE_TOKEN=<ТОЧНО ТО ЖЕ ЗНАЧЕНИЕ>
|
||||
|
||||
IDGTL_SMS_BASE_URL=https://direct.i-dgtl.ru
|
||||
IDGTL_SMS_API_KEY=<ГОТОВЫЙ TOKEN_1 БЕЗ ПОВТОРНОГО BASE64>
|
||||
IDGTL_SMS_CALLBACK_PUBLIC_URL=https://<PUBLIC_HOST>/callbacks/idgtl/sms
|
||||
IDGTL_SMS_CALLBACK_USERNAME=<openssl rand -hex 16>
|
||||
IDGTL_SMS_CALLBACK_PASSWORD=<openssl rand -hex 32>
|
||||
|
||||
NGINX_RATE_LIMIT_SMS_CALLBACK=120r/m
|
||||
|
||||
KEYCLOAK_OTP_MOCK_ENABLED=true
|
||||
KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=true
|
||||
```
|
||||
|
||||
Важно:
|
||||
|
||||
- пароль БД необходимо URL-encode, если он содержит специальные символы;
|
||||
- `SMS_SERVICE_TOKEN` и `KEYCLOAK_SMS_SERVICE_TOKEN` должны совпадать;
|
||||
- `IDGTL_SMS_API_KEY` — уже готовое значение Basic API key `TOKEN_1`, повторно кодировать его нельзя;
|
||||
- `KEYCLOAK_OTP_HMAC_KEY` во время rollout не менять.
|
||||
|
||||
### 2.1. Проверить egress IP
|
||||
|
||||
Из каталога `/opt/han-chat/backend`:
|
||||
|
||||
```bash
|
||||
docker compose --env-file .env --profile ops run --rm \
|
||||
--entrypoint curl toolbox -fsS https://api.ipify.org
|
||||
```
|
||||
|
||||
Полученный IP передать Direct для allowlist. При динамическом IP сначала настроить статический IP/NAT.
|
||||
|
||||
### 2.2. Проверить конфигурацию
|
||||
|
||||
```bash
|
||||
cd /opt/han-chat/backend
|
||||
|
||||
./scripts/validate-env .env
|
||||
docker compose --env-file .env config --quiet
|
||||
docker compose --env-file .env config --services
|
||||
```
|
||||
|
||||
## 3. Скопировать и собрать release
|
||||
|
||||
Копирование проекта выполняется по инструкции `deploy-steps.md`.
|
||||
|
||||
Сначала выполнить `rsync` с флагом `-n` и проверить список изменений. Убедиться, что исключены:
|
||||
|
||||
```text
|
||||
.env
|
||||
secrets/
|
||||
*.crt
|
||||
*.pem
|
||||
*.key
|
||||
```
|
||||
|
||||
После проверки повторить `rsync` без `-n`.
|
||||
|
||||
На ВМ:
|
||||
|
||||
```bash
|
||||
cd /opt/han-chat/backend
|
||||
|
||||
find . -type f \( -name '*.sh' -o -name 'validate-env' \) -exec dos2unix {} +
|
||||
chmod +x scripts/validate-env deployment/scripts/*.sh nginx/scripts/*.sh
|
||||
|
||||
./scripts/validate-env .env
|
||||
|
||||
docker compose --env-file .env build --pull \
|
||||
api-backend sms-service keycloak frontend-static nginx
|
||||
```
|
||||
|
||||
На этом этапе `KEYCLOAK_OTP_MOCK_ENABLED` всё ещё должен быть `true`.
|
||||
|
||||
## 4. Применить миграции
|
||||
|
||||
Перед миграцией создать PITR marker у провайдера БД.
|
||||
|
||||
```bash
|
||||
cd /opt/han-chat/backend
|
||||
|
||||
PITR_MARKER_CONFIRMED=true deployment/scripts/migrate.sh
|
||||
```
|
||||
|
||||
Команда применит:
|
||||
|
||||
- migration `0005_otp_settings` для `han_app`;
|
||||
- migration `0001_initial` для схемы `sms`;
|
||||
- migration `0002_seed` для схемы `sms`;
|
||||
- остальные штатные migrations проекта.
|
||||
|
||||
При необходимости применить production-like settings:
|
||||
|
||||
```bash
|
||||
docker compose --env-file .env --profile ops run --rm seed-settings
|
||||
```
|
||||
|
||||
Проверить версии:
|
||||
|
||||
```sql
|
||||
SELECT version_num FROM han_app.alembic_version;
|
||||
SELECT version_num FROM sms.alembic_version;
|
||||
```
|
||||
|
||||
Ожидается:
|
||||
|
||||
```text
|
||||
han_app: 0005_otp_settings
|
||||
sms: 0002_seed
|
||||
```
|
||||
|
||||
## 5. Записать согласованные sender и SMS-шаблон
|
||||
|
||||
Миграция намеренно создаёт placeholder. Пока он не заменён, `sms-service` будет возвращать `not_ready`.
|
||||
|
||||
Подключиться как `sms_user` и выполнить, подставив согласованные значения:
|
||||
|
||||
```sql
|
||||
UPDATE sms.sms_setting
|
||||
SET setting_value = to_jsonb('<APPROVED_SENDER>'::text),
|
||||
updated_at = now()
|
||||
WHERE setting_key = 'provider.idgtl.default_sender_name';
|
||||
|
||||
UPDATE sms.sms_template
|
||||
SET body_template = 'Код входа в HAN Chat: {code}. Действителен {ttl_min} мин.',
|
||||
sender_name = NULL,
|
||||
approved_at = now(),
|
||||
updated_at = now(),
|
||||
created_by = 'ops-approved'
|
||||
WHERE code = 'auth_otp'
|
||||
AND channel = 'SMS'
|
||||
AND locale = 'ru'
|
||||
AND version = 1;
|
||||
```
|
||||
|
||||
Если оператор согласовал другой текст, использовать именно его. В тексте должны остаться ровно два placeholders:
|
||||
|
||||
```text
|
||||
{code}
|
||||
{ttl_min}
|
||||
```
|
||||
|
||||
Для OTP должно сохраняться:
|
||||
|
||||
```text
|
||||
max_parts = 1
|
||||
```
|
||||
|
||||
Проверить:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
code,
|
||||
channel,
|
||||
locale,
|
||||
version,
|
||||
body_template,
|
||||
sender_name,
|
||||
max_parts,
|
||||
is_active,
|
||||
approved_at
|
||||
FROM sms.sms_template
|
||||
WHERE code = 'auth_otp';
|
||||
|
||||
SELECT setting_key, setting_value
|
||||
FROM sms.sms_setting
|
||||
ORDER BY setting_key;
|
||||
```
|
||||
|
||||
Должна существовать ровно одна active+approved версия `auth_otp`, а placeholder имени отправителя должен быть заменён.
|
||||
|
||||
## 6. Запустить SMS-контур при `mock=true`
|
||||
|
||||
```bash
|
||||
cd /opt/han-chat/backend
|
||||
|
||||
docker compose --env-file .env up -d sms-service sms-worker
|
||||
docker compose --env-file .env ps sms-service sms-worker
|
||||
docker compose --env-file .env logs --since=10m sms-service sms-worker
|
||||
```
|
||||
|
||||
Ожидается:
|
||||
|
||||
- `sms-service` — healthy;
|
||||
- worker запущен;
|
||||
- отсутствуют ошибки Direct `401`/`402`;
|
||||
- отсутствуют contract errors;
|
||||
- отсутствуют необъяснённые `uncertain`.
|
||||
|
||||
Затем запустить обновлённые смежные сервисы, не выключая mock:
|
||||
|
||||
```bash
|
||||
docker compose --env-file .env up -d --force-recreate \
|
||||
api-backend keycloak frontend-static nginx
|
||||
|
||||
docker compose --env-file .env exec -T nginx nginx -t -c /tmp/nginx.conf
|
||||
deployment/scripts/smoke.sh
|
||||
```
|
||||
|
||||
Первый запуск новой сборки Keycloak применит Liquibase expand migration. Старые незавершённые OTP challenges будут помечены истёкшими, поэтому запускать Keycloak лучше в период низкой активности.
|
||||
|
||||
## 7. Проверить i-Digital до включения real mode
|
||||
|
||||
Через внутренний endpoint:
|
||||
|
||||
```text
|
||||
POST /internal/sms/v1/send
|
||||
```
|
||||
|
||||
заказать одну SMS на контролируемый номер.
|
||||
|
||||
Требования к тесту:
|
||||
|
||||
- использовать уникальный `idempotency_key`;
|
||||
- не записывать service token и OTP в shell history;
|
||||
- JSON body создать во временном файле с правами `600`;
|
||||
- после теста удалить временный файл.
|
||||
|
||||
Проверить журнал:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
id,
|
||||
created_at,
|
||||
phone_masked,
|
||||
send_status,
|
||||
delivery_status,
|
||||
provider_message_id,
|
||||
provider_error_code,
|
||||
attempt_count,
|
||||
callback_last_at
|
||||
FROM sms.sms_outbound_message
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
Ожидается:
|
||||
|
||||
1. После заказа создана одна строка.
|
||||
2. `send_status` переходит в `accepted`.
|
||||
3. `provider_message_id` заполнен.
|
||||
4. Callback меняет `delivery_status` на `sent`/`delivered`.
|
||||
5. Повтор идентичного запроса возвращает тот же `sms_message_id` и не создаёт вторую SMS.
|
||||
|
||||
Проверить edge:
|
||||
|
||||
- публичный `/internal/sms/*` возвращает `404`;
|
||||
- callback не с IP Direct возвращает `403`;
|
||||
- реальный callback Direct проходит IP allowlist и Basic auth.
|
||||
|
||||
## 8. Включить реальные SMS
|
||||
|
||||
Только после успешной тестовой отправки изменить:
|
||||
|
||||
```dotenv
|
||||
KEYCLOAK_OTP_MOCK_ENABLED=false
|
||||
KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=false
|
||||
KEYCLOAK_OTP_MOCK_CODE=
|
||||
```
|
||||
|
||||
Применить:
|
||||
|
||||
```bash
|
||||
cd /opt/han-chat/backend
|
||||
|
||||
./scripts/validate-env .env
|
||||
|
||||
docker compose --env-file .env up -d \
|
||||
--no-deps \
|
||||
--force-recreate keycloak
|
||||
|
||||
docker compose --env-file .env ps keycloak
|
||||
docker compose --env-file .env logs --since=10m \
|
||||
keycloak sms-service sms-worker
|
||||
```
|
||||
|
||||
Проверить полный пользовательский сценарий:
|
||||
|
||||
1. Ввод номера телефона.
|
||||
2. Получение реальной SMS.
|
||||
3. Неверный OTP отклоняется.
|
||||
4. Верный OTP авторизует пользователя.
|
||||
5. Resend создаёт новый challenge.
|
||||
6. Старый challenge получает `superseded`.
|
||||
7. Старый код больше не принимается.
|
||||
8. OTP истекает через 60 секунд.
|
||||
9. Работают лимиты отправок и проверок.
|
||||
10. Уже active challenge продолжает локально проверяться при временно остановленном worker.
|
||||
|
||||
## 9. Аварийный откат
|
||||
|
||||
Не выполнять:
|
||||
|
||||
- downgrade Alembic;
|
||||
- downgrade Liquibase;
|
||||
- возврат старой сборки Keycloak.
|
||||
|
||||
После expand migration старая сборка Keycloak несовместима с новыми обязательными полями challenge.
|
||||
|
||||
Безопасный rollback — оставить новую сборку и вернуть mock:
|
||||
|
||||
```dotenv
|
||||
KEYCLOAK_OTP_MOCK_ENABLED=true
|
||||
KEYCLOAK_OTP_MOCK_CODE=<НЕПУБЛИЧНЫЙ 6-ЗНАЧНЫЙ КОД>
|
||||
KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=true
|
||||
```
|
||||
|
||||
```bash
|
||||
cd /opt/han-chat/backend
|
||||
|
||||
./scripts/validate-env .env
|
||||
|
||||
docker compose --env-file .env up -d \
|
||||
--no-deps \
|
||||
--force-recreate keycloak
|
||||
```
|
||||
|
||||
`sms-service` и worker можно оставить запущенными для обработки callback и reconciliation. Новые SMS-заказы от Keycloak прекратятся.
|
||||
|
||||
Записи со статусом `uncertain` автоматически не переотправлять — их необходимо разбирать вручную.
|
||||
Reference in New Issue
Block a user