Перейти к содержимому

Архитектура и схемы

Технический обзор: как связаны Keeper, агенты, PostgreSQL, Redis и Vault, и как прогон проходит путь от описания желаемого состояния до применения на хосте.

Раздел предполагает, что вы знакомы со словарём из Концепций (Keeper, Souls, Destiny, Soulprint, Essence, Coven, Incarnation).

В центре — Keeper-кластер: один или несколько одинаковых stateless-инстансов поверх общих PostgreSQL (холодное состояние) и Redis (presence, координация). Любой инстанс обслуживает любой запрос. На управляемых хостах — агенты Soul, которые сами инициируют соединение к Keeper-у.

flowchart TB
    Op["Оператор / CI<br/>OpenAPI · MCP · soulctl"] -->|HTTP / mTLS| KC
    subgraph KC["Keeper-кластер — HA, stateless"]
        direction TB
        KK["keeper × N инстансов"]
        R[("Redis<br/>presence · lease · pub/sub")]
        PG[("PostgreSQL<br/>реестры · каталог · журналы")]
        KK --- R
        KK --- PG
    end
    KC -->|"pull: Soul инициирует стрим<br/>gRPC bidi-stream поверх mTLS"| SD["soul<br/>демон, агент"]
    KC -->|"push: Keeper идёт сам<br/>по SSH, oneshot"| HO["host<br/>без агента"]
    SL["soul-lint · CI / dev<br/>офлайн-валидация Destiny / сценариев / Essence — без Keeper и без сети"]

Внешняя инфраструктура (PostgreSQL, Redis, Vault) — это компоненты, которые вы поднимаете и эксплуатируете сами; Soul Stack их не поставляет в комплекте. Обязательный контур — PostgreSQL + Redis + Vault (все три; Keeper проверяет каждый на старте и не стартует без любого из них).

Связь Keeper ↔ Soul — долгоживущий gRPC bidirectional stream поверх mTLS. Принципиальный момент: соединение инициирует агент, а не сервер.

  • На управляемых хостах не нужно открывать входящие порты — агент сам дозванивается до Keeper-а и держит стрим.
  • По этому же стриму идут в обе стороны: задания на применение (Keeper → Soul) и отчёты о прогоне, события задач, отчёт Soulprint (Soul → Keeper).
  • Отдельного heartbeat-сообщения нет: gRPC keepalive и любое прикладное сообщение по стриму обновляют «время последней активности» агента.

Аутентификация на стриме — взаимная (mTLS): Keeper проверяет клиентский сертификат агента, агент — серверный сертификат Keeper-а. Оба сертификата выпускаются из одного PKI-корня.

Keeper-инстансы stateless и взаимозаменяемы. Разделение хранилищ — ключ к масштабированию:

  • PostgreSQL — холодное хранилище. Реестры агентов и операторов, каталог сервисов, runtime-инстансы (incarnation), журналы прогонов и аудит. Единственный источник истины состояния кластера.
  • Redis — горячий слой. Presence (кто сейчас на связи), lease на идентификаторы агентов, heartbeat-кэш, pub/sub-координация между инстансами Keeper. Волатильные данные не пишутся синхронно в PostgreSQL — они живут в Redis.

Фоновую уборку ведёт Reaper — задача внутри Keeper-а: чистит просроченные онбординг-токены, зомби-записи и устаревший TLS-материал агентов. В кластере Reaper работает только на одном инстансе одновременно — лидер выбирается через Redis-lease.

Один и тот же исполняемый файл soul и один и тот же набор модулей работают в двух режимах доставки:

РежимКто инициируетКак работает
pullагентsoul запущен как демон, держит долгоживущий gRPC-стрим к Keeper-у, ждёт и применяет задания. Для постоянно управляемых хостов.
pushKeeperKeeper по SSH доходит до хоста, на котором агент не установлен постоянно, выполняет применение разово (oneshot) и ничего постоянного не оставляет, кроме самих изменений.

В обоих режимах модули применяются одинаково — режим определяет только способ доставки, не логику применения. В реестре push-хост и pull-хост — это записи одного типа, различающиеся полем транспорта; переключение между режимами — смена одного поля без потери истории.

Принципиальная особенность Soul Stack: рендер выполняется на стороне Keeper-а, не на хосте. Агент не тянет шаблонизатор, не резолвит секреты и не имеет доступа к Vault — он получает уже готовые к применению задачи.

flowchart TB
    Op["Оператор — создаёт / обновляет incarnation<br/>(через OpenAPI / soulctl)"] --> K1
    K1["Keeper<br/>1. Резолв сервиса — git-источник + ref<br/>2. Резолв хостов — coven / where<br/>3. Рендер Destiny — vault-resolve → input-validation → CEL → text/template"]
    K1 -->|"ApplyRequest — готовые задачи<br/>по gRPC-стриму поверх mTLS"| S1
    S1["Soul<br/>4. Применяет модули по очереди, идемпотентно<br/>→ события задач и отчёт о прогоне"]
    S1 -->|"отчёт о прогоне (RunResult)"| K2
    K2["Keeper<br/>5. Все хосты успешно → state-commit в PostgreSQL<br/>Хоть один упал → incarnation: error_locked (состояние не коммитится)"]

Все YAML-источники проходят одинаковый конвейер. Порядок фиксирован, фазы не перемешиваются:

  1. vault-resolve — ссылки на секреты в параметрах заменяются на значения; делается до вычисления выражений.
  2. input-validation — эффективный вход оператора сверяется с объявленным контрактом (типы, обязательность, pattern/enum).
  3. CEL-render — вычисляются выражения в YAML: условия таргетинга, интерполяция значений (маркер ${ … }), чтение фактов хоста.
  4. text/template-render — рендер файлов из шаблонов (.tmpl) с явно поднятыми значениями.

Почему рендер на сервере, а не на хосте:

  • Безопасность. Секреты резолвятся централизованно; на хост уезжают только нужные значения, у агента нет Vault-токенов и доступа к секрет-хранилищу.
  • Лёгкий агент. Агент не тянет движок выражений и шаблонов — он применяет готовые задачи.
  • Единая точка истины. Контекст рендера (факты хостов, параметры, секреты) собирается там, где он целиком доступен.

Подробнее про модель выражений и границу движков — в разделе DSL → Шаблонизатор.

Состояние incarnation в базе обновляется только после того, как все целевые хосты успешно отработали — это cross-host барьер:

  • прогон дожидается завершения всех задач на всех хостах;
  • только после барьера результат сценария коммитится в incarnation.state (PostgreSQL);
  • если хоть одна задача хоть на одном хосте упала — состояние не коммитится, incarnation переходит в error_locked, и следующая операция отклоняется до явного разрешения оператора.

Это сознательный fail-fast: частичные применения не «проскакивают» молча, и зафиксированное состояние всегда соответствует факту на хостах.

Идентичность Soul строится на двух сущностях:

  • SID — идентификатор агента, равный FQDN хоста. Это автоматически даёт дедупликацию при переустановке агента на тот же хост. Обратная сторона: переименование FQDN — это миграция (in-place rename не поддерживается).
  • SoulSeed — mTLS-пара (сертификат + приватный ключ), которой агент аутентифицируется на стриме. Выпускается через CSR: приватный ключ генерируется на хосте и никогда его не покидает. В базе хранится только отпечаток сертификата — без PEM и без приватных ключей. SoulSeed регулярно ротируется по живому стриму.

Онбординг идёт в два хода: оператор регистрирует хост и получает одноразовый bootstrap-токен, затем агент на хосте обменивает токен на SoulSeed. Пошаговый онбординг — в Quick Start.

АртефактГде живётПочему
Service / Destiny / ModulegitЭто код: ревью, версионирование через git-ref, история изменений.
Incarnation / CovenPostgreSQLЭто runtime-состояние: полный operator-flow через OpenAPI/MCP, меняется в эксплуатации.
Provider / ProfilePostgreSQLРеестр cloud-провайдеров и профилей: полный operator-flow через REST (/v1/providers, /v1/profiles) / MCP / UI под RBAC provider.* / profile.*. На roadmap — готовые soul-cloud-* драйверы (в базовую поставку пока не входят). См. core.cloud.

Версия Service / Destiny / Module — это git-ref (тег или ветка), а не поле в манифесте: зависимости пинуются точным ref-ом, без semver-диапазонов.

  • Компоненты — детальный разбор Keeper, Soul, soulctl, soul-lint.
  • DSL — как описывать Destiny, сценарии и Essence.
  • Конфигурация — настройка Keeper-а и агента.
  • Операции — день второй: обновления, мониторинг, восстановление.