Представим одно обращение. Посетитель интернет-сервиса нажимает кнопку «Подготовить краткое описание заказа», и где-то далеко, у стороннего поставщика, должна выполниться операция: сгенерировать текст, проверить адрес, пересчитать тариф, отправить уведомление. Самый короткий путь выглядит соблазнительно: форма, бэкенд, HTTP-запрос к чужому API. Но между «нажал» и «поставщик получил» лежит вопрос, который архитектура обычно решает неявно: что именно пересекает границу между вашей основной системой и внешним миром?
Если ответ звучит «всё, что пришло от браузера», у системы нет границы, есть труба. Дальше в статье мы построим границу осознанно: российская часть сервиса остаётся местом, где живут пользователи, заказы и исходные сведения, а зарубежный интеграционный узел превращается в небольшую, скучную и очень предсказуемую станцию, которая принимает подготовленные задачи, обращается к внешнему API и возвращает результат.
Сразу оговорим рамки. Это разговор о технической минимизации данных. Выбор страны для сервера сам по себе не делает обработку данных соответствующей закону, а вопросы персональных данных, трансграничной передачи и договорных условий с поставщиком требуют отдельной проверки для вашего проекта. Архитектура лишь помогает сделать эту проверку проще: когда в зарубежный узел заведомо попадает мало, обсуждать нечего лишнего.
Одна операция от кнопки до ответа поставщика
Проследим операцию по шагам, на условном примере: интернет-магазин товаров для хобби хочет автоматически генерировать для карточки товара короткий маркетинговый текст через внешний сервис. Пример выдуманный, он нужен только для иллюстрации.
- Сотрудник магазина в админке нажимает «Сгенерировать описание» у товара.
- Российский бэкенд проверяет права сотрудника, достаёт карточку товара из своей базы и решает, какие поля вообще пригодны для отправки.
- Бэкенд формирует задачу: короткий JSON с типом операции, ключом операции и полезной нагрузкой. Эта задача кладётся в очередь, а сотруднику сразу возвращается ответ «принято».
- Исполнитель на зарубежном узле забирает задачу, добавляет секрет поставщика (его в России нет) и делает запрос к внешнему API с собственным таймаутом.
- Ответ поставщика проверяется, нормализуется и возвращается в Россию. Там он записывается рядом с товаром как черновик текста, ожидающий одобрения человека.
Обратите внимание, что в этой цепочке браузер никогда не говорит с зарубежным узлом напрямую, а зарубежный узел никогда не ходит в основную базу. Узел не знает, кто такой сотрудник, какие у магазина покупатели и как устроены заказы. Он знает только то, что лежит в задаче. Это и есть граница: разовая, явная и проверяемая по схеме сообщения.
Почему не просто «прозрачный» прокси
Самое простое решение, которое приходит в голову, это поставить на зарубежном сервере NGINX и перенаправлять на него запросы через proxy_pass. Оно рабочее для некоторых задач, но для нашей цели у него коварная особенность. В документации модуля NGINX proxy сказано, что передача тела исходного запроса по умолчанию включена, и исходные заголовки тоже передаются по умолчанию, а отправляемыми заголовками можно управлять через proxy_set_header (документация NGINX).
Это значит, что прокси без дополнительной настройки пропустит дальше всё, что пришло: тело, куки, служебные заголовки, которые ваш фреймворк когда-то добавил для удобства отладки. Прокси не знает, что из этого лишнее. Если вы всё же выбираете такой вариант для части трафика, настройку заголовков нужно делать явно и проверять на тестовом запросе, глядя, что именно приходит на другую сторону. Но для вызова внешнего API с бизнес-смыслом лучше другое: не пересылать чужой запрос, а собирать свой, из заранее оговорённых полей.
Разница принципиальная. Прокси пересылает то, что есть. Интеграционный узел с очередью отправляет то, что разрешено схемой. В первом случае безопасность держится на внимательности настройки, во втором — на устройстве самой системы.
Что должно попасть в задачу, а что остаться дома
Состав задачи удобно определять не вопросом «что нужно поставщику», а вопросом «без чего операция не может быть выполнена». Первая формулировка тянет за собой «давайте отправим всю карточку, вдруг пригодится». Вторая заставляет перечислять поля по одному и каждое оправдывать.
Вернёмся к генерации описания. Для текста поставщику, скорее всего, достаточно названия товара, категории, нескольких характеристик и желаемого тона. Не нужны ни внутренние артикулы закупки, ни себестоимость, ни имя менеджера, ни идентификаторы покупателей, ни ссылки на административные страницы. Задача может выглядеть примерно так:
{
"operation_id": "desc-7f3a91",
"kind": "product_description",
"schema_version": 1,
"payload": {
"title": "Набор акварельных красок, 24 цвета",
"category": "Товары для рисования",
"attributes": ["металлическая коробка", "кисть в комплекте"],
"tone": "спокойный",
"max_chars": 400
}
}В примере нет персональных данных и нет секретов: ни ключа поставщика, ни пользовательских токенов. Это не случайность, а требование к самой форме задачи. Любое поле, которое появляется в схеме, должно пройти короткий вопрос: зачем оно поставщику? Если ответ «на всякий случай», поле остаётся дома.
Схема как контракт, а не как пожелание
Чтобы граница не размывалась со временем, схему задачи стоит закрепить в коде с явным списком допустимых полей и отказом на любое лишнее. Типичный сценарий деградации выглядит так: через полгода кто-то добавляет в задачу «ещё одно поле для отладки», через год туда попадает целый объект пользователя «потому что так удобнее логировать». Если схема строгая и лишние ключи отклоняются при постановке в очередь, такая история заканчивается ошибкой в тестах, а не тихой утечкой.
Поле schema_version здесь не украшение. Когда состав задачи меняется, узел может одновременно понимать старую и новую версии, а изменения становятся видимыми в истории репозитория. Для разговора с юристом или ответственным за данные это удобный объект: вот версия схемы, вот список полей, вот причины каждого.
Три категории полей
| Категория | Примеры | Что делать |
|---|---|---|
| Необходимо для операции | название, категория, тон текста | Включить в задачу |
| Полезно, но не обязательно | длинное описание от производителя | Обсудить отдельно, по умолчанию не отправлять |
| Внутреннее и идентифицирующее | идентификаторы покупателей, закупочные цены, заметки менеджеров | Оставить в российской части |
Если исходные сведения содержат свободный текст, написанный людьми (комментарии, пожелания заказчика), такие поля стоит считать подозрительными по умолчанию. В свободный текст люди кладут что угодно: телефоны, адреса, имена. Здесь нет технического фильтра, который гарантированно всё вычистит, поэтому надёжнее вообще не отправлять такие поля, либо отправлять только после явного решения о том, что их содержимое допустимо.
Где живут исходные данные, а где обрабатывается ответ
Вторая половина границы касается хранения. Ошибка, которую легко совершить, состоит в том, что интеграционному узлу «для удобства» дают собственную базу и складывают в неё и входящие задачи, и ответы, и промежуточные данные. Через месяц узел становится вторым хранилищем, а о том, какие сведения где лежат, уже никто не помнит.
Более здоровое разделение такое:
- Российская часть хранит исходные сведения, пользователей, историю операций и итоговые результаты. Это единственный источник истины.
- Зарубежный узел хранит только то, что нужно для работы в данный момент: очередь задач, короткоживущие записи о состоянии попыток и собственную конфигурацию. Долгих копий бизнес-данных там не должно быть.
- Ответ поставщика проходит через узел, нормализуется (например, обрезается до нужной длины и приводится к одному формату) и возвращается в российскую часть, где становится частью записи товара.
Нормализация на узле полезна по практической причине: внешние API меняются, поля в ответах появляются и исчезают. Если узел превращает ответ поставщика в собственный стабильный формат, основная система не обязана знать подробности чужого API. Если завтра вы смените поставщика, поменяется адаптер на узле, а контракт между узлом и Россией останется прежним.
Резервные копии: только то, что действительно есть
Раз основное хранилище находится в российской части, именно оно требует нормального экспорта. Если вы используете PostgreSQL, то согласно документации, pg_dump создаёт согласованный экспорт одной базы, в том числе при параллельной работе пользователей, но не охватывает глобальные роли и tablespaces, для которых предусмотрен pg_dumpall (документация PostgreSQL). Практический вывод для нашей схемы: роли и права доступа нужно сохранять отдельно от выгрузки данных, иначе восстановленная база может оказаться без нужных пользователей.
Зарубежному узлу, построенному по принципу «ничего ценного не храним», копии базы данных вообще не нужны. Достаточно хранить в репозитории его конфигурацию и способ развёртывания, чтобы поднять замену с нуля. Секреты поставщиков хранятся отдельно от кода, и их восстановление — это перевыпуск или повторная выдача из менеджера секретов, а не извлечение из архива.
Секреты и журнал
Ключ внешнего API должен жить только на узле, который к API обращается. Российская часть его не знает и не должна знать: так компрометация сайта не раскрывает доступ к поставщику, а компрометация узла не раскрывает пользовательскую базу, потому что в узле её нет. Это простое следствие разделения ролей.
Журнал заслуживает отдельного слова. Журнал запросов часто оказывается самой неряшливой частью системы: туда пишут тела запросов и заголовки «чтобы потом разбираться». Для узла разумный набор записей такой: идентификатор операции, тип, версия схемы, время начала и конца, статус ответа поставщика, длительность, номер попытки. Тела запросов и ответов не пишутся, заголовки авторизации не пишутся никогда. Если для диагностики иногда нужны фрагменты тела, лучше включать это временно и сознательно, а не держать по умолчанию.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Когда поставщик молчит: очередь, таймауты и повторы
Внешний API отвечает не всегда. Он может медленно думать, отвечать ошибкой, ограничивать частоту запросов или временно быть недоступным с вашего узла. Архитектура очереди нужна как раз для того, чтобы сайт на это не реагировал зависанием страницы: пользователь получает «задача принята», а дальше система сама разбирается.
Если вы используете Celery, в его документации по задачам важны два предупреждения. Во-первых, сетевым операциям нужны собственные таймауты, иначе исполнитель может зависнуть на ожидании ответа. Во-вторых, долгие и короткие задачи рекомендуется направлять разным исполнителям (документация Celery). Для нашего узла это выливается в практическое правило: вызовы к медленному поставщику живут в своей очереди, чтобы они не вытесняли быстрые служебные задачи вроде проверки статуса.
Повтор и ключ операции
Самое тонкое место всей схемы: что произойдёт, если задачу выполнят дважды? Это не экзотика. Документация Celery подчёркивает, что идемпотентность важна при повторном выполнении: момент подтверждения сообщения и фактическое завершение бизнес-операции различаются, а режим acks_late меняет момент подтверждения, но имеет исключения при завершении процесса. Проще говоря, ни очередь, ни настройка не дают вам гарантии «ровно один раз» для внешнего эффекта.
Поэтому защита должна быть в самой операции. Здесь и пригождается operation_id из примера выше. Он создаётся один раз, при постановке задачи в российской части, и остаётся неизменным при всех повторах. Важная деталь: новый идентификатор на каждую попытку ничего не даёт, потому что тогда повтор выглядит как совершенно новая операция. Идемпотентность появляется не от самого наличия идентификатора, а от того, что и узел, и приёмная сторона умеют по нему распознавать «это та же операция».
Что можно делать с таким ключом на практике:
- на узле перед вызовом поставщика проверять, не завершена ли уже операция с таким ключом, и если завершена — вернуть сохранённый короткий результат вместо повторного вызова;
- в российской части при записи результата использовать ключ как уникальный: повторная запись того же ключа не создаёт второй черновик, а обновляет или игнорируется;
- если поставщик API поддерживает собственный механизм идемпотентных запросов, передавать ему тот же ключ, но это нужно подтвердить по документации конкретного поставщика, а не предполагать.
Отдельно стоит помнить, что транзакция вашей базы не охватывает внешний вызов. Нельзя написать «записать в базу и вызвать API» так, чтобы всё откатилось вместе. Либо операция завершилась у поставщика, но ответ потерялся, либо ответ пришёл, но запись в базу не удалась. Поэтому состояние операции моделируется явно: «поставлена», «отправляется», «получен ответ», «записана», «ошибка». Каждый переход можно повторить без вреда, если он опирается на ключ.
Какие отказы бывают и что с ними делать
| Ситуация | Реакция узла |
|---|---|
| Таймаут ответа поставщика | Повтор с тем же ключом, увеличивающаяся пауза, ограниченное число попыток |
| Ответ с ошибкой на стороне поставщика | Повторять, если ошибка похожа на временную; иначе пометить операцию как неуспешную |
| Ошибка в самой задаче (не прошла схема) | Не повторять, вернуть причину в российскую часть |
| Поставщик недоступен с узла длительное время | Остановить повторы, показать оператору состояние «интеграция недоступна» |
Последняя строка важна по смыслу: зарубежное расположение узла не гарантирует, что поставщик будет доступен. Узел может быть заблокирован, ограничен по региону или частоте запросов самим поставщиком, а условия доступа могут меняться. Поэтому интерфейс для человека должен честно показывать «операция в очереди» или «интеграция сейчас недоступна», а не делать вид, что всё работает мгновенно. Прикладное следствие: функция, зависящая от внешнего API, не должна блокировать основной сценарий сайта. Описание товара можно написать руками, пока генератор молчит.
Что нужно выяснить до размещения узла
Техническая схема отвечает на вопрос «как передавать мало», но не на вопрос «можно ли передавать это в принципе». Некоторые вещи придётся выяснять за пределами кода, и лучше сделать это до разворачивания сервера, а не после.
- Состав данных. Составьте перечень полей задачи и отметьте, какие из них могут относиться к физическим лицам прямо или косвенно. Если таких полей нет и схема это принудительно обеспечивает, разговор упрощается. Если есть, нужна отдельная оценка.
- Условия поставщика. Прочитайте, как поставщик обрабатывает присланные данные, хранит ли их, использует ли для обучения, где расположены его мощности, и допускает ли ваш тип использования и ваш регион.
- Требования проекта. Договоры с клиентами, внутренние политики и отраслевые правила могут накладывать собственные ограничения на размещение и обработку. Они зависят от конкретного бизнеса.
- Роль узла. Определите, действует ли зарубежный узел как простой транспорт или обрабатывает сведения по существу. От этого зависит, как его описывать в документах.
По перечисленным вопросам решение принимают ответственные за проект и, при необходимости, юристы. Статья о серверах не может заменить такого заключения, а страна размещения сама по себе ничего не решает: узел в любой юрисдикции передаёт ровно то, что в него положили. Зато хорошо ограниченный состав задачи делает такой разговор предметным: вы показываете схему, а не рассуждаете о намерениях.
Если у вас вопрос про оплату и подключение конкретного поставщика, а не про устройство узла, отдельно разобран пример с ИИ-сервисом: как подключить и оплатить OpenAI API из России. Он дополняет эту статью со стороны условий доступа и платежей.
Какой VPS нужен узлу, который почти ничего не хранит
Профиль нагрузки у такого узла своеобразный. Он почти не пишет на диск, не держит большую базу и в основном ждёт ответов по сети. Основные ресурсы нужны на очередь, несколько исполнителей, TLS-соединения и запас под всплески. Поэтому нет смысла начинать с большой машины: разумно взять небольшой тариф и мерить реальное поведение.
Среди тарифов MAATRIX есть, например, Ba (1 vCPU, 2 ГБ RAM, 40 ГБ NVMe) и Uab (2 vCPU, 4 ГБ, 80 ГБ). Это отправные точки для теста, а не обещание определённой ёмкости: сколько параллельных вызовов выдержит узел, зависит от числа исполнителей, размера задач и поведения поставщика, и это нужно проверить собственным нагрузочным прогоном. Если окажется тесно, переход на следующий тариф — рутинная операция, а вот перестройка архитектуры была бы куда дороже.
Что реально полезно в предложении: на VPS есть root-доступ, значит можно поставить собственные очередь, исполнителей и настроить фаервол так, чтобы узел принимал соединения только от вашей российской части. В публичном предложении перечислены локации UK, US, RU и FR, что даёт выбор места под задачу, но выбор локации не гарантирует доступность стороннего API с этого адреса: это проверяется практическим запросом к нужному поставщику до окончательного решения. Если вы автоматизируете цепочки, в каталоге также упоминаются n8n и Nextcloud с установкой, а точное наличие образов проверяется в форме заказа. Подробнее о самом предложении можно посмотреть на странице аренды VPS MAATRIX.
Заметьте, чего в этом списке нет. Мы не рассчитываем на «встроенные» резервные копии или изоляцию, потому что они не входят в подтверждённые факты о тарифах. Для узла без собственных данных это нормально: восстановление сводится к повторному развёртыванию по описанной конфигурации. А вот если вы решите хранить на нём что-то ценное, резервное копирование придётся организовать самостоятельно.
Как выглядит разделённая система целиком
Соберём картину обратно. В России работает сайт и основная база: там пользователи, товары, заказы и итоговые тексты. Когда нужна внешняя операция, сайт формирует короткую задачу по строгой схеме, присваивает ей постоянный ключ и кладёт в очередь. Зарубежный узел забирает задачу, добавляет секрет, вызывает поставщика с таймаутом, нормализует ответ и возвращает его обратно. При сбоях повторяется та же операция с тем же ключом, а не новая. Журнал хранит факты о вызове, но не содержимое. Состояние операции видно человеку, а основной сценарий сайта не зависит от того, отвечает ли сейчас поставщик.
У каждого звена своя понятная функция. Российская часть владеет данными и решает, что можно отправить. Очередь отделяет скорость пользователя от скорости внешнего мира. Узел знает про поставщика и его секрет, но ничего не знает про ваших людей. Обмен между ними сведён к тому, что без чего не обойтись, и именно в этой скромности основная ценность схемы: когда придёт время проверять условия проекта, придётся объяснять не «всё сразу», а один короткий документ о том, что и зачем пересекает границу.