textmachine/platform/deploy/README.md

12 KiB
Raw Blame History

Развёртывание платформы

Одна VM, systemd, бинари артефактами CI (PLATFORM_DIRECTION.md §3). Не Kubernetes: дети-tmctl живут часами и держат эксклюзивный лок на файлах книги на локальном диске — оркестратор, способный переселить под посреди прогона, этой нагрузке враждебен.

Файлы

  • tmplatformd.service — юнит контрольной панели. Тело юнита проверено systemd-analyze verify (systemd 259) — exit 0, без замечаний. ⚠ Проверять надо с ПОДСТАВЛЕННЫМ существующим ExecStart=: дословно юнит даёт exit 1, потому что verify проверяет и наличие бинаря, а /usr/local/bin/tmplatformd на стенде нет. ⚠ юнит целиком под systemd не запускался — нет sudo и нет бинаря на стенде.

    Отдельные свойства песочницы при этом проверены ЖИВЫМ прогоном в пользовательском systemd 259 (systemd-run --user --wait), а не вычитаны из доки: ProtectSystem=strict + ReadWritePaths= на несуществующем пути → 226/NAMESPACE, юнит не стартует; тот же путь созданным → 0/SUCCESS; префикс - на несуществующем → 0/SUCCESS (строка игнорируется — потому мы её и не префиксуем); ProtectHome=yes/home пуст, чтение /home/<user> даёт Permission denied; ProtectHome=tmpfs + BindPaths=<каталог> → каталог виден. Ресурсные потолки (MemoryMax=80%, OOMPolicy=continue) живьём не проверялись — вывод из systemd.resource-control(5)/systemd.service(5).

Откат релиза: не ниже версии 5

goose down до версии 4 и ниже НЕ РАБОТАЕТ на живой базе: down-путь 00005 восстанавливает users_email_key и email NOT NULL, а обе формы нарушают строки, которые пишет боевой код (неподтверждённая личность даёт email = NULL; один адрес законно принадлежит двум аккаунтам). Откат транзакционный, поэтому падение ничего не портит — но планировать откат ниже 5 нельзя, план отката — накатить вперёд. Разбор: docs/STACK_DECISIONS.md §8.

Что юнит закрывает содержательно

  • ПЕРЕ-ДИСПОЗИЦИЯ (P4, D39.106): прогон НЕ ребёнок этого юнита и не живёт в его cgroup. Каждый прогон — транзиентный юнит в СОБСТВЕННОМ systemd-менеджере пользователя tmplatform, в срезе tm-runs.slice, потому что перевод обязан пережить деплой платформы. Следствия для установки: обязателен loginctl enable-linger tmplatform; юниту нужен доступ к своей же пользовательской шине (ProtectHome=tmpfs + BindPaths=/run/user/%U + DBUS_SESSION_BUS_ADDRESS); ресурсные потолки НИЖЕ ограничивают только контрол-плейн.
  • PD-13 (осиротевшие процессы движка) закрыт теперь ТАМ, где прогон: собственным cgroup прогона (TM_PLATFORM_RUN_MEMORY_MAX/_TASKS_MAX), и это ИЗМЕРЕНО — в дефолтном app.slice пользовательского менеджера лимиты принимаются и не применяются (STACK_DECISIONS §16). Дев-путь супервизора с группой процессов остаётся дев-путём.
  • TimeoutStopSec=90 больше, чем дренаж платформы (15 с) плюс grace движка (30 с). Меньше — и systemd прибьёт tmctl посреди остановки, оставив лок проекта.
  • Секреты через LoadCredential=, а не через окружение: переменная окружения видна в /proc/<pid>/environ и наследуется каждым ребёнком-tmctl. Конфиг читает *_FILE первым.

Установка (набросок, исполняется владельцем)

useradd --system --home-dir /srv/textmachine tmplatform
# ⚠ Каталог создаём ЯВНО: --system не создаёт домашний каталог, а `ReadWritePaths=/srv/textmachine`
# при `ProtectSystem=strict` на несуществующем пути валит сборку mount-namespace — юнит не стартует
# вовсе (PD-91, systemd.exec(5)). Это первая команда, которую пропускают, читая набросок сверху вниз.
install -d -m0750 -o tmplatform -g tmplatform /srv/textmachine
install -D -m0755 tmplatformd  /usr/local/bin/tmplatformd
install -D -m0755 tmplatformctl /usr/local/bin/tmplatformctl
# ⚠ БЕЗ ЭТОГО НИ ОДИН ПРОГОН НЕ СТАРТУЕТ. Прогоны — транзиентные юниты в пользовательском
# менеджере `tmplatform`, а он существует вне сессии входа только при включённом linger.
# Проверка: `systemctl --user -M tmplatform@ is-system-running` отвечает, а не «Failed to connect».
loginctl enable-linger tmplatform
# Движок кладётся по ВЕРСИОНИРОВАННОМУ пути: попытка прогона пиннится к тому, с которого началась
# (строка 139), поэтому выкат новой версии не подменяет исполняемое у идущего прогона.
install -D -m0755 tmctl /opt/textmachine/engine/<версия>/tmctl
install -d -m0700 -o root -g root /etc/tmplatform
printf '%s' 'postgres://...' > /etc/tmplatform/dsn && chmod 0400 /etc/tmplatform/dsn
printf '%s' '<oauth client secret>' > /etc/tmplatform/oidc_client_secret && chmod 0400 /etc/tmplatform/oidc_client_secret

/etc/tmplatform/env — несекретное окружение:

TM_PLATFORM_ADDR=127.0.0.1:8080
TM_PLATFORM_TRUSTED_ORIGINS=https://app.example.org
TM_PLATFORM_OIDC_ISSUER=https://accounts.google.com
TM_PLATFORM_OIDC_CLIENT_ID=...
TM_PLATFORM_OIDC_REDIRECT_URL=https://app.example.org/auth/callback
TM_PLATFORM_AFTER_LOGIN=/library
TM_PLATFORM_SIGNUP_GRANT_USD=5
TM_PLATFORM_BOOKS_DIR=/srv/textmachine/books
TM_PLATFORM_METRICS_ADDR=127.0.0.1:9464

TM_PLATFORM_BOOKS_DIR — каталог, который сервис ПИШЕТ и, на отклонённом интейке, УДАЛЯЕТ. Всё под ним создано загрузкой книг; книга, заведённая дев-инструментом с чужим --workdir, туда не попадает и не удаляется никогда (гард books.owns). Каталог должен существовать и принадлежать tmplatform, и он обязан быть в ReadWritePaths= юнита — при ProtectSystem=strict отсутствующий путь валит сборку mount-namespace целиком (PD-91, та же грабля, что и с /srv/textmachine):

install -d -m0750 -o tmplatform -g tmplatform /srv/textmachine/books

/metrics не аутентифицированего защищает только адрес привязки. Дефолт 127.0.0.1, то есть снаружи он недостижим; поднять его на внешний адрес значит открыть операционную форму деплоя (глубина очереди, число прогонов, возраст холдов) всем, кто до него дотянется. Скрейпер живёт на том же хосте либо ходит через тот же edge, что и API. Разбор — docs/STACK_DECISIONS.md §24, строка риска — PD-179.

Загрузка книги идёт минуты, и это касается edge-прокси. Маршрут POST /v0/books принимает до TM_PLATFORM_MAX_UPLOAD_BYTES (64 МиБ) и держит соединение до TM_PLATFORM_UPLOAD_DEADLINE (10 минут). Прокси перед сервисом обязан разрешать столько же: у него свои client_max_body_size и свои таймауты чтения, и молчаливо режет их он, а не мы.

TM_PLATFORM_STATE_DIR меняется только когда живых прогонов нет. Exit-маркер пишется по пути, вычисленному при СПАВНЕ, а читается по пути из текущей конфигурации: после смены каталога конец идущего прогона становится невидим, и реконсилятор перезапускает его как потерянный (PD-155). Путь обязан быть абсолютным — относительный демон отвергает на старте, потому что маркер пишет юнит из каталога книги, а читает демон из своего.

Миграции выкатываются ОДИН раз, не каждой репликой: TM_PLATFORM_MIGRATE=1 tmplatformd разово либо отдельный шаг деплоя. goose держит advisory-лок, так что параллельный запуск не гонка, но и не норма.

Где на сервере лежат книги

/srv/textmachine — корень библиотеки НА СЕРВЕРЕ. Решение владельца «книги живут в ~/books» относится к машине разработки: под этим юнитом домашние каталоги недоступны вовсе (ProtectHome=yes подставляет пустой /home и детям-tmctl тоже), поэтому библиотека под домашним каталогом на сервере просто не откроется — не «сработает медленнее», а не найдётся. Оператору, которому это нужно, юнит называет ровно две строки замены (ProtectHome=tmpfs + BindPaths=); других изменений не требуется.

⚠ Выбирающего путь кода ещё нет: Supervisor.Workdir задаёт вызывающий, а вызывающий — воркер, которого нет (строка 103 единого бэклога). Когда он появится, корень становится настройкой, и её дефолт — этот каталог.

Чего здесь ещё нет

TLS и домен (перед юнитом предполагается edge-прокси), ограничитель соединений на edge, ротация логов, бэкап Postgres. Всё это — работа с первым реальным деплоем, не раньше.