13 KiB
#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 создать отдельного пользователя:
sms_user
Использовать случайный пароль не короче 32 символов.
1.2. Создать схему
Подключиться к han_chat под dbAdmin и выполнить:
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:
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 на ВМ
Файл:
/opt/han-chat/backend/.env
Добавить или обновить:
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 keyTOKEN_1, повторно кодировать его нельзя;KEYCLOAK_OTP_HMAC_KEYво время rollout не менять.
2.1. Проверить egress IP
Из каталога /opt/han-chat/backend:
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. Проверить конфигурацию
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 и проверить список изменений. Убедиться, что исключены:
.env
secrets/
*.crt
*.pem
*.key
После проверки повторить rsync без -n.
На ВМ:
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 у провайдера БД.
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:
docker compose --env-file .env --profile ops run --rm seed-settings
Проверить версии:
SELECT version_num FROM han_app.alembic_version;
SELECT version_num FROM sms.alembic_version;
Ожидается:
han_app: 0005_otp_settings
sms: 0002_seed
5. Записать согласованные sender и SMS-шаблон
Миграция намеренно создаёт placeholder. Пока он не заменён, sms-service будет возвращать not_ready.
Подключиться как sms_user и выполнить, подставив согласованные значения:
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:
{code}
{ttl_min}
Для OTP должно сохраняться:
max_parts = 1
Проверить:
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
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:
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:
POST /internal/sms/v1/send
заказать одну SMS на контролируемый номер.
Требования к тесту:
- использовать уникальный
idempotency_key; - не записывать service token и OTP в shell history;
- JSON body создать во временном файле с правами
600; - после теста удалить временный файл.
Проверить журнал:
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;
Ожидается:
- После заказа создана одна строка.
send_statusпереходит вaccepted.provider_message_idзаполнен.- Callback меняет
delivery_statusнаsent/delivered. - Повтор идентичного запроса возвращает тот же
sms_message_idи не создаёт вторую SMS.
Проверить edge:
- публичный
/internal/sms/*возвращает404; - callback не с IP Direct возвращает
403; - реальный callback Direct проходит IP allowlist и Basic auth.
8. Включить реальные SMS
Только после успешной тестовой отправки изменить:
KEYCLOAK_OTP_MOCK_ENABLED=false
KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=false
KEYCLOAK_OTP_MOCK_CODE=
Применить:
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
Проверить полный пользовательский сценарий:
- Ввод номера телефона.
- Получение реальной SMS.
- Неверный OTP отклоняется.
- Верный OTP авторизует пользователя.
- Resend создаёт новый challenge.
- Старый challenge получает
superseded. - Старый код больше не принимается.
- OTP истекает через 60 секунд.
- Работают лимиты отправок и проверок.
- Уже active challenge продолжает локально проверяться при временно остановленном worker.
9. Аварийный откат
Не выполнять:
- downgrade Alembic;
- downgrade Liquibase;
- возврат старой сборки Keycloak.
После expand migration старая сборка Keycloak несовместима с новыми обязательными полями challenge.
Безопасный rollback — оставить новую сборку и вернуть mock:
KEYCLOAK_OTP_MOCK_ENABLED=true
KEYCLOAK_OTP_MOCK_CODE=<НЕПУБЛИЧНЫЙ 6-ЗНАЧНЫЙ КОД>
KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=true
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 автоматически не переотправлять — их необходимо разбирать вручную.