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

keeper.yml

Конфиг одного инстанса Keeper-а. Несколько инстансов с разным kid стоят за общими PostgreSQL и Redis и образуют кластер — конфиг у каждого свой, различается как минимум идентификатором.

Файл — типизированный YAML: на старте он проходит валидацию (структура, типы, инварианты). Неизвестный ключ, неверный тип или нарушенный инвариант отвергаются с диагностикой; конфиг не применяется частично.

ЗаписьСмысл
stringстрока UTF-8.
intцелое.
booltrue / false.
durationGo-duration (1s / 500ms / 1h30m) плюс суффикс <N>d для дней (30d). Композитная форма вида 1d2h не поддерживается.
enum{a,b,c}строка из перечисленного множества.
string(host:port)host:port; host — IP или DNS-имя, port1..65535.
vault-refстрока vault:<path> (например vault:secret/keeper/postgres); резолвится Vault-клиентом Keeper-а на старте.
pathабсолютный путь в ФС хоста Keeper-а.

В таблицах ниже default: — означает обязательное поле. Опциональные блоки можно опускать целиком — применяются значения по умолчанию.

kid: keeper-eu-west-01
ПолеТипDefaultНазначение
kidstring (kebab-case, уникален в кластере)Стабильный человекочитаемый идентификатор инстанса. Используется в lease на агентов, в аудит-событиях, в метках метрик. Обязателен.

Keeper поднимает несколько независимых listener-ов. gRPC разделён на два sub-listener-а: bootstrap (онбординг агента, server-only TLS — у агента до онбординга ещё нет клиентского сертификата) и event_stream (долгоживущий bidi-стрим, mTLS).

listen:
grpc:
bootstrap:
addr: "0.0.0.0:9442"
tls:
cert: /etc/keeper/tls/server.crt
key: /etc/keeper/tls/server.key
event_stream:
addr: "0.0.0.0:8443" # прод-дефолт 8443; dev часто 9443
max_apply_size_mb: 8
tls:
cert: /etc/keeper/tls/server.crt
key: /etc/keeper/tls/server.key
ca: /etc/keeper/tls/ca.crt
openapi: { addr: "0.0.0.0:8080" }
mcp: { addr: "0.0.0.0:8081" } # опционально; уберите блок — MCP выключен
metrics: { addr: "0.0.0.0:9090" }
ПолеТипDefaultНазначение
listen.grpc.bootstrap.addrstring(host:port)bind-адрес listener-а онбординга (server-only TLS). Обязателен. Типично :9442 (одинаков в dev и проде).
listen.grpc.bootstrap.tls.cert / .keypathсерверный сертификат и ключ для онбординга. CA сюда не кладётся (TLS односторонний).
listen.grpc.event_stream.addrstring(host:port)bind-адрес долгоживущего стрима к агентам (mTLS). Обязателен; должен отличаться от bootstrap-адреса. Прод-дефолт :8443, в dev обычно :9443.
listen.grpc.event_stream.tls.cert / .keypathсерверный сертификат и ключ. Допустимо совпадение с bootstrap.
listen.grpc.event_stream.tls.capathCA, которой валидируются клиентские сертификаты агентов на mTLS-handshake.
listen.grpc.event_stream.max_apply_size_mbint (МиБ, ≥1)8Потолок размера одного исходящего сообщения к агенту (прежде всего пачка отрендеренных задач). При попытке отправить больше — fail-fast с понятной ошибкой, а не молчаливый отказ на стороне агента. Должен быть ≤ recv-лимиту агента (keeper.max_apply_size_mb в soul.yml); дефолты обеих сторон совпадают (8 МиБ).
listen.openapi.addrstring(host:port)bind-адрес Operator API (первичный интерфейс оператора, OpenAPI). Обязательный listener; отключение не предусмотрено. Типично :8080.
listen.mcp.addrstring(host:port)bind-адрес MCP-сервера (интерфейс наравне с OpenAPI). Опциональный listener: если addr не задан / блок убран — MCP не поднимается, MCP выключен. Типично :8081.
listen.metrics.addrstring(host:port)bind-адрес выделенного Prometheus-/metrics. Отдельный порт (обычно :9090), без auth-цепочки Operator API. Обязательный listener; отключение не предусмотрено. Опц. защита — блок metrics.
postgres:
dsn_ref: vault:secret/keeper/postgres
pool: { min: 5, max: 50 }

PostgreSQL — единственное холодное хранилище состояния кластера: реестры агентов и операторов, каталог сервисов, журналы.

ПолеТипDefaultНазначение
postgres.dsn_refvault-refVault-ссылка на полный DSN. Plaintext-DSN в файл не пишется. Поле в Vault KV — dsn.
postgres.pool.minint (≥1)2Минимальный размер пула на инстанс.
postgres.pool.maxint (≥min)20Максимальный размер пула. Суммарный поток к PG = max × число инстансов — учитывайте при настройке max_connections PostgreSQL.

Redis — горячий слой и шина координации между инстансами: presence/heartbeat, lease на идентификаторы агентов, выбор лидера фоновых задач. Клиент поддерживает три топологии нативно — mode: standalone | sentinel | cluster; пустой/опущенный mode = standalone (forward-compat). Выбор режима под HA — prerequisites.md → Redis.

# Standalone (default): один узел.
redis:
mode: standalone # можно опустить — это default
addr: "redis.example.com:6379"
password_ref: vault:secret/keeper/redis
# Sentinel: HA с автоматическим failover (рекомендуемый прод-путь on-premise).
redis:
mode: sentinel
master_name: mymaster
sentinels:
- "sentinel-1.example.com:26379"
- "sentinel-2.example.com:26379"
- "sentinel-3.example.com:26379"
password_ref: vault:secret/keeper/redis # пароль Redis-узлов
sentinel_password_ref: vault:secret/keeper/redis#sentinel # опц., пароль sentinel-узлов
# Cluster: шардирование по слотам (горизонтальное масштабирование).
redis:
mode: cluster
nodes:
- "redis-1.example.com:6379"
- "redis-2.example.com:6379"
- "redis-3.example.com:6379"
password_ref: vault:secret/keeper/redis
ПолеТипDefaultНазначение
redis.modeenum{standalone,sentinel,cluster}standalone (пусто/опущено)Топология Redis. standalone — один узел; sentinel — Redis Sentinel HA (master-discovery в клиенте); cluster — Redis Cluster (slot-routing в клиенте).
redis.addrstring(host:port)Адрес узла. Обязателен при mode: standalone; при sentinel/cluster игнорируется.
redis.master_namestring— (optional)Имя monitored master group. Обязателен при mode: sentinel.
redis.sentinelslist<string(host:port)>— (optional)Адреса sentinel-узлов. Обязателен (непустой) при mode: sentinel.
redis.nodeslist<string(host:port)>— (optional)Адреса узлов кластера для bootstrap-discovery (клиент сам подтянет полную топологию). Обязателен (непустой) при mode: cluster.
redis.password_refvault-ref или stringПароль Redis. vault:<mount>/<path>[#field] резолвится из Vault (default-поле password, override через #field); plaintext работает как есть; пустое — без пароля.
redis.sentinel_password_refvault-ref или string— (optional)Отдельный пароль самих sentinel-узлов. Та же форма, что password_ref. Имеет смысл только при mode: sentinel.

Vault — обязательная зависимость Keeper-а: блок проверяется на старте, без доступного Vault Keeper не стартует. Несёт PKI для выпуска mTLS-идентичности агентов, KV для секретов (DSN, пароли) и ключ подписи операторских токенов.

Keeper читает KV-секреты прозрачно как из KV v1, так и из KV v2 — версия mount-а определяется автоматически, оператору всё равно, какая из них поднята. Указывать версию в конфиге не нужно (см. опциональный override kv_version ниже).

# Dev: статический токен
vault:
addr: "http://127.0.0.1:8200"
token: "dev-root-token"
auth: { method: token } # default; блок auth можно опустить
pki_mount: "pki"
# Прод: AppRole
vault:
addr: "https://vault.example.com:8200"
auth:
method: approle
role_id: keeper-prod # не секрет — допустим inline
secret_id_file: /etc/keeper/vault-secret-id # mode 0400/0600
pki_mount: "pki/soulstack"

vault.auth.method выбирает способ аутентификации Keeper-а в Vault:

  • token (default) — статический токен из vault.token. Удобно для локальной разработки. Блок auth целиком можно опустить — это эквивалент method: token.
  • approle — прод-путь: Keeper делает approle/login с role_id + secret_id и получает renewable-токен, который продлевается в фоне.

AppRole-credentials не читаются из самого Vault (иначе — циклическая зависимость: именно ими Keeper и логинится). Источник локальный: role_id — не секрет, задаётся inline; secret_id — секрет, берётся из mode-ограниченного файла (secret_id_file) либо переменной окружения (secret_id_env).

ПолеТипDefaultНазначение
vault.addrstring (URL)Адрес Vault.
vault.tokenstringСтатический токен для method: token. При method: approle задавать нельзя.
vault.kv_mountstringsecretMount point KV (без указания версии). Работает и для KV v1, и для KV v2 — версия определяется автоматически.
vault.kv_versionenum{"1","2"}— (auto)Опциональный override версии KV mount-а. По умолчанию — автоопределение, указывать не нужно. Задаётся только для захардненного Vault, где автоопределение закрыто ACL (политика не даёт читать служебный mount-эндпоинт). Значение вне множества отвергается.
vault.auth.methodenum{token,approle}tokenМетод аутентификации. Пустое = token.
vault.auth.role_idstringrole_id AppRole (не секрет). Обязателен при method: approle.
vault.auth.secret_id_filepathПуть к файлу с secret_id. Взаимоисключающ с secret_id_env; ровно один обязателен при approle.
vault.auth.secret_id_envstringИмя переменной окружения с secret_id.
vault.pki_mountstringMount PKI engine, через который Keeper выпускает идентичность агентов при онбординге.
vault.pki_rolestring(optional)Имя PKI-роли. Vault подписывает CSR через <pki_mount>/sign/<pki_role>.

Версию KV в конфиге указывать не нужно — один и тот же vault-блок работает и на KV v1, и на KV v2. Keeper определяет версию mount-а автоматически (probe через sys/internal/ui/mounts), поэтому в обычной конфигурации достаточно одного kv_mount:

vault:
addr: "https://vault.internal:8200"
auth:
method: approle
role_id: keeper-prod
secret_id_file: /etc/keeper/vault-secret-id
kv_mount: "secret" # путь mount-а; v1 или v2 — определяется автоматически
pki_mount: "pki/soulstack"

Симметрия распространяется и на ссылки на секреты. vault:-ref пишется mount-relative и без сегмента data/ — путь одинаков для обеих версий, сегмент data/ (нужный только KV v2) клиент подставляет сам:

postgres:
dsn_ref: vault:secret/keeper/postgres # берёт поле dsn
redis:
password_ref: vault:secret/keeper/redis#password # конкретное поле через #
auth:
jwt:
signing_key_ref: vault:secret/keeper/jwt-signing-key

То же правило — для явного чтения секрета в сценарии через core.vault.kv-read: path указывается без data/ и работает на обеих версиях одинаково.

- name: Read DB credentials from Vault
on: keeper
module: core.vault.kv-read
register: db_creds
params:
path: secret/redis/admin # без data/; работает и на v1, и на v2
fields: [username, password] # опционально; без — вернётся весь payload

Единственное место, где версия появляется в конфиге, — опциональный override kv_version. Он нужен только для захардненного Vault, где политика закрывает служебный probe-endpoint sys/internal/ui/mounts и автоопределение не проходит:

vault:
# ... остальной блок ...
kv_mount: "secret"
kv_version: "1" # явно фиксируем версию, раз авто-определение недоступно

В норме строки kv_version в конфиге нет: поле опционально, default — auto (см. таблицу полей vault выше).

JWT-аутентификация операторов для Operator API и MCP. Блок отвечает только за подпись и формат токенов; реестр операторов и каталог ролей живут в PostgreSQL и управляются через API.

auth:
jwt:
signing_key_ref: vault:secret/keeper/jwt-signing-key
issuer: keeper-eu-west-01
ttl_default: 24h
ttl_bootstrap: 720h # 30 дней
ПолеТипDefaultНазначение
auth.jwt.signing_key_refvault-refvault:secret/keeper/jwt-signing-keyVault KV-путь до ключа подписи операторских токенов.
auth.jwt.issuerstring<kid>Значение claim iss в выпускаемых токенах. По умолчанию — kid инстанса; можно задать единое имя на кластер.
auth.jwt.ttl_defaultduration24hTTL обычных операторских токенов. Короткий TTL — естественная защита (отозванный оператор перестаёт действовать с истечением токена).
auth.jwt.ttl_bootstrapduration720h (30 дней)TTL первого bootstrap-токена, выпускаемого keeper init.

У агента блока auth нет — он аутентифицируется к Keeper-у по mTLS (см. soul.yml). JWT — только для операторов.

Опциональный блок защиты /metrics (сам bind-адрес — listen.metrics.addr). При отсутствии блока эндпоинт обслуживается без аутентификации.

metrics:
auth:
basic:
enabled: true
username: scrape
password_ref: vault:secret/keeper/metrics-password
ПолеТипDefaultНазначение
metrics.auth.basic.enabledboolfalseВключить HTTP Basic-auth на /metrics.
metrics.auth.basic.usernamestringИмя пользователя. Обязательно при enabled: true.
metrics.auth.basic.password_refvault-refVault-ссылка на пароль (поле password в KV). Plaintext запрещён. Обязательно при enabled: true.

Пароль сравнивается constant-time и не попадает в логи. У агента симметричной защиты через Vault нет (у soul нет Vault-клиента) — там пароль задаётся файлом, см. soul.yml → metrics.

otel:
enabled: true
exporter: otlp
endpoint: "otel-collector.example.com:4317"

OpenTelemetry — push: при включении Keeper сам отправляет трейсы в указанный OTLP-коллектор. Входящего listener-порта для OTel у Keeper нет.

ПолеТипDefaultНазначение
otel.enabledboolfalseВключить экспорт.
otel.exporterenum{otlp}otlpФормат экспорта.
otel.endpointstring(host:port)Адрес OTLP-коллектора (gRPC). Обязателен при enabled: true.
otel.export_metricsboolfalseОпц. push метрик по OTLP в дополнение к Prometheus-scrape. На текущем этапе экспортируются только трейсы; по умолчанию метрики идут через Prometheus-/metrics.
logging:
level: info
format: json
file: /var/log/keeper/keeper.log # пусто → stderr без ротации
rotation:
max_size_mb: 100
max_age_days: 7
max_files: 10
compress: true

Поведение зависит от logging.file:

  • не задан → вывод в stderr без ротации (удобно под systemd/journald и в контейнере);
  • задан → запись в файл со встроенной ротацией (без зависимости от внешнего logrotate), архивы рядом по шаблону <file>-<timestamp>.<ext>.
ПолеТипDefaultНазначение
logging.levelenum{debug,info,warn,error}infoУровень логирования.
logging.formatenum{json,text}jsonjson для машинной обработки, text для человека.
logging.filepath— (stderr)Путь к лог-файлу. Пусто — stderr без ротации.
logging.rotation.max_size_mbint (МБ)100Порог ротации одного файла.
logging.rotation.max_age_daysint (≥0)7Сколько дней хранить архив.
logging.rotation.max_filesint10Сколько архивов держать.
logging.rotation.compressbooltrueСжимать ли архивы.

Поля logging.rotation.* применяются только когда задан logging.file.

Каталог плагинов с host = Keeper: CloudDriver-плагины (для cloud-провижининга) и SshProvider-плагины (для push по SSH). Keeper резолвит их из git-репозиториев в локальный кеш при старте.

plugins:
cache_root: /var/lib/soul-stack-keeper/plugins
work_root: /var/lib/soul-stack-keeper/plugin-src
fetch_timeout: 120s
max_artifact_size_mb: 256
max_clone_size_mb: 1024
cloud_drivers:
- { name: aws, source: "git@github.com:example/soul-cloud-aws.git", ref: v2.0.0 }
ssh_providers:
- { name: vault-ssh, source: "git@github.com:example/soul-ssh-vault.git", ref: v1.0.0 }
ПолеТипDefaultНазначение
plugins.cache_rootpath (абс.)/var/lib/soul-stack-keeper/pluginsКеш собранных артефактов плагинов.
plugins.work_rootpath (абс.)/var/lib/soul-stack-keeper/plugin-srcКорень рабочих git-клонов резолвера. Должен быть вне cache_root.
plugins.fetch_timeoutduration120sПотолок одной цепочки git-операций резолва плагина.
plugins.max_artifact_size_mbint (МиБ, ≥1)256Потолок размера одного извлекаемого исполняемого файла плагина (защита диска от враждебного репозитория). Превышение — fail-closed.
plugins.max_clone_size_mbint (МиБ, ≥1)1024Потолок размера рабочего дерева клона. Превышение — fail-closed.
plugins.cloud_drivers[].name / .source / .refstring / git-URL / git-refИмя провайдера, git-репозиторий плагина и его ref (tag или branch).
plugins.ssh_providers[].name / .source / .refstring / git-URL / git-refТо же для SSH-провайдеров push.

Версия артефакта — это git ref (tag или branch), а не поле в манифесте; semver-range не используется.

Lifecycle host-процесса плагинов на Keeper-стороне: таймауты handshake-а и остановки, whitelist capabilities, политика конфликтов ресурсов.

plugin_runtime:
socket_dir: /var/run/soul-stack-keeper/plugins
startup_timeout: 10s
shutdown_grace: 10s
allowed_capabilities:
- run_as_root
- network_outbound
- network_inbound
- vault_access
- fs_write_root
- exec_subprocess
conflict_policy: warn
enable_tls: false
ПолеТипDefaultНазначение
plugin_runtime.socket_dirpath/var/run/soul-stack-keeper/plugins/Каталог Unix-domain socket-ов плагинов (mode 0700, владелец — служебный пользователь Keeper-а).
plugin_runtime.startup_timeoutduration10sВремя от запуска плагин-процесса до handshake. Превышение — SIGTERM, затем SIGKILL.
plugin_runtime.shutdown_graceduration10sОкно от SIGTERM до SIGKILL при остановке плагина.
plugin_runtime.allowed_capabilitieslist<enum>все 6Whitelist возможностей плагина. Линтер отвергает Destiny до запуска, если плагин запрашивает capability вне списка. По умолчанию разрешены все шесть; сужайте по политике безопасности.
plugin_runtime.conflict_policyenum{warn,fail}warnЧто делать, если два плагина в одном прогоне претендуют на один ресурс: warn — записать аудит и продолжить, fail — пометить шаг неуспешным.
plugin_runtime.enable_tlsboolfalsemTLS на plugin-сокете. В текущем релизе — только false; безопасность обеспечивается правами 0700 на сокет.
audit:
enabled: true
otel_export: true
retention_days: 365
ПолеТипDefaultНазначение
audit.enabledbooltrueГлобальный выключатель аудита. При false write-path не пишет в PostgreSQL. В проде держите true (compliance).
audit.otel_exportbooltrueДублировать аудит-событие в OTel span как атрибут (источник правды — PostgreSQL).
audit.retention_daysint (≥1)365Срок хранения записей аудита (дней). Чистка выполняется фоновой задачей.

У агента блока audit нет: события Soul-стороны (отчёты о прогонах) идут через Keeper и пишутся им.

Фоновая задача очистки PostgreSQL от просроченных записей (один лидер на кластер, выбирается через Redis-lease).

reaper:
enabled: true
interval: 1h
dry_run: false
batch_size: 500
lock_ttl: 5m
rules:
expire_pending_seeds: { enabled: true, max_age: 24h, action: delete }
mark_disconnected: { enabled: true, stale_after: 90s, action: set_status, target_status: disconnected }
purge_audit_old: { enabled: true, max_age: 365d, action: delete }
# … остальные правила
ПолеТипDefaultНазначение
reaper.enabledbooltrueВключить очистку.
reaper.intervalduration1hИнтервал прохода.
reaper.dry_runboolfalseСухой прогон без мутаций (для проверки).
reaper.batch_sizeint500Размер батча одного прохода.
reaper.lock_ttlduration5mTTL Redis-lease на лидерство.
reaper.rulesmap<string, object>Предопределённые правила чистки (просроченные токены онбординга, отметка отвалившихся агентов, удаление старого аудита и т. п.). Поля правила зависят от его action.

Опциональный блок планировщика регулярных запусков (один лидер на кластер). При отсутствии — поднимается с дефолтами.

cadence_scheduler:
enabled: true # опущено → ON по умолчанию; false → выключить
poll_floor: 30s
poll_ceiling: 60s
poll_idle: 120s
lock_ttl: 5m
ПолеТипDefaultНазначение
cadence_scheduler.enabledbool (tri-state)nil → ONВключение планировщика. Опущено/null → ON (по умолчанию включён); false → выключить; true → включить. Читается на старте.
cadence_scheduler.poll_floorduration30sНижняя граница шага опроса. Абсолютный минимум — 30s.
cadence_scheduler.poll_ceilingduration60sВерхняя граница шага опроса. Должна быть ≥ poll_floor.
cadence_scheduler.poll_idleduration120sШаг опроса при пустом реестре расписаний. Должен быть ≥ poll_ceiling.
cadence_scheduler.lock_ttlduration5mTTL Redis-lease на лидерство планировщика.

Опциональный per-оператор rate-limiter тяжёлых write-эндпоинтов. При отсутствии блока — включён с дефолтами.

tempo:
enabled: true
voyage_create: # лимит создания батчевых прогонов
rate: 10 # токенов в секунду
burst: 20 # допустимый всплеск
voyage_preview: # лимит предпросмотра (мягче — без записи)
rate: 30
burst: 60
ПолеТипDefaultНазначение
tempo.enabledbool (tri-state)trueВключить лимитер. Опущено/null → включён.
tempo.voyage_create.ratefloat10Скорость пополнения токенов на создание прогона (запросов в секунду).
tempo.voyage_create.burstint20Допустимый всплеск создания.
tempo.voyage_preview.ratefloat30Скорость пополнения на предпросмотр (мягче — операция без записи).
tempo.voyage_preview.burstint60Допустимый всплеск предпросмотра.

Top-level тоггл встроенного операторского web-UI на маршруте /ui. Реальный UI включён в исполняемый файл keeper и отдаётся им из коробки — отдельного процесса, порта или backend-а не требует; статика садится на уже существующий Operator-API-listener (listen.openapi.addr, обычно :8080). Подробнее о доступе и работе с UI — Web-интерфейс.

# web_ui_enabled: true # дефолт (опущено / null → ON); false — opt-out
ПолеТипDefaultНазначение
web_ui_enabled*bool (tri-state)trueМонтировать ли встроенный UI на /ui. Опущено / nulltrue (default-ON, single-binary UI «из коробки»); явный falseopt-out: статика /ui не монтируется, /v1/* и /docs не затрагиваются. В отличие от tempo / cadence_scheduler от инфраструктуры не зависит — UI вшит в исполняемый файл, внешнего backend-а не нужно.

HA-инвариант: воркер-пул при нескольких инстансах

Заголовок раздела «HA-инвариант: воркер-пул при нескольких инстансах»

В кластере из нескольких живых инстансов исполнение прогонов должно идти через общий воркер-пул, а не привязываться к памяти одного инстанса. Этим управляет ключ acolytes:

acolytes: 4 # число воркеров пула исполнения на инстанс; 0 — пул выключен
ПолеТипDefaultНазначение
acolytesint (≥0)0Число воркеров пула исполнения прогонов на инстанс. 0 — пул не поднимается, исполнение идёт «классическим» путём в памяти инстанса-владельца.

Что это. acolytes: N — число фоновых воркеров-«аколитов» в одном инстансе Keeper, которые разбирают общую очередь прогонов (apply-заданий) из PostgreSQL и исполняют их. Значение считается per-инстанс; суммарная параллельность кластера = acolytes × число живых инстансов.

С чем масштабируется. С числом и шириной одновременно идущих прогонов, не с числом душ: 10000 агентов — это не 10000 аколитов. Агенты просто висят на стримах и сами по себе аколита не занимают; аколит активен, только когда в очереди есть задания запущенного прогона. Один прогон на 50 хостов = 50 заданий в очереди, и их параллельно разгребают все живые аколиты всех инстансов кластера.

Сколько ставить — стартовые числа:

Топологияacolytes (на инстанс)Почему
Одиночный Keeper0Штатный дефолт: прогон исполняет сам принявший его инстанс. Для одного узла корректно и безопасно.
HA из 2–3 инстансов10Разумный старт: половина дефолтного postgres.pool.max (20) с запасом; даёт 20–30 параллельных заданий по кластеру. acolytes > 0 здесь обязателен (см. HA-инвариант).
Высокая нагрузка20+Только если под реальной нагрузкой очередь planned-заданий устойчиво растёт и apply задерживаются — широкие прогоны на сотни хостов или много одновременных запусков.

Потолок и связь с postgres.pool.max. Держите acolytes ≤ postgres.pool.max − запас (~8–10). Каждый воркер берёт PostgreSQL-коннекты из общего пула, и за те же коннекты с ним конкурируют Operator API, Reaper и обработка Voyage. Если воркеров больше, чем коннектов в пуле, — лишние просто ждут свободный коннект (голодание, рост латентности), а параллелизма не прибавляется. Нужно больше воркеров — сначала поднимите postgres.pool.max, потом acolytes. Жёсткого верхнего предела у самого acolytes в конфиге нет (валидируется только ≥ 0); фактический потолок задаёт размер PG-пула.

hot_reload:
enable_signal: true
enable_inotify: false
audit_correlation_id: true

Управляет триггерами hot-reload. Блок опционален; при отсутствии — дефолты из таблицы. Все три поля требуют перезапуска при изменении (они контролируют сам механизм reload).

ПолеТипDefaultНазначение
hot_reload.enable_signalbooltrueВключить SIGHUP-триггер: процесс перечитывает keeper.yml с диска, валидирует и атомарно подменяет.
hot_reload.enable_inotifyboolfalseАвто-reload по изменению файла (Linux-only). По умолчанию выключено.
hot_reload.audit_correlation_idbooltrueГенерировать correlation-id для аудит-событий reload.

Конфиг можно перечитывать без полного перезапуска процесса. Два пути:

  • File-edit — оператор правит keeper.yml на хосте и шлёт процессу SIGHUP. Pipeline: parse → валидация → атомарная подмена → аудит.
  • API/MCP — мутация конфига через Operator API; дополнительно к подмене происходит write-back: изменённое значение записывается обратно в keeper.yml (с сохранением комментариев и порядка ключей, атомарной заменой файла).

Любая ошибка валидации оставляет текущее состояние нетронутым — файл не модифицируется.

Что применяется без перезапуска: уровень логирования, параметры пула PostgreSQL, интервалы и правила фоновых задач, пороги rate-limiter-а, политики плагинов (whitelist capabilities, conflict-policy, таймауты новых запусков).

Что требует перезапуска: адреса listener-ов и TLS-сертификаты, DSN PostgreSQL и параметры Redis, адрес/auth Vault, ключ подписи токенов, пути лог-файлов и параметры ротации, OTel-экспортёр, поднятие/гашение целых подсистем (планировщик, воркер-пул).

Каждый инстанс кластера перечитывает свой конфиг независимо — централизованной cross-host синхронизации reload в текущем релизе нет.

  • RBAC (роли, привязки операторов, permissions) — в PostgreSQL, управление через API. Ключ rbac: в keeper.yml отвергается.
  • Реестр сервисов и связанные well-known значения — в PostgreSQL, управление через API. Ключи services: / default_destiny_source: в keeper.yml отвергаются.

Минимальный валидный keeper.yml со всеми обязательными полями:

kid: keeper-eu-west-01
listen:
grpc:
bootstrap:
addr: "0.0.0.0:9442"
tls:
cert: /etc/keeper/tls/server.crt
key: /etc/keeper/tls/server.key
event_stream:
addr: "0.0.0.0:8443"
tls:
cert: /etc/keeper/tls/server.crt
key: /etc/keeper/tls/server.key
ca: /etc/keeper/tls/ca.crt
openapi: { addr: "0.0.0.0:8080" }
mcp: { addr: "0.0.0.0:8081" }
metrics: { addr: "0.0.0.0:9090" }
postgres:
dsn_ref: vault:secret/keeper/postgres
pool: { min: 5, max: 50 }
redis:
addr: "redis.example.com:6379"
password_ref: vault:secret/keeper/redis
vault:
addr: "https://vault.example.com:8200"
auth:
method: approle
role_id: keeper-prod
secret_id_file: /etc/keeper/vault-secret-id
pki_mount: "pki/soulstack"
auth:
jwt:
signing_key_ref: vault:secret/keeper/jwt-signing-key
issuer: keeper-eu-west-01
ttl_default: 24h
ttl_bootstrap: 720h
otel:
enabled: true
exporter: otlp
endpoint: "otel-collector.example.com:4317"
logging:
level: info
format: json
rotation: { max_size_mb: 100, max_files: 10, compress: true }
audit:
enabled: true
otel_export: true
retention_days: 365
reaper:
enabled: true
interval: 1h
batch_size: 500
lock_ttl: 5m
rules:
expire_pending_seeds: { enabled: true, max_age: 24h, action: delete }
mark_disconnected: { enabled: true, stale_after: 90s, action: set_status, target_status: disconnected }
purge_audit_old: { enabled: true, max_age: 365d, action: delete }
# Для HA-кластера (несколько инстансов) задайте воркер-пул:
# acolytes: 4