fix, feat, doc: massive build system refactorion, bug fixes and apex documentation (#19)

This commit is contained in:
deytenit authored and GitHub committed 2026-07-12 01:57:40 +03:00
1 parent e8b42c60c2
commit 91f5245823
165 files changed
+8598 -1205

No files matched your search

+2 -1
View File
@@ -7,6 +7,7 @@ title: 'Документация ermnvldmr.com'
Добро пожаловать в единую документацию для моих собственных проектов
{{< cards >}}
{{< card link="root" title="ermnvldmr.com-root-commons" icon="server" subtitle="Документация общих модулей корневой инфраструктуры ermnvldmr.com" >}}
{{< card link="/en/apex" title="apex.ermnvldmr.com" icon="server" subtitle="Fleet operations framework (English only)" >}}
{{< card link="quetza" title="Quetza Bot" icon="terminal" subtitle="Документация модульного фреймворка Discord-бота" >}}
{{< card link="root" title="ermnvldmr.com-root-commons (deprecated)" icon="archive" subtitle="Устарело — заменено фреймворком apex" >}}
{{< /cards >}}
+37
View File
@@ -0,0 +1,37 @@
---
title: Документация фреймворка операций флота apex
description: "Документация фреймворка операций флота apex: движок на stdlib-python и общее ядро compose для self-hosted узлов."
weight: 1
type: docs
cascade:
type: docs
---
`apex` — это [commons](/apex/glossary#commons) (общий репозиторий) флота self-hosted [узлов](/apex/glossary#node): [движок](/apex/glossary#engine) на python3-stdlib, который запускает сгруппированные [действия](/apex/glossary#action) как `apex <group>/<action>`, в паре с общим, параметризуемым через `${env}` ядром docker-compose. Каждый репозиторий узла подключает фреймворк как закреплённый git-субмодуль `commons/`. apex приходит на смену bash-фреймворку commons под названием `root`.
Стек намеренно минимален: `git` плюс стандартная библиотека Python 3. Здесь нет ни `pip`, ни Node, ни Go, ни CUE, а compose-файлы написаны вручную — без кодогенерации.
## Основные возможности
- **Единая точка входа в локальном чекауте** — `apex` всегда действует на том чекауте, из которого он запущен; аргумент `<node>` передавать не нужно.
- **Явная идентичность из `node.env`** — [идентичность узла](/apex/glossary#node-identity) объявляется для каждого узла в `node.env` (`APEX_NODE_FQDN`, `APEX_NODE_HOST`, `APEX_SUBNET`); фреймворк не предполагает какого-либо конкретного домена, а короткое `name` узла — это просто первая метка хоста.
- **Репозитории узлов из четырёх опор** — каждый репозиторий узла имеет одинаковую структуру: закреплённый субмодуль `commons/`, локальные [проприетарные модули узла](/apex/glossary#proprietaries), его [композиции](/apex/glossary#compositions) и `node.env`.
- **Проприетарный оверлей** — локальные проприетарные модули узла [накладываются](/apex/glossary#overlay) поверх общих действий; commons содержит только то, что разделяют все узлы.
- **Многоуровневое хранилище с изоляцией привилегий** — стандартизированные [уровни](/apex/glossary#tier) хранилища с изоляцией привилегий на уровне проекта.
- **Общая базовая композиция** — [базовая композиция](/apex/glossary#core-composition) (`compositions/apex/docker-compose.yml`) параметризуется через `${env}` и включается в композиции каждого узла.
## Разделы
{{% cards %}}
{{< card link="getting-started" title="Начало работы" icon="map" subtitle="Предварительные требования, начальная настройка и первые действия на новом узле." >}}
{{< card link="concepts" title="Концепции" icon="book-open" subtitle="Архитектура, идентичность, оверлей и модель хранилища." >}}
{{< card link="guides" title="Руководства" icon="map" subtitle="Практические руководства по эксплуатации и расширению узла." >}}
{{< card link="reference" title="Справочник" icon="code" subtitle="Действия, вспомогательная библиотека движка и коды выхода." >}}
{{< card link="troubleshooting" title="Устранение неполадок" icon="shield-check" subtitle="Симптомы, диагностика и исправление типовых сбоев." >}}
{{% /cards %}}
---
**См. также:**
- [GitHub: apex.ermnvldmr.com](https://github.com/deytenit/apex.ermnvldmr.com)
@@ -0,0 +1,25 @@
---
title: Концепции
description: "Архитектура, идентичность, действия, контекст выполнения, композиции, хранилище, адресация и безопасность."
weight: 10
---
Теоретические основы фреймворка операций флота apex: как [узел](/apex/glossary#node) объявляет свою [идентичность](/apex/concepts/node-identity), как выполняются [действия](/apex/glossary#action) и как узел организует свои сервисы, хранилище и адресацию.
{{% cards %}}
{{< card link="architecture" title="Архитектура" icon="share" subtitle="Слои движка, commons и configs и как они сочетаются друг с другом." >}}
{{< card link="node-identity" title="Идентичность узла" icon="finger-print" subtitle="Как узел объявляет, кто он и где ему место." >}}
{{< card link="actions-and-overlay" title="Действия и оверлей" icon="lightning-bolt" subtitle="Общие действия и локальные переопределения узла." >}}
{{< card link="execution-context" title="Контекст выполнения" icon="terminal" subtitle="Окружение, которое каждое действие получает во время выполнения." >}}
{{< card link="compositions" title="Композиции" icon="cube" subtitle="Как стеки сервисов объявляются и разворачиваются на каждом узле." >}}
{{< card link="tiered-storage" title="Многоуровневое хранилище" icon="database" subtitle="Стандартизированная классификация хранилища для данных сервисов." >}}
{{< card link="addressing" title="Адресация" icon="globe-alt" subtitle="Подсеть узла, якорные IP и соглашение об анклаве." >}}
{{< card link="security-model" title="Модель безопасности" icon="shield-check" subtitle="Изоляция привилегий и усиление защиты сети." >}}
{{% /cards %}}
---
**См. также:**
- [Руководства: Эксплуатация флота apex](/apex/guides)
- [Справочник: Команды и интерфейсы](/apex/reference)
@@ -0,0 +1,111 @@
---
title: "Действия и оверлей"
description: "Контракт действия и как узло-локальные проприетарии затеняют действия commons."
weight: 13
---
Каждая единица автоматизации в apex — это [действие](/apex/glossary#action): один Python-модуль, который [движок](/apex/glossary#engine) диспетчеризует по имени. [Оверлей](/apex/glossary#overlay) позволяет [проприетариям](/apex/glossary#proprietaries) узла добавлять, переопределять или отключать действия [«commons»](/apex/glossary#commons), не форкая их. Эта страница определяет контракт действия, декларативные дескрипторы аргументов и правила разрешения.
## Контракт действия {#contract}
Действие — это один файл `.py` в дереве `actions/`. Путь к файлу **является** именем действия: `actions/<group>/<action>.py` вызывается как `apex <group>/<action> [args...]`. Нет реестра, который нужно обновлять — размещение файла в дереве публикует действие.
Модуль должен предоставлять две вещи:
- **`METADATA = Meta(...)`** &rarr; краткое описание и дескрипторы аргументов, которые управляют как текстом справки, так и парсингом. Система дескрипторов пришла на смену heredoc-у `action_metadata()` bash-фреймворка «root» commons, предшественника apex.
- **`run(ctx, args)`** &rarr; точка входа. `ctx` — это построенный движком контекст выполнения; `args` — распарсенное пространство имён `argparse`.
```python {filename="actions/<group>/<action>.py"}
from engine.descriptor import Meta, Arg, Flag
METADATA = Meta(
summary="Однострочное описание, показываемое в перечислении действий",
args=[
Arg("target", help="Обязательный позиционный аргумент"),
Flag("--dry-run", help="Булев флаг"),
],
)
def run(ctx, args):
...
return 0
```
Возвращаемое значение `run` становится кодом завершения процесса. Нецелочисленный возврат (включая `None`) трактуется как успех (`0`). Необработанное исключение логируется с префиксом контекста и завершается с кодом `1`.
> [!NOTE]
> `METADATA` технически необязательна — модуль без неё диспетчеризуется с пустым `Meta` и не принимает аргументов — но каждое настоящее действие должно её объявлять, поскольку вывод в перечислении и `--help` генерируется из неё.
## Декларативные аргументы {#arguments}
`Meta.args` — это список дескрипторов. Каждый отображается ровно на одну конструкцию `argparse`:
| Дескриптор | Отображается на | Поведение |
| --- | --- | --- |
| `Arg("name")` | позиционный аргумент | Обязателен по умолчанию; `required=False` делает его опциональным позиционным (`nargs="?"`, по умолчанию `None`). |
| `Opt("--name", default=...)` | опция со значением | Опциональный флаг, принимающий значение, с объявленным значением по умолчанию. |
| `Flag("--name")` | булев флаг | `store_true`; наличие означает `True`. |
| `Rest("name")` | остаток | Жадная сквозная передача всех оставшихся argv (`argparse.REMAINDER`). |
Движок строит парсер как `apex <group>/<action>` и парсит оставшиеся argv перед вызовом `run`. `--help` обрабатывается на этапе парсинга. Ошибка использования — неизвестный флаг, отсутствующий позиционный аргумент — печатает usage и завершается с кодом `64` (`EX_USAGE`) вместо стандартного для argparse `2`.
## Перечисление без импорта {#listing}
`apex` без аргументов (или `--help`) перечисляет каждое доступное действие. Перечисление никогда не импортирует модули действий: движок статически сканирует каждый файл модулем `ast` и извлекает только краткое описание `METADATA` и любую константу `DISABLED`. Поэтому код действия не может выполниться — или упасть — во время перечисления; файл, который не удаётся распарсить, перечисляется как `(unparseable)`, а не ломает перечисление.
Каждая запись помечается своим источником оверлея:
- нет метки &rarr; действие commons,
- `(local)` &rarr; существует только в проприетариях,
- `(local override)` &rarr; действие проприетариев, затеняющее действие commons,
- `[DISABLED: reason]` &rarr; действие объявляет `DISABLED`.
## Оверлей {#overlay}
Имена действий разрешаются по двум деревьям, сначала проприетарии:
{{< filetree/container >}}
{{< filetree/folder name="node repository" state="open" >}}
{{< filetree/file name="node.env" >}}
{{< filetree/folder name="commons" state="open" >}}
{{< filetree/folder name="engine" state="closed" >}}{{< /filetree/folder >}}
{{< filetree/folder name="actions" state="open" >}}
{{< filetree/file name="<group>/<action>.py" >}}
{{< /filetree/folder >}}
{{< /filetree/folder >}}
{{< filetree/folder name="proprietaries" state="open" >}}
{{< filetree/folder name="actions" state="open" >}}
{{< filetree/file name="<group>/<action>.py" >}}
{{< /filetree/folder >}}
{{< /filetree/folder >}}
{{< /filetree/folder >}}
{{< /filetree/container >}}
Движок определяет эту схему по своему собственному расположению: когда он запускается из каталога `commons/`, родитель которого содержит `node.env`, родитель является корнем репозитория [узла](/apex/glossary#node), а `proprietaries/actions/` — деревом оверлея. Автономный checkout commons является собственным корнем и не имеет слоя оверлея. Внутри репозитория узла [идентичность узла](/apex/glossary#node-identity) разрешается из FQDN хоста перед диспетчеризацией.
Разрешение для заданного имени проверяет `proprietaries/actions/<name>.py` первым и `commons/actions/<name>.py` вторым; ровно один модуль загружается по пути. Неизвестное имя — это ошибка (код завершения `1`). Оверлей поддерживает четыре приёма:
- **Добавление (Add)** &rarr; файл, существующий только в проприетариях, становится узло-локальным действием, перечисляется как `(local)`.
- **Переопределение (Override)** &rarr; файл проприетариев по тому же относительному пути полностью затеняет реализацию commons; путь commons записывается в разрешении как `shadowed`.
- **Обёртывание (Wrap)** &rarr; переопределяющий модуль может делегировать реализации, которую он затеняет, через `ctx.commons.run(args)`, добавляя узло-локальное поведение до или после логики commons вместо её замены.
- **Отключение (Disable)** &rarr; модуль, присваивающий `DISABLED = "reason"`, отклоняется при диспетчеризации: движок логирует причину и завершается с кодом `1`. Перечисление показывает `[DISABLED: reason]`. Голый `DISABLED = True` также отключает, с обобщённой причиной `disabled`; пустая строка не отключает.
## Коды завершения {#exit-codes}
Действия и движок следуют фиксированному соглашению о кодах завершения:
| Код | Значение | Типичная причина |
| --- | --- | --- |
| `0` | успех | `run` вернул `0` или нецелочисленное значение. |
| `1` | общая ошибка | Неизвестное действие, отключённое действие, необработанное исключение, нецелочисленный `SystemExit`. |
| `64` | ошибка использования | Аргументы отклонены парсером (`EX_USAGE`). |
| `65` | ошибка данных | Некорректные входные данные (`EX_DATAERR`), поднимается библиотеками движка. |
| `66` | отсутствующий ввод | Требуемый ввод отсутствует (`EX_NOINPUT`), поднимается библиотеками движка. |
---
**См. также:**
- [Концепция: Архитектура фреймворка](/apex/concepts/architecture)
- [Концепция: Контекст выполнения](/apex/concepts/execution-context)
- [GitHub: apex.ermnvldmr.com](https://github.com/deytenit/apex.ermnvldmr.com)
@@ -0,0 +1,46 @@
---
title: "Схема адресации флота"
description: "Подсети /24 для каждого узла и статические якорные IP внутри пространства анклава (по умолчанию 198.18.0.0/16)."
weight: 17
---
Apex назначает каждому сервису фиксированный, предсказуемый IPv4-адрес внутри подсети узла. Каждый узел объявляет собственный `/24`, якоря сервисов — это статические адреса внутри него, а вся схема механически принудительно проверяется линтером якорных IP.
## Адресное пространство {#address-space}
Пространство [анклава](/apex/glossary#enclave) по умолчанию равно `198.18.0.0/16` — диапазону для бенчмаркинга, выделенному [RFC 2544: Benchmarking Methodology for Network Interconnect Devices](https://datatracker.ietf.org/doc/html/rfc2544). Этот диапазон никогда не маршрутизируется в публичном Интернете и не конфликтует с распространёнными частными диапазонами (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), которые уже используют облачные провайдеры и домашние сети. Оператор может переопределить его через `APEX_ENCLAVE_SUBNET`.
## Подсети узлов {#node-subnets}
Каждый [узел](/apex/glossary#node) объявляет ровно один `/24` как `APEX_SUBNET` в своём `node.env`, и эта подсеть является диапазоном IPAM сети `direct`. Оператор назначает её; по соглашению она располагается внутри пространства анклава (`198.18.0.0/16` по умолчанию или того, что задано в `APEX_ENCLAVE_SUBNET`), но фреймворк не накладывает на этот выбор никакой арифметики.
> [!NOTE]
> В качестве опционального соглашения оператора вы можете организовать `/24` узлов в непрерывные блоки (например, группируя связанные узлы), чтобы сохранять читаемость распределений. Это вопрос учёта, а не правило фреймворка.
## Распределение внутри `/24` узла {#node-allocation}
Внутри `/24` узла адреса хостов разделяются по ролям:
| Диапазон | Назначение |
|---|---|
| `.10`–`.19` | Зарезервировано для якорей [базовой композиции](/apex/glossary#core-composition); `traefik` всегда `.10` (через `APEX_TRAEFIK_DIRECT_IP`) |
| `.20` и выше | Якоря сервисов, распределяемые по `<project>` |
Каждый якорь сервиса — это статический `ipv4_address` в сети direct — фиксированный [якорный IP](/apex/glossary#anchor-ip), а не динамически назначаемая аренда. Распределения записываются в `compositions/ADDRESSING.md`, который служит человекочитаемым реестром подсети узла.
## Принудительная проверка {#enforcement}
Линтер compose проверяет якорные IP по всем [композициям](/apex/glossary#compositions) на узле. Для каждого `ipv4_address`, найденного в `compositions/*/docker-compose.yml`, он проверяет, что адрес:
- **Корректно сформирован** — некорректный литерал сообщается как ошибка линтера, а не крах.
- **Уникален** — один и тот же якорный IP не может встречаться в двух проектах; дубликаты сообщаются с указанием обоих конфликтующих проектов.
- **Внутри подсети узла** — каждый якорь должен попадать в объявленный `/24` узла.
Любое нарушение приводит к провалу линтера с кодом выхода `1` (общая ошибка). Успешное прохождение проверки логирует `Anchor IP validation passed.` Реализация находится в [действии](/apex/glossary#action) `utils/lint-docker-compose`; см. [GitHub: apex.ermnvldmr.com](https://github.com/deytenit/apex.ermnvldmr.com).
---
**См. также:**
- [GitHub: apex.ermnvldmr.com](https://github.com/deytenit/apex.ermnvldmr.com)
- [RFC 2544: Benchmarking Methodology for Network Interconnect Devices](https://datatracker.ietf.org/doc/html/rfc2544)
@@ -0,0 +1,78 @@
---
title: Архитектура фреймворка
description: Четыре опоры репозиториев узла, субмодуль «commons» и поток диспетчеризации.
weight: 11
---
apex разделяет автоматизацию флота на один общий репозиторий — [«commons» (общий репозиторий)](/apex/glossary#commons) — и по одному репозиторию на каждый [узел](/apex/glossary#node). «commons» несёт всё то, что общее для каждого узла: [движок](/apex/glossary#engine), общие [действия](/apex/glossary#action) и [ядровую композицию](/apex/glossary#core-composition). Репозиторий узла несёт всё то, что делает этот узел самим собой: его идентичность, хостовые [конфигурации](/apex/glossary#configs), сервисные [композиции](/apex/glossary#compositions) и узло-локальные [проприетарии](/apex/glossary#proprietaries).
## Четыре опоры {#pillars}
Каждый репозиторий узла имеет одинаковую структуру:
{{< filetree/container >}}
{{< filetree/folder name="<node>.<your-domain>" state="open" >}}
{{< filetree/folder name="commons" state="closed" >}}{{< /filetree/folder >}}
{{< filetree/folder name="proprietaries" state="closed" >}}{{< /filetree/folder >}}
{{< filetree/folder name="configs" state="closed" >}}{{< /filetree/folder >}}
{{< filetree/folder name="compositions" state="closed" >}}{{< /filetree/folder >}}
{{< filetree/file name="node.env" >}}
{{< filetree/file name="init.sh" >}}
{{< /filetree/folder >}}
{{< /filetree/container >}}
| Опора | Содержимое | Владение |
|---|---|---|
| `commons/` | Git-субмодуль → `apex.ermnvldmr.com`, закреплённый на релизном теге | Общее, версионируемое |
| `proprietaries/` | Узло-локальные действия, библиотеки и исходники инструментов ([накладываются оверлеем](/apex/glossary#overlay) поверх commons) | Узел |
| `configs/` | Исходники конфигурации хоста: `ufw/`, `cron/crontab`, `systemd/`, `crowdsec/` | Узел |
| `compositions/` | По одному каталогу на проект плюс ядровая обёртка `apex/` и реестр адресации | Узел |
Закрепление субмодуля — это версионный контракт фреймворка: узел выполняет ровно тот движок и ту ядровую композицию, которые записаны в его gitlink. Обновление узла — это осознанное действие: переключитесь на более новый тег внутри `commons/` и зафиксируйте перемещённый указатель.
## Поток диспетчеризации {#dispatch}
Лаунчер `commons/apex` действует на локальный checkout; аргумента узла нет нигде.
```mermaid
flowchart TD
A["apex &lt;group&gt;/&lt;action&gt; [args]"] --> B{Схема размещения}
B -- "каталог commons/ с node.env рядом" --> C[Checkout узла:<br/>оверлей = proprietaries/actions]
B -- иначе --> D[Автономный commons:<br/>без оверлея]
C --> E["Разрешение оверлея<br/>(сначала proprietaries)"]
D --> E
E --> F["Разрешение идентичности<br/>(FQDN + node.env)"]
F --> G["Построение ctx<br/>(log, sys, tpl, host, ufw,<br/>crowdsec, systemd, notify)"]
G --> H["разбор аргументов (Meta)"]
H --> I["run(ctx, args)"]
```
Движок определяет схему размещения по своему собственному расположению: запуск из каталога с именем `commons/`, родитель которого содержит `node.env`, означает «checkout узла» — родитель становится корнем репозитория, а `proprietaries/actions/` становится деревом оверлея. Любое другое расположение — это автономный checkout commons, выступающий в роли собственного корня.
Перечисление (`apex --help`) никогда не импортирует модули действий; краткие описания и маркеры отключённых действий извлекаются статическим сканированием через `ast`. Диспетчеризация загружает ровно один модуль по пути. Подробности в [Концепция: Действия и оверлей](/apex/concepts/actions-and-overlay).
## Модель «capture-up» {#capture-up}
Репозитории узлов — это реестры, а не просто источники: хост сам записывает то, что он выполняет.
1. Запланированный запуск `sync/repository` на хосте фиксирует рабочее дерево в ветку `sync/<node>` (включая указатель субмодуля commons), выполняет rebase на `origin/main` и отправляет с `--force-with-lease`.
2. Запланированное CI-задание в репозитории узла вливает `sync/<node>` обратно в `main` под идентичностью бота.
Этот цикл «capture-up» ([capture-up](/apex/glossary#capture-up)) означает, что дрейф на хосте превращается в коммит — видимый, поддающийся сравнению через diff и откату — вместо тихого расхождения. Синхронизация прерывается вместо фиксации, если субмодуль commons «грязный» или дерево `compositions/` отсутствует, поэтому сломанный checkout никогда не становится записанным состоянием.
> [!NOTE]
> CI-задание слияния выполняет checkout репозитория *без* субмодулей: субмодуль commons использует SSH-URL, который стандартный CI-токен не может получить, и слияние в любом случае лишь перемещает указатель gitlink.
## Проектные ограничения {#constraints}
- **Только stdlib.** Движок работает на штатном Python 3 из Debian — без `pip`, без сторонних импортов. Опциональный каталог `vendor/` существует для закреплённого чистого Python-кода, но поставляется пустым.
- **Composes, написанные вручную.** Нет слоя манифестов или генерации кода; compose-файлы являются источником истины.
- **Свидетельство важнее соглашения.** Всё, что может быть обеспечено принудительно, обеспечивается действием (линт якорного IP, откат crontab, самовосстановление блоков-маркеров), а не документацией.
---
**См. также:**
- [Концепция: Действия и оверлей](/apex/concepts/actions-and-overlay)
- [Концепция: Разрешение идентичности узла](/apex/concepts/node-identity)
- [Руководство: Сборка репозитория узла](/apex/guides/node-repository)
@@ -0,0 +1,87 @@
---
title: Композиции и общее ядро
description: Ядровая композиция на основе include, профили, сети и наслоение окружения.
weight: 15
---
Среда выполнения узла объявляется как [композиции](/apex/glossary#compositions): по одному docker-compose-проекту на сервис в каталоге `compositions/<project>/`, плюс [ядровая композиция](/apex/glossary#core-composition) — граничные и инфраструктурные сервисы, необходимые каждому узлу. Ядро располагается в [«commons»](/apex/glossary#commons) и потребляется каждым узлом через тонкую обёртку, поэтому его определение написано один раз и версионируется вместе с фреймворком.
## Обёртка include {#include}
Файл `compositions/apex/docker-compose.yml` каждого узла состоит из трёх строк:
```yaml {filename="compositions/apex/docker-compose.yml"}
name: apex
include:
- ../../commons/compositions/apex/docker-compose.yml
```
Общий файл полностью параметризован через `${APEX_*}` и не содержит относительных хостовых путей — каждое монтирование разрешается через переменные, которые [движок](/apex/glossary#engine) экспортирует при вызове, поэтому один и тот же файл корректен на каждом узле.
Структурное исключение — узел, которому действительно нужно изменить ядровой сервис, например опубликовать дополнительный хостовый порт, — наслаивает переопределение через форму include со списком путей вместо редактирования общего файла:
```yaml {filename="compositions/apex/docker-compose.yml"}
name: apex
include:
- path:
- ../../commons/compositions/apex/docker-compose.yml
- ./traefik.override.yml
```
> [!NOTE]
> Директива `include:` требует Docker Compose v2.20 или новее.
## Ядровые сервисы и профили {#core-services}
Ядровая композиция определяет общий набор сервисов флота. Всегда включённые сервисы:
- **traefik** — граничный обратный прокси и якорь сети `direct` (всегда `.10` в подсети узла). Он завершает TLS через DNS-челлендж, обнаруживает бэкенды через docker-метки и выставляет свою панель управления на FQDN узла за списком разрешённых IP [анклава](/apex/glossary#enclave).
- **socket-proxy** — отфильтрованный прокси для docker-сокета; traefik читает состояние контейнеров через него и никогда не обращается к сокету напрямую.
Опциональные сервисы, управляемые через compose-профили, которые узел включает в `COMPOSE_PROFILES` (задаётся в `apex.env`):
| Профиль | Сервис | Роль |
|---|---|---|
| `xray` | xray | Прокси-край с host-network для анклава. |
| `adguard` | adguard | DNS-фильтрация с использованием сетевого пространства имён traefik; маршрутизируется по `/adguard` на FQDN узла. |
| `alloy` | alloy | Понодовый агент наблюдаемости (Grafana Alloy). Собирает метрики хоста и контейнеров, а также логи journal и контейнеров и **проталкивает** их исходящим потоком в центральные Prometheus и Loki (см. [Концепция: Модель безопасности](/apex/concepts/security-model#observability)). Его `hostname` контейнера установлен в `APEX_NODE_HOST`, и это значение проставляет метки `node` и `instance` на каждой метрике и потоке логов. |
| `manual` | resticontainer | Одноразовый исполнитель резервного копирования, управляемый метками (`ghcr.io/deytenit/resticontainer`) — никогда не запускается через `up`; вызывается действием `backup/run`, которое обнаруживает намерение резервного копирования для каждого сервиса из compose-меток `restic.*`. |
## Сети {#networks}
Ядровая композиция владеет тремя сетями узла; сервисные композиции ссылаются на них по ключу с `external: true`.
| Сеть | Тип | Назначение |
|---|---|---|
| `direct` | bridge, подсеть = `APEX_SUBNET` | Сеть со статическими [якорными IP](/apex/glossary#anchor-ip); якорь каждого проекта удерживает здесь фиксированный адрес (см. [Концепция: Схема адресации флота](/apex/concepts/addressing)). |
| `enclave` | bridge, `internal: true` | Бэкенд-сеть обратного прокси; traefik достигает помеченных сервисов здесь. |
| `socket` | bridge, `internal: true` | Несёт только трафик traefik ↔ socket-proxy. |
Внутри проекта соседние контейнеры обычно присоединяются к сетевому пространству имён якоря (`network_mode: service:<anchor>`) вместо самостоятельного подключения к сетям.
## Наслоение окружения {#env-layering}
Значения достигают композиции из трёх слоёв в порядке возрастания приоритета:
1. **`.env`** — незакоммиченные секреты, символьная ссылка в хранилище уровня 1 (учётные данные `APEX_RESTIC_*`, токены `APEX_TRAEFIK_*`, блок UID/GID).
2. **`apex.env`** — закоммиченные несекретные скалярные значения (`APEX_TRAEFIK_DIRECT_IP`, `APEX_RESTIC_REPOSITORY`, `COMPOSE_PROFILES`, опциональный `APEX_RESTIC_COMPRESSION`).
3. **Экспорты движка** — идентичность `APEX_*` и пути [уровней](/apex/glossary#tier) из `ctx.vars()`, внедряемые в окружение процесса действиями оркестрации и потому выигрывающие у обоих файлов.
Критические переменные несут маркеры `?error` в общем файле, поэтому отсутствующий секрет громко проваливает загрузку конфигурации композиции вместо запуска неправильно настроенного контейнера.
## Оркестрация {#orchestration}
`apex compose <action>` управляет каждой композицией на узле:
- **Порядок** — сначала ядровой проект (он владеет сетями), затем каждая сервисная композиция в алфавитном порядке; `down` разворачивает последовательность.
- **Env-файлы** — для каждого проекта `.env` и `apex.env` передаются как `--env-file`, если они присутствуют.
- **`up`** подразумевает `-d`; любые дополнительные аргументы передаются в `docker compose`.
- **`--dry-run`** печатает каждую команду с её рабочим каталогом вместо выполнения — принимается в любом месте командной строки.
---
**См. также:**
- [Концепция: Схема адресации флота](/apex/concepts/addressing)
- [Руководство: Добавление сервисной композиции](/apex/guides/service-composition)
- [Справочник: Переменные окружения](/apex/reference/environment)
@@ -0,0 +1,71 @@
---
title: Контекст выполнения
description: ctx — единый шов между действием и хостом.
weight: 14
---
Каждое [действие](/apex/glossary#action) получает объект `ctx` в качестве первого аргумента. Это единственный санкционированный способ взаимодействовать с хостом: логирование, выполнение процессов, рендеринг шаблонов, изменение файлов, управление фаерволом и сервисами, а также уведомления — всё проходит через него. Поэтому действия остаются декларативными, а поведение, которое должно быть согласованным по всему флоту (формат логов, дисциплина резервного копирования, полезная нагрузка уведомлений), находится в одном месте — [движке](/apex/glossary#engine).
## Члены {#members}
| Член | Назначение |
|---|---|
| `ctx.log` | Логирование с префиксом: `[<node>] [<action>] [LEVEL] message`; ошибки идут в stderr, всё остальное — в stdout. |
| `ctx.sys` | Выполнение процессов (`run`, `sudo`, `ok`), помощники для сервисов systemd и идемпотентная установка пакетов. |
| `ctx.tpl` | Рендеринг шаблонов над опорой `configs/` с помощью `string.Template.safe_substitute`. |
| `ctx.host` | Идемпотентное изменение хоста: резервные копии с меткой времени, атомарная запись файлов, инъекция блоков-маркеров, установка crontab с откатом. |
| `ctx.ufw` | Применение фаервола хоста и контейнеров (см. [Концепция: Модель безопасности](/apex/concepts/security-model)). |
| `ctx.crowdsec` | Установка CrowdSec, развёртывание конфигурации и управление коллекциями. |
| `ctx.systemd` | Рендеринг и включение systemd-юнитов из `configs/systemd/`. |
| `ctx.notify` | Уведомления Telegram; каждый метод возвращает булево значение, чтобы действия могли ставить код выхода в зависимость от доставки. |
| `ctx.paths` | Разрешённые пути репозитория: `repo_root`, `commons`, `proprietaries`, `configs`, `compositions`, `core` и свойства уровней. |
| `ctx.node` | Разрешённая запись [идентичности узла](/apex/glossary#node-identity) (`name`, `subnet`, `fqdn`, `host`); `None` в автономном чекауте commons. |
| `ctx.commons` | Доступен только в переопределяющем проприетарном действии: `ctx.commons.run(args)` делегирует затенённой реализации commons. |
Сигнатуры уровня методов перечислены в [Справочник: API движка](/apex/reference/engine-api).
## Семантика шаблонов {#templates}
`ctx.tpl` рендерит файлы из `configs/` с помощью питоновского `string.Template.safe_substitute`:
- Известные ссылки `$VAR` и `${VAR}` заменяются.
- **Неизвестные ссылки остаются буквальными.** Опечатка в шаблоне сохраняется в отрендеренном выводе, где она видна, вместо того чтобы молча свернуться в пустую строку.
- Подстановка команд shell (`$(...)`) проходит нетронутой, что сохраняет целостность записей crontab со встроенным `$(cat ...)`.
Набор подстановки — это `ctx.vars()`: полное окружение процесса плюс предоставляемые движком значения `APEX_*`, приведённые ниже. Циклы и условия никогда не живут в шаблонах; их место — в действии.
## Экспорт переменных {#vars}
`ctx.vars()` возвращает окружение, используемое для каждого рендеринга шаблона, а его подмножество `APEX_*` экспортируется в `docker compose` действиями оркестрации. На узле оно содержит:
| Переменная | Значение |
|---|---|
| `APEX_COMMONS` | Абсолютный путь директории commons (лаунчер находится в `$APEX_COMMONS/apex`). |
| `APEX_REPO_ROOT` | Абсолютный путь корня репозитория узла. |
| `APEX_NODE_HOST`, `APEX_NODE_FQDN`, `APEX_SUBNET` | Разрешённые поля идентичности. |
| `APEX_TIER1`…`APEX_TIER3` | Директории данных базового проекта по каждому [уровню](/apex/glossary#tier). |
| `APEX_TIER1_SHARED`…`APEX_TIER3_SHARED` | Общие области внутри каждого уровня. |
| `APEX_TIER_ROOT1`…`APEX_TIER_ROOT3` | Корни хранилищ уровней (ссылки уровня узла). |
Полная матрица источников/потребителей приведена в [Справочник: Переменные окружения](/apex/reference/environment).
## Дисциплина изменения хоста {#host}
`ctx.host` кодирует правила записи фреймворка:
- **Атомарная запись.** `write_file` рендерит во временный файл (режим `0644`, соответствующий тому, что создал бы редирект root-шелла) и устанавливает его через `mv` или `sudo cp`; перезапись существующего файла сохраняет режим этого файла.
- **Резервные копии с меткой времени.** По запросу предыдущий файл сохраняется рядом с целевым как `<name>.<epoch>.bak` перед записью.
- **Инъекция блоков-маркеров.** Управляемые блоки внутри системных файлов (например, `/etc/ufw/before.rules`) ограничиваются начальными/конечными комментариями-маркерами. Инъекция удаляет каждый предыдущий управляемый блок — включая блоки, оставленные предшествующим инструментарием, — перед вставкой свежего после его якорной строки, поэтому повторные запуски никогда не накапливаются.
- **Откат crontab.** `install_crontab` делает снимок текущего crontab, отказывается от пустого рендера (код выхода `65`), показывает предпросмотр результата и восстанавливает снимок, если установка не удалась.
## Уведомления {#notifications}
`ctx.notify` отправляет сообщения Telegram через стандартную библиотеку (`urllib`). Полезная нагрузка несёт имя инстанса — публичный хост узла `APEX_NODE_HOST` — статус (`firing`/`resolved`), уровень эскалации и сообщение, усечённое до 200 символов. Каждый метод возвращает `True` только тогда, когда отправка удалась; автоматические действия трактуют неудачную отправку своего финального сообщения о статусе как сбой (код выхода `1`), поэтому сломанный канал уведомлений замечается, а не молча терпится.
---
**См. также:**
- [Справочник: API движка](/apex/reference/engine-api)
- [Справочник: Переменные окружения](/apex/reference/environment)
- [Концепция: Действия и оверлей](/apex/concepts/actions-and-overlay)
@@ -0,0 +1,77 @@
---
title: "Разрешение идентичности узла"
description: "Как узел узнаёт, кто он: ключи идентичности node.env и запасной вариант FQDN."
weight: 12
---
Каждая операция apex выполняется в контексте ровно одного [узла](/apex/glossary#node). [Движок](/apex/glossary#engine) выводит эту [идентичность узла](/apex/glossary#node-identity) — имя узла, подсеть, FQDN и публичный хост — из файла `node.env` в корне репозитория, используя имя хоста ОС только как запасной вариант для FQDN. Фреймворк не предполагает ни домена, ни кластеризации: узел объявляет, кто он, явно. Этот шаг разрешения приходит на смену механизму `root_require_node` bash-фреймворка commons «root», который заменяет apex.
## Источники идентичности {#sources}
### node.env
`node.env` — это обычный файл вида `KEY=value` (пригодный для `source` в shell), расположенный в корне репозитория. При разборе комментарии и пустые строки игнорируются, а окружающие значения одинарные или двойные кавычки удаляются.
Он объявляет идентичность явно через следующие ключи:
- `APEX_NODE_FQDN` — реальный FQDN узла, используемый для маршрутизации traefik `Host()`. Если не задан, движок откатывается к имени хоста ОС (и выводит предупреждение).
- `APEX_NODE_HOST` — публичное имя хоста узла, используемое для `--host` резервного копирования и `instance` для наблюдаемости/уведомлений. По умолчанию равно `APEX_NODE_FQDN`.
- `APEX_SUBNET` — `/24` узла для сети `direct`. **Обязателен**; другого источника для него нет (см. [Концепция: Схема адресации флота](/apex/concepts/addressing)).
```ini {filename="node.env"}
APEX_NODE_FQDN=node1.example.com
APEX_NODE_HOST=node1.example.com
APEX_SUBNET=198.18.16.0/24
```
### Имя хоста ОС
Имя хоста ОС используется только как запасной вариант для `APEX_NODE_FQDN`. Когда в `node.env` не задан `APEX_NODE_FQDN`, движок использует имя хоста и выводит предупреждение. Оно никогда не разбирается для получения имени узла или любой другой координаты.
## Порядок разрешения {#resolution}
{{% steps %}}
### Разрешите FQDN
`fqdn` — это `APEX_NODE_FQDN` из `node.env` или имя хоста ОС, когда этот ключ не задан. Если ни то ни другое не даёт значения, разрешение завершается неудачей (см. [Режимы сбоев](#failures)).
### Разрешите публичный хост
`host` — это `APEX_NODE_HOST` из `node.env` или `fqdn`, когда этот ключ не задан.
### Выведите короткое имя
`name` — это **первая метка** `host` (`host.split(".")[0]`) — например, `node1` для `node1.example.com`. Оно используется для логов, ветки `sync/<node>` и сообщений.
### Прочитайте подсеть
`subnet` всегда читается из `APEX_SUBNET` в `node.env`. Она обязательна.
{{% /steps %}}
Результат — запись `Node` с четырьмя полями: `name`, `subnet`, `fqdn` и `host`.
## Предупреждения {#warnings}
Разрешение идентичности выводит ровно одно предупреждение; оно сигнализирует об исправимой несогласованности, которую следует устранить.
| Условие | Сообщение предупреждения |
|---|---|
| `APEX_NODE_FQDN` не задан в `node.env` (вместо него используется имя хоста) | `APEX_NODE_FQDN not set in node.env; using hostname '<fqdn>'.` |
## Режимы сбоев {#failures}
Разрешение отказывается угадывать. Два условия фатальны, и оба завершаются с кодом `66` — соглашение движка о коде выхода для отсутствующего ввода:
- **Нет FQDN вовсе.** `APEX_NODE_FQDN` не задан в `node.env` *и* имя хоста ОС пусто. Движок пишет `identity: APEX_NODE_FQDN missing from node.env and no hostname` в stderr и завершается.
- **Нет подсети.** `APEX_SUBNET` отсутствует в `node.env` (в том числе когда сам файл отсутствует). Движок пишет `identity: APEX_SUBNET missing from node.env` в stderr и завершается.
> [!WARNING]
> Действительный `node.env`, несущий `APEX_SUBNET`, обязателен на каждом узле. `APEX_NODE_FQDN` можно опустить только тогда, когда имя хоста ОС предоставляет пригодный FQDN.
---
**См. также:**
- [GitHub: apex.ermnvldmr.com](https://github.com/deytenit/apex.ermnvldmr.com)
@@ -0,0 +1,53 @@
---
title: Модель безопасности
description: Изоляция привилегий, управление межсетевым экраном, обнаружение вторжений и обработка секретов.
weight: 18
---
apex укрепляет [узел](/apex/glossary#node) в независимых слоях: изоляция привилегий на уровне файловой системы, управляемый межсетевой экран хоста, обнаружение вторжений, ограничение на уровне контейнеров и строгая граница секретов. Каждый слой применяется [действием](/apex/glossary#action) и потому воспроизводим и повторно запускаем.
## Изоляция привилегий {#privilege}
Каждый проект выполняется под собственным заблокированным системным пользователем `noroot-<project>`; общее хранилище доступно на запись группе с установленным sticky-битом, поэтому проекты могут обмениваться файлами, не имея возможности удалять файлы друг друга. Полный механизм — пользователи, группы и матрица владения — описан в [Концепция: Многоуровневое хранилище](/apex/concepts/tiered-storage).
## Межсетевой экран хоста {#firewall}
`apex configure/ufw` применяет межсетевой экран узла из опоры `configs/ufw/` в трёх частях:
- **Правила хоста** (`configs/ufw/host/host.rules`) — простые команды `ufw`, по одной на строку, применяемые по порядку.
- **Блоки правил уровня ядра** (`configs/ufw/host/before.rules`, `after.rules`) — внедряются в `/etc/ufw/before.rules` и `/etc/ufw/after.rules` как управляемые блоки-маркеры. Внедрение является **самовосстанавливающимся**: каждый предыдущий управляемый блок — включая блоки, оставленные инструментарием-предшественником, — удаляется перед вставкой свежего блока после якоря ufw `# End required lines`, а рядом сохраняется `.bak` целевого файла с меткой времени.
- **Правила контейнеров** (`configs/ufw/docker/*.rules`) — применяются через `ufw-docker`, который транслирует их в правила, переживающие собственные манипуляции Docker с iptables. `configure/base` устанавливает вспомогательный инструмент в `~/.local/bin/ufw-docker`.
Отсутствующая конфигурация — это ошибка, а не тихий пропуск: отсутствующий каталог `docker/` или `host/` завершается с кодом `66`, если существует его аналог, а отсутствующий целевой файл `/etc/ufw` прерывает запуск. Действие завершается напоминанием, что `ufw` должен быть перезапущен, чтобы изменения вступили в силу, — и что SSH-порт должен быть разрешён в первую очередь.
## Обнаружение вторжений {#crowdsec}
`apex configure/crowdsec` устанавливает CrowdSec через его официальный установщик, когда `cscli` отсутствует, рендерит всё из `configs/crowdsec/` в `/etc/crowdsec/` (источники сбора, парсеры, конфигурация bouncer) и устанавливает коллекции `crowdsecurity/linux` и `crowdsecurity/traefik`. Когда конфигурации узла включают конфигурацию firewall-bouncer, устанавливается и соответствующий пакет bouncer, замыкая цикл от обнаружения к блокировке.
## Ограничение на уровне контейнеров {#containers}
[Ядровая композиция](/apex/glossary#core-composition) кодирует укрепление на уровне контейнеров:
- Граничный прокси никогда не обращается к Docker-сокету: он обнаруживает бэкенды через **отфильтрованный socket-proxy**, который выставляет состояние контейнеров/сервисов в режиме только для чтения в изолированной сети.
- `enclave` и `socket` — сети с `internal: true` — без исходящего трафика, без выставления хостовых портов.
- Ядровые сервисы устанавливают `no-new-privileges` и работают под noroot-идентичностью проекта везде, где это позволяет нагрузка; capabilities выдаются для каждого сервиса, а не глобально.
## Наблюдаемость {#observability}
Наблюдаемость — это **исходящий push**, а не выставленный маршрут. Опциональный ядровой сервис `alloy` (Grafana Alloy, один агент на узел) собирает метрики хоста (эквивалент `node_exporter`), метрики контейнеров (cadvisor), systemd journal и логи docker-контейнеров и проталкивает их в **центральные** Prometheus и Loki, которые располагаются на выделенном узле — метрики через `prometheus.remote_write` в `APEX_PROM_PUSH_URL`, логи через `loki.write` в `APEX_LOKI_PUSH_URL`. Обе цели аутентифицируются через HTTP basic auth с использованием `APEX_OBS_BASIC_AUTH_USER` / `APEX_OBS_BASIC_AUTH_PASS`. Поскольку push является исходящим, Alloy подключается к сети `direct` (у которой есть исходящий трафик), а не к `internal`-сети `enclave`; узло-локального хранилища логов нет, как нет и маршрута `/loki`, который нужно было бы защищать.
## Граница секретов {#secrets}
- Секреты живут только в файлах `.env` для каждого проекта, которые являются **символьными ссылками в хранилище уровня 1** — за пределами рабочего дерева репозитория и покрытыми `.gitignore` (`**/.env`).
- Каждый репозиторий узла документирует ожидаемые ключи в `SECRETS.md`; файл документирует имена, но никогда — значения.
- Закоммиченная конфигурация (`apex.env`) ограничена несекретными скалярными значениями.
- Общая композиция помечает критические секреты через `?error`, поэтому отсутствующее значение быстро проваливается на этапе загрузки конфигурации compose.
- Коммиты «capture-up» безопасны по построению: игнорируемые ссылки `.env` и данные уровней никогда не попадают в git-индекс.
---
**См. также:**
- [Концепция: Многоуровневое хранилище и изоляция привилегий](/apex/concepts/tiered-storage)
- [Концепция: Композиции и общее ядро](/apex/concepts/compositions)
- [Справочник: Действия](/apex/reference/actions)
@@ -0,0 +1,75 @@
---
title: Многоуровневое хранилище и изоляция привилегий
description: Структура ссылок @tier, общее хранилище и понодовые noroot-пользователи.
weight: 16
---
Данные сервисов никогда не живут внутри репозитория [узла](/apex/glossary#node). Они живут в трёх [уровнях](/apex/glossary#tier) хранилища — раздельных расположениях, выбираемых для каждого хоста, — и достигают [композиций](/apex/glossary#compositions) через стандартизированную структуру символьных ссылок. Владение этими данными делегируется системным пользователям для каждого проекта, поэтому ни один сервисный контейнер не пишет от имени root.
## Структура ссылок уровней {#links}
`apex tiers/link <tier1> <tier2> <tier3>` строит два слоя ссылок:
- **На уровне узла:** `compositions/@tierN` → корень хранилища уровня. Именно их движок выставляет как `APEX_TIER_ROOT1..3`.
- **Для каждого проекта:** для каждого `compositions/<project>/` действие создаёт `<tier-root>/<project>/` и связывает `compositions/<project>/@tierN` → `<tier-root>/<project>`. Внутри каждого уровня оно также поддерживает `shared/` с подкаталогом для каждого проекта и относительной ссылкой `shared` внутри каталога уровня каждого проекта, указывающей обратно на него.
Каждый проект дополнительно получает `compositions/<project>/.env` → `<tier1-root>/<project>/.env` (создаётся пустым, если отсутствует) — файл незакоммиченных секретов, описанный в [Концепция: Композиции](/apex/concepts/compositions). Ссылки `@tierN` и файлы `.env` игнорируются git; репозиторий остаётся чистым источником.
Если путь, который должен быть символьной ссылкой, существует как настоящий файл или каталог, действие предупреждает и запрашивает подтверждение перед его удалением; отказ (включая неинтерактивный EOF) пропускает эту ссылку и продолжает.
## Пользователи и группы {#users}
`apex tiers/useradd` провизионирует по одному заблокированному системному пользователю на проект — `noroot-<project>` (имя санитизируется, собственная первичная группа) — плюс общефлотовую группу `noroot-shared`, к которой присоединяется каждый пользователь проекта. Затем оно внедряет разрешённые ID в `.env` проекта между комментариями-маркерами, заменяя любой предыдущий блок при повторных запусках:
```ini {filename="compositions/<project>/.env"}
# START AUTO GENERATED APEX-NOROOT-USERS
APEX_UID=998
APEX_GID=998
APEX_SHARED_GID=997
# END AUTO GENERATED APEX-NOROOT-USERS
```
Композиции потребляют их как `user: "${APEX_UID?error}:${APEX_GID?error}"` и `group_add: ["${APEX_SHARED_GID?error}"]`.
## Матрица владения {#ownership}
`apex tiers/chown` обеспечивает владение по всем трём уровням. Без аргументов оно работает с корнями уровней (`APEX_TIER_ROOT1..3`); явные пути уровней могут переопределить это.
| Путь | Владение | Примечания |
|---|---|---|
| `<tier-root>` | `root:root` | Только inode каталога. |
| `<tier-root>/shared` | `root:root` | Только inode каталога — никогда рекурсивно, поэтому существующие данные проектов внутри не затрагиваются. |
| `<tier-root>/node-infra` | `root:root` | Рекурсивно. |
| `<tier-root>/<project>` | `uid:gid` | Рекурсивно; ID читаются из `.env` проекта. |
| `<tier-root>/shared/<project>` | `uid:shared_gid` | Рекурсивно, плюс sticky-бит — участники могут добавлять файлы, но удалять только свои. |
Проекты с неполным набором `APEX_UID`/`APEX_GID`/`APEX_SHARED_GID` пропускаются с предупреждением, указывающим на `tiers/useradd`.
## Переменные уровней движка {#variables}
| Переменная | Разрешается в |
|---|---|
| `APEX_TIER_ROOT1..3` | Корни хранилищ уровней (`compositions/@tierN`, полностью разрешённые). |
| `APEX_TIER1..3` | Каталоги данных ядрового проекта внутри каждого уровня (`compositions/apex/@tierN`). |
| `APEX_TIER1..3_SHARED` | Общая область внутри уровней ядрового проекта. |
Ядровая композиция монтирует исключительно через них; исполнитель резервного копирования вместо этого монтирует корень данных флота `/srv` в режиме только для чтения и разрешает контейнерные монтирования каждого помеченного сервиса обратно в их хостовые пути.
## Резервные копии {#backups}
`apex backup/run <telegram-bot-url>` управляет ядровым сервисом `resticontainer` (профиль `manual`, одноразовый) от начала до конца. Намерение резервного копирования **управляется метками**: resticontainer обнаруживает каждый сервис, несущий `restic.enable=true`, читает его `restic.backup.paths` (и опциональные `restic.backup.stop` / `restic.hooks.*`), разрешает эти контейнерные пути в хостовые пути и делает снимок их объединения в одном запуске restic.
1. Проверяет доступность репозитория через `snapshots`; инициализирует репозиторий при необходимости.
2. Выполняет `backup` с `--host <APEX_NODE_HOST>` и `--tag biweekly` — resticontainer выполняет pre-hook каждого помеченного сервиса, останавливает любой контейнер, помеченный `restic.backup.stop`, делает снимок разрешённых хостовых путей, затем перезапускает и выполняет post-hooks. Запуск отказывается сообщать об успехе, если не появляется новый снимок (защита от обнаружения нулевого количества меток).
3. Применяет удержание через `forget --prune` (7 ежедневных, 4 еженедельных, 12 ежемесячных).
4. Отправляет статус в Telegram в любом случае; успех, уведомление о котором не может быть доставлено, всё равно завершается с кодом `1`, чтобы планировщики это заметили.
Расположение репозитория и учётные данные полностью берутся из конфигурации `APEX_RESTIC_*` (см. [Справочник: Переменные окружения](/apex/reference/environment)). Какие данные захватываются, решается для каждого сервиса его метками `restic.*` — например, ядровые сервисы резервируют только свою конфигурацию (динамические правила traefik, конфигурацию xray/adguard), оставляя воспроизводимые данные уровня 3, такие как кэши и логи, вне набора.
---
**См. также:**
- [Концепция: Модель безопасности](/apex/concepts/security-model)
- [Справочник: Действия](/apex/reference/actions)
- [Руководство: Добавление сервисной композиции](/apex/guides/service-composition)
@@ -0,0 +1,102 @@
---
title: Начало работы с узлом apex
description: Начальная настройка репозитория узла и перевод хоста под управление apex.
weight: 5
---
Это краткое руководство проведёт новый хост от склонированного репозитория [узла](/apex/glossary#node) до работающего стека сервисов. Предполагается, что репозиторий узла уже существует — чтобы собрать его с нуля, см. [Руководство: Сборка репозитория узла](/apex/guides/node-repository).
## Предварительные требования
- Хост на базе Debian. Подойдёт любой FQDN — [идентичность узла](/apex/glossary#node-identity) объявляется явно в `node.env` (`APEX_NODE_FQDN`, `APEX_NODE_HOST`, `APEX_SUBNET`), а не выводится из обязательного формата имени хоста.
- `git` и Python 3 — [движок](/apex/glossary#engine) использует только стандартную библиотеку; `pip` никогда не требуется.
- Docker с Compose версии v2.20 или новее ([базовая композиция](/apex/glossary#core-composition) подключается через директиву `include:`).
- Пользователь для входа с возможностью sudo.
- Три места хранения для [уровней](/apex/glossary#tier) (например, три точки монтирования).
## Начальная настройка
{{% steps %}}
### Склонируйте репозиторий узла
```bash
git clone --recurse-submodules git@github.com:<owner>/<node-repo>.git
cd <node-repo>
./init.sh
source ~/.bashrc
```
`init.sh` инициализирует субмодуль `commons/`, указывает `core.hooksPath` на `commons/githooks`, помечает чекаут как `safe.directory` и добавляет `commons/` в начало `PATH` — лаунчер находится в `commons/apex`.
### Проверьте обнаружение действий
```bash
apex --help
```
В списке отображается каждое действие [commons](/apex/glossary#commons) плюс любые локальные [проприетарные модули](/apex/glossary#proprietaries) узла с меткой `(local)`. Когда в `node.env` отсутствует `APEX_NODE_FQDN`, движок выводит предупреждение и откатывается к имени хоста ОС — безобидно на рабочей станции, но на самом узле задайте `APEX_NODE_FQDN` явно. (`APEX_SUBNET` обязателен: если он отсутствует, движок завершается с кодом `66`.)
### Свяжите уровни хранилища
```bash
apex tiers/link <tier1-path> <tier2-path> <tier3-path>
```
Создаёт ссылки `compositions/@tierN` уровня узла, директории уровней и ссылки `@tierN` для каждого проекта, области `shared/` и символьную ссылку `.env` каждого проекта в уровень 1. Успех завершается сообщением `@tier setup completed.`
### Заведите пользователей проектов
```bash
apex tiers/useradd
apex tiers/chown
```
`tiers/useradd` создаёт по одному системному пользователю `noroot-<project>` на каждую композицию плюс группу `noroot-shared`, а также добавляет `APEX_UID`/`APEX_GID`/`APEX_SHARED_GID` в `.env` каждого проекта. Затем `tiers/chown` применяет матрицу владения (см. [Концепция: Многоуровневое хранилище](/apex/concepts/tiered-storage)) и завершается сообщением `Permissions applied.`
### Заполните базовые секреты
Отредактируйте `compositions/apex/.env` (символьная ссылка в хранилище уровня 1, никогда не коммитится), указав ключи, задокументированные в файле `SECRETS.md` репозитория узла:
```ini {filename="compositions/apex/.env"}
APEX_RESTIC_PASSWORD=...
APEX_RESTIC_AWS_ACCESS_KEY_ID=...
APEX_RESTIC_AWS_SECRET_ACCESS_KEY=...
APEX_TRAEFIK_CF_DNS_API_TOKEN=...
APEX_TRAEFIK_ACME_EMAIL=...
APEX_OBS_BASIC_AUTH_PASS=... # только когда включён профиль наблюдаемости alloy
```
`APEX_UID`/`APEX_GID`/`APEX_SHARED_GID` уже присутствуют — `tiers/useradd` поддерживает их между комментариями-маркерами. Несекретные скалярные значения (`APEX_RESTIC_REPOSITORY`, push-URL-ы, `COMPOSE_PROFILES`, `TZ`, …) хранятся в коммитируемом `compositions/apex/apex.env` — см. [Руководство: Сборка репозитория узла](/apex/guides/node-repository).
### Настройте хост
```bash
apex configure
```
Запускает каждое действие `configure/*` в фиксированном порядке: `base`, `ufw`, `crowdsec`, `cron`, `systemd`, `routing`. Каждый шаг можно вызвать и по отдельности; некоторые принимают `--dry-run`. См. [Руководство: Эксплуатация узла](/apex/guides/host-operations).
### Поднимите стек
```bash
apex compose up
```
Сначала поднимается базовая композиция (она владеет сетями), затем каждая композиция сервиса в алфавитном порядке.
{{% /steps %}}
## Проверка
- `apex --help` выводит список действий без предупреждений на работающем хосте.
- `docker ps` показывает базовые контейнеры (`apex-traefik`, `apex-socket-proxy` плюс любые сервисы, включённые профилями) и якорь каждого проекта.
- `apex utils/lint-docker-compose` сообщает `Anchor IP validation passed.`
---
**См. также:**
- [Руководство: Сборка репозитория узла](/apex/guides/node-repository)
- [Концепция: Архитектура фреймворка](/apex/concepts/architecture)
- [GitHub: apex.ermnvldmr.com](https://github.com/deytenit/apex.ermnvldmr.com)
+69
View File
@@ -0,0 +1,69 @@
---
title: Глоссарий
description: Терминология, используемая в рамках фреймворка операций флота apex.
weight: 100
---
Терминология, используемая в рамках фреймворка операций флота apex.
## Action {#action}
**Action (Действие)** - Атомарная задача автоматизации: один модуль на Python, экспортирующий `METADATA = Meta(...)` и `run(ctx, args)`, диспетчеризуемый по пути своего файла как `apex <group>/<action>`.
## Anchor IP {#anchor-ip}
**Anchor IP (Якорный IP)** - Статический `ipv4_address`, который основной контейнер проекта удерживает в сети `direct`. Якоря делают адреса сервисов предсказуемыми и проверяются линтером compose.
## Capture-up {#capture-up}
**Capture-up** - «capture-up» (запись состояния хоста в ветку `sync/<node>`): поток, который фиксирует текущее состояние хоста обратно в его репозиторий — плановый коммит в ветку `sync/<node>`, ребазируемый на `main` и сворачиваемый обратно средствами CI. Дрейф становится diff-ом, а не расхождением.
## Commons {#commons}
**Commons** - «commons» (общий репозиторий) (`apex.ermnvldmr.com`), содержащий движок, общие действия и базовую композицию. Каждый узел подключает его как закреплённый git-субмодуль `commons/`.
## Compositions {#compositions}
**Compositions (Композиции)** - Опора репозитория узла, объявляющая среду выполнения узла: по одному docker-compose-проекту на сервис плюс базовая обёртка и реестр адресации.
## Configs {#configs}
**Configs (Конфигурации)** - Опора репозитория узла, содержащая исходники конфигурации хоста (`ufw/`, `cron/`, `systemd/`, `crowdsec/`), рендеримые через слой шаблонов действиями `configure/*`.
## Core composition {#core-composition}
**Core composition (Базовая композиция)** - Общий, параметризуемый через `${APEX_*}` docker-compose-проект, определяющий общие сервисы флота (краевой прокси, socket-прокси и опциональные сервисы профилей). Определяется один раз в commons, включается на каждом узле.
## Enclave {#enclave}
**Enclave (Анклав)** - Приватное адресное пространство флота, `198.18.0.0/16`, и, как расширение, внутренняя backend-сеть обратного прокси с именем `enclave` в базовой композиции.
## Engine {#engine}
**Engine (Движок)** - Среда выполнения на stdlib-Python в `commons/engine/`: лаунчер, разрешение идентичности, оверлей, контекст выполнения и вспомогательная библиотека `ctx.*`.
## Node {#node}
**Node (Узел)** - Хост, управляемый фреймворком, определяемый ровно одним репозиторием узла и идентифицируемый по FQDN, который он объявляет в `node.env` (например, `node1.example.com`).
## Node identity {#node-identity}
**Node identity (Идентичность узла)** - Разрешённая запись о том, кто есть узел — имя, подсеть, FQDN, публичный хост — выведенная из `node.env`, с именем хоста ОС в качестве запасного варианта для FQDN.
## Overlay {#overlay}
**Overlay (Оверлей)** - Правило разрешения, позволяющее проприетарным модулям узла добавлять, переопределять, оборачивать или отключать действия commons: `proprietaries/actions/` проверяется раньше `commons/actions/`.
## Proprietaries {#proprietaries}
**Proprietaries (Проприетарные модули)** - Опора репозитория узла для локального кода узла: действий, библиотек и исходников инструментов, которые применяются ровно к одному узлу и потому не относятся к commons.
## Tier {#tier}
**Tier (Уровень)** - Один из трёх стандартизированных классов хранилища, монтируемых узлом (конфигурация, ценные данные, воспроизводимые данные), доступный из композиций через структуру символьных ссылок `@tierN`.
---
**См. также:**
- [GitHub: apex.ermnvldmr.com](https://github.com/deytenit/apex.ermnvldmr.com)
@@ -0,0 +1,24 @@
---
title: Руководства
description: Практические руководства по сборке репозиториев узлов, действий и композиций.
weight: 20
---
Практические руководства для повседневной работы с apex. Каждое руководство решает одну повторяющуюся задачу: сборка репозитория [узла](/apex/glossary#node), разработка [действий](/apex/glossary#action) для [движка](/apex/glossary#engine), определение [композиций](/apex/glossary#compositions) сервисов и эксплуатация лежащего в основе хоста.
## Доступные руководства {#available-guides}
{{< cards >}}
{{< card link="/apex/guides/node-repository" title="Репозиторий узла" subtitle="Соберите репозиторий узла, который объявляет идентичность узла, его конфигурации и композиции." >}}
{{< card link="/apex/guides/action-development" title="Разработка действий" subtitle="Пишите действия, которые движок диспетчеризует на узле." >}}
{{< card link="/apex/guides/service-composition" title="Композиция сервисов" subtitle="Определяйте композиции проектов и разворачивайте их вместе с базовой композицией." >}}
{{< card link="/apex/guides/host-operations" title="Операции с хостом" subtitle="Выполняйте рутинные операции над работающим хостом через движок." >}}
{{< /cards >}}
---
**См. также:**
- [Docs: Getting started](/apex/getting-started)
- [Docs: Concepts](/apex/concepts)
- [GitHub: apex.ermnvldmr.com](https://github.com/deytenit/apex.ermnvldmr.com)
@@ -0,0 +1,90 @@
---
title: Разработка действий
description: Пишите, переопределяйте и тестируйте действия движка в commons или proprietaries.
weight: 22
---
**Контекст:** вся автоматизация поставляется в виде [действий](/apex/glossary#action) — по одному Python-модулю на каждое, диспетчеризуемых по пути. Общее поведение размещается в [commons](/apex/glossary#commons) `actions/` («commons» — общий репозиторий); всё, что специфично для узла, размещается в `proprietaries/actions/` этого узла. Оба используют идентичный контракт, а запускатель помещает оба дерева в `sys.path`, поэтому проприетарное действие импортирует `engine.*` точно так же, как и действие из commons.
## Минимальное действие {#minimal}
```python {filename="proprietaries/actions/utils/example.py"}
"""Однострочный docstring модуля, описывающий действие."""
import os
from engine.descriptor import Meta, Arg, Flag
METADATA = Meta(
summary="Скопировать отрендеренную конфигурацию на место.",
args=[Arg("target_dir", "Каталог назначения"),
Flag("--dry-run", "Только предпросмотр.")],
)
def run(ctx, args):
if not os.path.isdir(args.target_dir):
ctx.log.error(f"Нет такого каталога: {args.target_dir}")
raise SystemExit(66)
content = ctx.tpl.render("example/config", ctx.vars())
ctx.host.write_file(os.path.join(args.target_dir, "config"), content,
dry_run=args.dry_run, sudo=True)
ctx.log.success("Конфигурация развёрнута.")
```
Путь к файлу — это его имя: модуль выше диспетчеризуется как `apex utils/example`. Верните `0`/`None` для успеха, целое число для конкретного кода выхода или возбудите `SystemExit(code)`; [соглашение о кодах выхода](/apex/concepts/actions-and-overlay#exit-codes) таково: `64` — ошибка использования, `65` — ошибка данных, `66` — отсутствующий ввод, `1` — общая ошибка.
## Соглашения для действий без надзора {#unattended}
Действия, запускаемые из cron, следуют двум правилам, чтобы сбои приводили к оповещению, а не исчезали бесследно:
- **Оберните тело** в `try/except Exception` и отправьте `ctx.notify.error(...)` перед повторным возбуждением как `SystemExit(1)` — Python-эквивалент shell-ловушки ERR.
- **Привяжите код выхода к финальному уведомлению**: `ctx.notify.*` возвращает `False` при неудачной доставке; завершённый запуск, чьё статусное сообщение так и не пришло, всё равно должен завершиться с кодом `1`.
```python
try:
...работа...
log.success("Готово.")
if not ctx.notify.success(TITLE, url, "Готово.", node):
raise SystemExit(1)
except SystemExit:
raise
except Exception as e:
log.error(f"{type(e).__name__}: {e}")
ctx.notify.error(TITLE, url, f"Критическая ошибка: {e}", node)
raise SystemExit(1)
```
## Переопределение, обёртывание, отключение {#override}
Проприетарный файл с тем же относительным путём, что и действие из commons, полностью затеняет его. Внутри затеняющего модуля `ctx.commons.run(args)` вызывает затенённую реализацию — паттерн обёртывания:
```python {filename="proprietaries/actions/sync/packages.py"}
from engine.descriptor import Meta, Arg
METADATA = Meta(summary="sync/packages с локальным для узла предварительным шагом.",
args=[Arg("telegram_bot_url", "Telegram Bot URL")])
def run(ctx, args):
ctx.log.info("Локальная для узла подготовка...")
return ctx.commons.run(args) # делегирование реализации из commons
```
Чтобы вывести действие из эксплуатации на одном узле, присвойте `DISABLED = "reason"` на верхнем уровне модуля: диспетчеризация отклонит его (код выхода `1`), а в списке отобразится `[DISABLED: reason]`.
## Тестирование {#testing}
Набор тестов commons использует stdlib `unittest`:
```bash
./tests/run.sh # python3 -m unittest discover -s tests -p 'test_*.py' -v
```
Изменения движка должны сохранять набор тестов «зелёным». Дешёвая smoke-проверка для любого действия — из commons или проприетарного — это его парсер: `apex <name> --help` должен завершиться с кодом `0`, что доказывает, что модуль разбирается, его `METADATA` корректно сформирован, и его видит механизм обнаружения.
Pre-commit-хук commons (подключаемый `init.sh` через `core.hooksPath`) запускает `apex utils/lint-docker-compose --hook` для staged-файлов compose, поэтому изменения композиций линтуются в момент коммита.
---
**См. также:**
- [Concept: Actions and the overlay](/apex/concepts/actions-and-overlay)
- [Concept: The execution context](/apex/concepts/execution-context)
- [Reference: Engine API](/apex/reference/engine-api)
@@ -0,0 +1,73 @@
---
title: Эксплуатация узла
description: "Операции day-2: конфигурирование, композиции, резервные копии и синхронизация репозитория."
weight: 24
---
**Контекст:** после первоначальной настройки [узел](/apex/glossary#node) эксплуатируется полностью через [движок](/apex/glossary#engine). Это руководство охватывает повторяющиеся операции и то, что каждая из них фактически делает на хосте.
## Конфигурирование хоста {#configure}
`apex configure` запускает полный набор конфигурирования в фиксированном порядке; каждый шаг вызывается индивидуально, когда изменился только один слой:
| Шаг | Эффект |
|---|---|
| `configure/base` | Обеспечивает базу Debian: обновление индекса пакетов, необходимые пакеты (`curl`, `wget`, `git`, `rsyslog`), запущенный rsyslog, установленный `ufw` и вспомогательный скрипт `ufw-docker` в `~/.local/bin/ufw-docker`. |
| `configure/ufw` | Применяет правила хоста, внедряет управляемые блоки правил уровня ядра, применяет правила контейнеров через `ufw-docker`. Поддерживает `--dry-run`. |
| `configure/crowdsec` | Устанавливает CrowdSec, если он отсутствует, разворачивает `configs/crowdsec/` в `/etc/crowdsec/`, устанавливает коллекции и firewall bouncer, если он настроен. Поддерживает `--dry-run`. |
| `configure/cron` | Рендерит `configs/cron/crontab` через слой шаблонов и устанавливает его — с предпросмотром, отказом при пустом рендере (код выхода `65`) и откатом при неудачной установке. Поддерживает `--dry-run`. |
| `configure/systemd` | Рендерит файлы верхнего уровня из `configs/systemd/` в `/etc/systemd/system/`, затем выполняет daemon-reload и включает каждый `.service`. Поддерживает `--dry-run`. |
| `configure/routing` | Включает IP-форвардинг и устанавливает правило и таблицу policy-routing по fwmark, используемые границей анклава; идемпотентно. |
После `configure/ufw` намеренно перезапустите `ufw` (`sudo systemctl restart ufw`) — действие напоминает об этом и предупреждает, что порт SSH уже должен быть разрешён.
## Жизненный цикл композиции {#compose}
```bash
apex compose up # сначала core, затем сервисы по алфавиту
apex compose down # обратный порядок
apex compose restart
apex compose up --dry-run # только напечатать команды
```
Дополнительные аргументы передаются в `docker compose` для каждого проекта. Файлы `.env` и `apex.env` каждого проекта подставляются автоматически, если они присутствуют.
## Резервные копии {#backups}
```bash
apex backup/run "$(cat <path-to-telegram-url-file>)"
```
Запускает [resticontainer](https://github.com/deytenit/resticontainer) в режиме one-shot под профилем compose `manual`: init-if-needed, затем управляемое метками резервное копирование, которое определяет намерение каждого сервиса из его меток compose `restic.*` (`restic.enable`, `restic.backup.paths`, …), делает снимок объединения разрешённых путей хоста в одном запуске restic (`--host` = `APEX_NODE_HOST`, `--compression max`), применяет политику хранения (`forget --prune`) и уведомляет. Он отказывается сообщать об успехе, если ни один сервис с меткой `restic.*` не создал нового снимка. Полный процесс описан в [Concept: Tiered storage](/apex/concepts/tiered-storage#backups). Запланируйте его в `configs/cron/crontab`, чтобы запуск и его результат в Telegram проходили без надзора.
## Синхронизация репозитория (capture-up) {#sync}
```bash
apex sync/repository <telegram-bot-url>
```
Коммитит рабочую копию в `sync/<node>`, выполняет rebase на `origin/main` и force-push с lease; CI сворачивает ветку обратно в `main`. «capture-up» — механизм захвата состояния узла обратно в репозиторий. Действие отказывается выполняться — с постраничной ошибкой — когда сабмодуль commons «грязный» или дерево `compositions/` отсутствует, и аккуратно прерывается при конфликте rebase (требуется ручное вмешательство). См. [Concept: Framework architecture](/apex/concepts/architecture#capture-up) для описания модели.
## Осведомлённость об обновлениях {#updates}
```bash
apex sync/packages <telegram-bot-url>
```
Сообщает об ожидающих apt-обновлениях и устаревших образах контейнеров (локальные дайджесты сравниваются с реестром через `skopeo`) в едином дайджесте Telegram. Он ничего не изменяет на хосте — это плановая проверка осведомлённости.
## Извлечение сертификатов {#certs}
```bash
apex utils/extract-traefik-certs <acme.json> <dest-dir> [telegram-bot-url]
```
Выгружает хранилище ACME граничного прокси в пары `fullchain.pem`/`privkey.pem` по каждому домену (через контейнеризованный dumper), ограничивает права на ключи до `600` и переназначает владельца вывода вызывающему пользователю — для сервисов, которым нужен «сырой» PEM-материал вместо TLS, терминируемого прокси.
---
**См. также:**
- [Getting started with an apex node](/apex/getting-started)
- [Reference: Actions](/apex/reference/actions)
- [Docs: Troubleshooting](/apex/troubleshooting)
@@ -0,0 +1,105 @@
---
title: Сборка репозитория узла
description: Соберите четыре опоры, зафиксируйте commons и подключите поток capture-up.
weight: 21
---
**Контекст:** каждый [узел](/apex/glossary#node) получает ровно один репозиторий. Выбирайте любое имя репозитория — [идентичность](/apex/glossary#node-identity) объявляется явно в `node.env`, поэтому имя репозитория — это соглашение, а не функциональный ввод. Это руководство собирает такой репозиторий с нуля.
{{% steps %}}
### Инициализируйте репозиторий и зафиксируйте commons
```bash
git init <node-repo> && cd <node-repo>
git submodule add <commons-url> commons
( cd commons && git fetch --tags && git checkout <release-tag> )
git add .gitmodules commons
mkdir -p proprietaries/actions proprietaries/lib configs compositions/apex .github/workflows
```
Gitlink фиксирует точную версию движка, которую запускает узел. Обновление в дальнейшем означает checkout более нового тега внутри `commons/` и коммит перемещённого указателя.
### Объявите идентичность
```ini {filename="node.env"}
APEX_NODE_FQDN=node1.example.com
APEX_NODE_HOST=node1.example.com
APEX_SUBNET=198.18.16.0/24
APEX_GIT_AUTHOR_NAME=apex [bot]
APEX_GIT_AUTHOR_EMAIL=apex@localhost
```
`node.env` объявляет идентичность узла явно — не предполагается никакой формы домена. `APEX_NODE_FQDN` — это реальный FQDN, используемый для маршрутизации traefik по `Host()` (fallback: имя хоста ОС, с предупреждением). `APEX_NODE_HOST` — это публичное имя хоста, используемое для `--host` резервного копирования и метки `instance` наблюдаемости/уведомлений (по умолчанию: `APEX_NODE_FQDN`); короткое `name` узла — это его первая метка. `APEX_SUBNET` — это /24 узла для сети `direct` и является **обязательным** — отсутствующее значение приводит к выходу с кодом `66` (см. [Concept: Fleet addressing scheme](/apex/concepts/addressing)). `APEX_GIT_AUTHOR_NAME`/`APEX_GIT_AUTHOR_EMAIL` — это идентичность бота capture-up (общие значения по умолчанию, если не заданы).
### Напишите базовую обёртку и её env-файлы
```yaml {filename="compositions/apex/docker-compose.yml"}
name: apex
include:
- ../../commons/compositions/apex/docker-compose.yml
```
```ini {filename="compositions/apex/apex.env"}
APEX_TRAEFIK_DIRECT_IP=198.18.16.10
APEX_RESTIC_REPOSITORY=s3:<endpoint>/<bucket>/<node>
COMPOSE_PROFILES=adguard,xray,alloy
# цели push-наблюдаемости (только когда включён профиль alloy)
APEX_PROM_PUSH_URL=https://<central-host>/api/v1/push
APEX_LOKI_PUSH_URL=https://<central-host>/loki/api/v1/push
APEX_OBS_BASIC_AUTH_USER=<node-user>
# опциональные скаляры со значениями compose по умолчанию:
# TZ=Etc/UTC
# APEX_RESTIC_COMPRESSION=max
# APEX_RESTIC_AWS_REGION=us-east-1 # задайте для любого не-AWS S3-хранилища
# APEX_RESTIC_AWS_ENDPOINT= # напр., URL S3-совместимого эндпоинта
```
`apex.env` содержит только коммитируемые несекретные скаляры. `COMPOSE_PROFILES` выбирает, какие опциональные базовые сервисы запускает этот узел — `adguard`, `xray`, `alloy` (агент push-наблюдаемости) и `manual` (одноразовые резервные копии) — и может быть пустым. Задокументируйте некоммитируемые секретные ключи в `compositions/apex/SECRETS.md`, чтобы оператор на свежем хосте знал, что заполнить в `.env` (`APEX_RESTIC_PASSWORD`, ключи доступа S3, `APEX_TRAEFIK_CF_DNS_API_TOKEN`, `APEX_TRAEFIK_ACME_EMAIL` и `APEX_OBS_BASIC_AUTH_PASS`, когда наблюдаемость включена).
### Игнорируйте то, чему никогда не место в git
```gitignore {filename=".gitignore"}
**/@tier1
**/@tier2
**/@tier3
**/.env
!compositions/apex/apex.env
__pycache__/
*.pyc
```
### Добавьте bootstrap-скрипт
`init.sh` подготавливает свежий клон на хосте: `git submodule update --init --recursive`, `git config core.hooksPath commons/githooks`, `git config --global --add safe.directory <repo>` и добавляет `<repo>/commons` в начало `PATH` в login-shell, чтобы `apex` разрешался. Пометьте его исполняемым.
### Заведите реестр адресации
Создайте `compositions/ADDRESSING.md` с подсетью узла (`APEX_SUBNET`) и зарезервированными диапазонами (`.10`–`.19` — core, `.20`+ — сервисы). Каждый [якорный IP](/apex/glossary#anchor-ip), выделяемый позднее, записывается здесь.
### Подключите CI capture-up
Добавьте плановый workflow, который сливает `sync/<node>` в `main`, когда ветка существует, коммитя под идентичностью бота. Выполняйте checkout **без** сабмодулей — слияние лишь перемещает указатель commons, а submodule-URL по SSH не извлекается с CI-токеном по умолчанию. См. [Concept: Framework architecture](/apex/concepts/architecture) для полного цикла [capture-up](/apex/glossary#capture-up).
### Проверьте
```bash
./commons/apex --help
```
В списке должно отображаться каждое действие из commons (плюс ваши proprietaries, после их добавления). Вне хоста — до заполнения `node.env` — ожидайте безобидное предупреждение о том, что `APEX_NODE_FQDN` не задан (движок откатывается к имени хоста ОС); `APEX_SUBNET` при этом всё равно должен быть задан, иначе движок завершится с кодом `66`. Закоммитьте собранный репозиторий в `main`.
{{% /steps %}}
## Заметки
- Опора `proprietaries/` может начинаться пустой (сохраняйте её присутствие с помощью файлов `.gitkeep`); движок корректно обрабатывает отсутствующий оверлей.
- Переносите источники конфигурации хоста в `configs/` (`ufw/`, `cron/crontab`, `systemd/`, `crowdsec/`) по мере того как узел принимает каждый шаг `configure/*` — действия потребляют `configs/` дословно через слой шаблонов.
---
**См. также:**
- [Getting started with an apex node](/apex/getting-started)
- [Concept: Framework architecture](/apex/concepts/architecture)
- [Guide: Add a service composition](/apex/guides/service-composition)
@@ -0,0 +1,101 @@
---
title: Добавление композиции сервисов
description: "Подключите новый проект к узлу: якорный IP, сети, уровни и линтинг."
weight: 23
---
**Контекст:** проект присоединяется к [узлу](/apex/glossary#node) как один каталог в `compositions/`, содержащий написанный вручную файл compose. Контракт фреймворка для этого файла невелик: статический [якорный IP](/apex/glossary#anchor-ip) в сети `direct`, ссылки на внешние сети по ключу и пути к данным относительно уровней.
{{% steps %}}
### Выделите якорный адрес
Выберите следующий свободный адрес `.20` или выше в /24 узла и запишите его в `compositions/ADDRESSING.md`. Адреса `.10`–`.19` зарезервированы для [базовой композиции](/apex/glossary#core-composition) (см. [Concept: Fleet addressing scheme](/apex/concepts/addressing)).
### Напишите файл compose
```yaml {filename="compositions/<project>/docker-compose.yml"}
name: <project>
services:
<anchor>:
image: ...
container_name: <project>-<anchor>
user: "${APEX_UID?error}:${APEX_GID?error}"
group_add:
- "${APEX_SHARED_GID?error}"
volumes:
- ./@tier1/<anchor>/config:/config
- ./@tier2/<anchor>/data:/data
networks:
direct:
ipv4_address: 198.18.16.20
enclave: {}
labels:
- "traefik.enable=true"
- "traefik.http.routers.<project>.rule=Host(`...`)"
- "traefik.http.services.<project>.loadbalancer.server.port=8080"
# намерение резервного копирования (resticontainer, определяется из меток restic.*)
- "restic.enable=true"
- "restic.backup.paths=/config"
restart: unless-stopped
<sidecar>:
image: ...
network_mode: "service:<anchor>"
networks:
direct:
external: true
enclave:
external: true
```
Применяемые соглашения:
- **Один якорь на проект** держит статический `ipv4_address` в `direct`; sidecar-контейнеры разделяют его сетевое пространство имён через `network_mode: service:<anchor>`.
- **На сети ссылаются по ключу** (`direct`, `enclave`, `socket`) с `external: true` — их определения принадлежат базовой композиции.
- **Членство в `enclave` плюс метки traefik** публикуют сервис через граничный прокси; прокси обнаруживает его через socket-прокси, никогда через опубликованные порты.
- **Пути к данным относительны уровням** (`./@tierN/...`) и разрешаются через симлинки, создаваемые `tiers/link`.
- **Резервное копирование включается по выбору через метки `restic.*`.** `apex backup/run` управляет [resticontainer](https://github.com/deytenit/resticontainer), который определяет, что снимать, из меток compose: `restic.enable=true` помечает сервис, `restic.backup.paths=<container-path>` называет монтирования для резервного копирования (resticontainer разрешает их в пути хоста). Сервис без меток `restic.*` просто не резервируется. Резервируйте конфигурацию только там, где данные восстановимы.
- Тройка `${APEX_UID...}` берётся из `.env` проекта, поддерживаемого `tiers/useradd`.
### Обеспечьте хранилище и идентичность
```bash
apex tiers/link <tier1> <tier2> <tier3>
apex tiers/useradd
apex tiers/chown
```
Повторный запуск тройки идемпотентен; он подхватывает новый проект, создаёт его каталоги уровней и `.env`, обеспечивает его пользователя `noroot-<project>` и применяет матрицу владения.
### Линтинг
```bash
apex utils/lint-docker-compose
```
Проверяет каждый якорный IP (корректность формы, уникальность, нахождение внутри подсети узла) и запускает линтер compose по репозиторию. Pre-commit-хук обеспечивает тот же линтинг для staged-файлов compose.
### Разверните
```bash
apex compose up
```
Базовая композиция уже поднята; новый проект стартует в своём алфавитном слоте. Проверьте с помощью `docker ps` и, если сервис проксируется, через его маршрут на границе.
{{% /steps %}}
## Заметки
- Внутренние секреты сервиса (например, пароль базы данных) размещаются в собственном `.env` проекта в хранилище уровня 1, рядом с автоматически внедряемым блоком UID.
- Никогда не определяйте `direct`/`enclave`/`socket` внутри файла проекта — второе определение будет конкурировать с базовой композицией за владение сетью.
---
**См. также:**
- [Concept: Compositions and the shared core](/apex/concepts/compositions)
- [Concept: Tiered storage and privilege isolation](/apex/concepts/tiered-storage)
- [Concept: Fleet addressing scheme](/apex/concepts/addressing)
@@ -0,0 +1,20 @@
---
title: Справочник
description: CLI, действия, переменные окружения и API движка.
weight: 30
---
Технический справочник по поверхности фреймворка операций над флотом apex: интерфейс командной строки, встроенные [действия](/apex/glossary#action), контракт окружения, который они потребляют, и API [движка](/apex/glossary#engine) на Python.
{{% cards %}}
{{< card link="cli" title="Справочник CLI" icon="terminal" subtitle="Вызов из командной строки, подкоманды и коды возврата." >}}
{{< card link="actions" title="Справочник действий" icon="play" subtitle="Встроенные действия и их аргументы." >}}
{{< card link="environment" title="Справочник окружения" icon="variable" subtitle="Переменные окружения, доступные действиям и композициям." >}}
{{< card link="engine-api" title="Справочник API движка" icon="code" subtitle="Модули Python, обеспечивающие работу движка." >}}
{{% /cards %}}
---
**См. также:**
- [GitHub: apex.ermnvldmr.com](https://github.com/deytenit/apex.ermnvldmr.com)
@@ -0,0 +1,123 @@
---
title: Справочник действий
description: "Каждое действие commons: аргументы и поведение."
weight: 32
---
Шестнадцать действий [«commons»](/apex/glossary#commons) (общего репозитория), по группам. Аргументы показаны в том виде, в котором их принимает парсер; необязательные аргументы заключены в квадратные скобки. Коды возврата следуют [соглашению движка](/apex/reference/cli#exit-codes).
## configure {#configure}
### configure
`apex configure [--dry-run]`
Запускает весь набор в фиксированном порядке — `base`, `ufw`, `crowdsec`, `cron`, `systemd`, `routing` — и останавливается на первом неудачном шаге, пробрасывая его код возврата. `--dry-run` передаётся тем шагам, которые его поддерживают.
### configure/base
`apex configure/base`
Проверка только для Debian (иначе выход с кодом `1`). Обновляет индекс пакетов, устанавливает необходимые пакеты (`curl`, `wget`, `git`, `rsyslog`), гарантирует, что rsyslog запущен, устанавливает `ufw`, если он отсутствует, и загружает вспомогательную утилиту `ufw-docker` в `~/.local/bin/ufw-docker` (пропускается, если она уже присутствует).
### configure/ufw
`apex configure/ufw [--dry-run]`
Применяет межсетевой экран узла из `configs/ufw/`: `host/host.rules` через обычный `ufw`, `host/before.rules`/`after.rules` как самовосстанавливающиеся управляемые блоки внутри `/etc/ufw/`, а `docker/*.rules` — через `ufw-docker`. Выходит с кодом `1`, когда отсутствует корень конфигурации, `66` — когда отсутствует ровно один из каталогов `docker/`/`host/`, `1` — когда целевой файл в `/etc/ufw` не существует. Требует предварительного выполнения `configure/base` (проверяет `ufw` и `ufw-docker`). Завершается напоминанием о необходимости перезапуска.
### configure/crowdsec
`apex configure/crowdsec [--dry-run]`
Устанавливает CrowdSec через официальный скрипт, если `cscli` отсутствует, рендерит `configs/crowdsec/` в `/etc/crowdsec/` (создавая резервные копии перезаписываемых файлов), устанавливает коллекции `crowdsecurity/linux` и `crowdsecurity/traefik`, а также устанавливает firewall-баунсер iptables, когда существует `configs/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml.local`, а пакет отсутствует. `--dry-run` рендерит без записи и пропускает шаги установки.
### configure/cron
`apex configure/cron [--dry-run]`
Рендерит `configs/cron/crontab` с помощью `ctx.vars()` и устанавливает его через `crontab -`. Отсутствующий исходный файл: выход `1`. Пустой результат рендеринга: выход `65`, предыдущий crontab сохраняется. Неудачная установка: предыдущий crontab восстанавливается, выход `1`. Результат рендеринга всегда выводится для проверки.
### configure/systemd
`apex configure/systemd [--dry-run]`
Рендерит каждый файл верхнего уровня (не скрытый) из `configs/systemd/` в `/etc/systemd/system/<name>`; для каждого юнита `.service` выполняет daemon-reload и включает его с `--now`. Отсутствующий каталог конфигурации: выход `1`.
### configure/routing
`apex configure/routing`
Включает `net.ipv4.ip_forward`, устанавливает правило политики fwmark `0x1` → routing-table-100 и локальный маршрут по умолчанию в этой таблице. Обе проверки идемпотентны — повторные запуски ничего не меняют.
## sync {#sync}
### sync/repository
`apex sync/repository <telegram_bot_url>`
Коммит [«capture-up»](/apex/glossary#capture-up) (захват изменений): переключается на (или создаёт) `sync/<node>`, добавляет всё дерево плюс обновлённый указатель на подмодуль commons, коммитит под идентичностью бота, если что-либо изменилось, выполняет rebase на `origin/main`, обновляет подмодули и пушит с `--force-with-lease`. Прерывается (выход `1`), когда подмодуль commons содержит незакоммиченные изменения или отсутствует `compositions/`; при конфликте rebase прерывает rebase и оповещает через Telegram. Успех и каждый путь отказа сопровождаются уведомлением; неудачная отправка уведомления об успехе также приводит к выходу с кодом `1`.
### sync/packages
`apex sync/packages <telegram_bot_url>`
Только для Debian. Собирает ожидающие обновления apt (`--just-print`) и устаревшие образы работающих контейнеров (локальный RepoDigest против дайджеста реестра через `skopeo`), затем отправляет одну сводку в Telegram. Только для чтения в отношении пакетов и образов. Необработанные сбои оповещают через Telegram и приводят к выходу `1`; так же ведёт себя и неудачная доставка сводки.
## backup {#backup}
### backup/run
`apex backup/run <telegram_bot_url>`
Управляет однократным запуском основного сервиса `resticontainer`, управляемого метками (профиль `manual`): проверка доступности репозитория с инициализацией при необходимости, затем один `backup`, в котором resticontainer обнаруживает каждый сервис, несущий метки compose `restic.*`, запускает их pre-хуки, останавливает все контейнеры, помеченные `restic.backup.stop`, разрешает помеченные монтирования контейнеров в пути на хосте и делает снимок объединения за один запуск restic. Снимок помечается `--host <APEX_NODE_HOST>`, `--tag biweekly`, `--compression max`. Затем защита подтверждает, что свежий снимок для этого хоста действительно создан — если метки `restic.*` не были обнаружены, резервное копирование является тихой пустой операцией, поэтому оно отказывается сообщать об успехе. Далее следует политика удержания через `forget --prune` (7 daily / 4 weekly / 12 monthly). Каждый сбой restic оповещает и приводит к выходу `1`; успех тоже оповещает, а неудачное уведомление об успехе приводит к выходу `1`.
## tiers {#tiers}
### tiers/link
`apex tiers/link <tier1_path> <tier2_path> <tier3_path>`
Строит всю структуру символьных ссылок [уровней](/apex/glossary#tier): ссылки уровня узла `compositions/@tierN`, каталоги уровней для каждого проекта, области `shared/` с обратными ссылками, ссылки `@tierN` для каждого проекта и символьную ссылку `.env` в уровень 1 (цель создаётся, если отсутствует). Существующие пути, не являющиеся символьными ссылками, запрашивают подтверждение; отказ пропускает создание этой ссылки.
### tiers/useradd
`apex tiers/useradd`
Гарантирует наличие группы `noroot-shared`, затем создаёт по одному заблокированному системному пользователю `noroot-<project>` на каждый проект композиции (собственная основная группа, без домашнего каталога, оболочка `nologin`, член общей группы) и внедряет `APEX_UID`/`APEX_GID`/`APEX_SHARED_GID` в `.env` каждого проекта между управляемыми маркерами.
### tiers/chown
`apex tiers/chown [tier1_path] [tier2_path] [tier3_path]`
Применяет матрицу владения (см. [Концепция: Многоуровневое хранилище](/apex/concepts/tiered-storage#ownership)) по корням уровней — по умолчанию используются ссылки уровней уровня узла. Требует наличия `noroot-shared` (иначе выход `1`); проекты с неполной идентичностью в `.env` пропускаются с предупреждением.
## utils {#utils}
### utils/extract-traefik-certs
`apex utils/extract-traefik-certs <source_acme> <dest_dir> [telegram_bot_url]`
Извлекает хранилище ACME в `fullchain.pem`/`privkey.pem` для каждого домена с помощью контейнеризованного дампера, затем выполняет `chmod 600` для ключей и переназначает владение выходным деревом вызывающему пользователю. Отсутствующий источник: выход `1`. Сбои внутри извлечения оповещают через Telegram, если был указан URL.
### utils/lint-docker-compose
`apex utils/lint-docker-compose [--hook]`
Два прохода: (1) проверка [якорных IP](/apex/glossary#anchor-ip) по `compositions/*/docker-compose.yml` — корректность формата, уникальность, нахождение внутри подсети узла; (2) линтер compose `dclint` с `--fix`, запускаемый в контейнере по репозиторию (ссылки уровней, `.env` и подмодуль commons исключены). `--hook` ограничивает проход 2 файлами compose в индексе и повторно добавляет в индекс то, что исправил линтер. Код возврата отражает оба прохода.
## compose {#compose}
### compose
`apex compose <action> [--dry-run] [extra...]`
Оркеструет `docker compose <action>` по всему узлу: сначала основной проект, затем сервисы в алфавитном порядке (обратном для `down`). `up` подразумевает `-d`; `.env`/`apex.env` передаются для каждого проекта при наличии; дополнительные аргументы пробрасываются; `--dry-run` (принимается в любой позиции) выводит команды вместо их выполнения.
---
**См. также:**
- [Справочник: CLI](/apex/reference/cli)
- [Руководство: Эксплуатация узла](/apex/guides/host-operations)
- [GitHub: apex.ermnvldmr.com](https://github.com/deytenit/apex.ermnvldmr.com)
@@ -0,0 +1,56 @@
---
title: Справочник CLI
description: "Лаунчер apex: перечисление, диспетчеризация, справка и коды возврата."
weight: 31
---
Лаунчер — это единый скрипт на Python из стандартной библиотеки, `commons/apex` (в `PATH` после `init.sh`). Всё, что он делает, нацелено на локальный checkout — аргумента узла не существует.
## Формы вызова {#invocation}
| Команда | Поведение |
|---|---|
| `apex` или `apex --help` | Перечислить каждое доступное [действие](/apex/glossary#action) с его кратким описанием; выход `0`. |
| `apex <group>/<action> [args...]` | Выполнить одно действие. |
| `apex <group>/<action> --help` | Вывести справку argparse для действия, сгенерированную из его `METADATA`; выход `0`. |
Перечисление помечает каждую запись источником [оверлея](/apex/glossary#overlay) — без метки (commons), `(local)`, `(local override)` — и добавляет `[DISABLED: reason]`, когда модуль объявляет `DISABLED`. Перечисление использует статическое сканирование `ast`, поэтому код действий не выполняется.
## Раскладка и разрешение путей {#layout}
При запуске лаунчер определяет, в какой из двух раскладок он находится:
- **Checkout узла** — лаунчер находится в каталоге с именем `commons/`, родитель которого содержит `node.env`. Родитель является корнем репозитория; `proprietaries/actions/` — это дерево оверлея; [идентичность узла](/apex/glossary#node-identity) разрешается перед диспетчеризацией, и любые предупреждения об идентичности логируются первыми.
- **Автономный commons** — любое другое расположение. Каталог commons является собственным корнем, оверлея нет, а `ctx.node` равен `None` (строки лога несут префикс `global`).
`sys.path` собирается так: каталог commons (делает `engine` импортируемым), затем `commons/vendor/` при наличии, затем `../proprietaries/` в checkout узла (делает проприетарный код `lib/` импортируемым).
## Коды возврата {#exit-codes}
| Код | Источник |
|---|---|
| `0` | Действие вернуло `0` или нецелое значение; пути `--help`. |
| `1` | Неизвестное действие; отключённое действие; необработанное исключение (логируется как `Type: message` с префиксом контекста); нецелый `SystemExit`. |
| `64` | Ошибка использования при разборе аргументов (`EX_USAGE`). |
| `65` | Ошибка данных, вызванная библиотеками движка (`EX_DATAERR`) — например, пустой отрендеренный crontab. |
| `66` | Отсутствующий ввод (`EX_NOINPUT`) — например, отсутствующие поля идентичности или отсутствующий каталог конфигурации ufw. |
Любой другой код — это то, что вернуло или вызвало само действие.
## Формат лога {#log-format}
Весь вывод движка и действий следует одной форме строки:
```text
[<node>] [<group>/<action>] [LEVEL] message
```
`LEVEL` — это `INFO`, `WARN`, `ERROR` или `SUCCESS`; строки `ERROR` идут в stderr, всё остальное — в stdout — удобно для cron как для захвата, так и для оповещения.
---
**См. также:**
- [Концепция: Действия и оверлей](/apex/concepts/actions-and-overlay)
- [Справочник: Действия](/apex/reference/actions)
- [Справочник: API движка](/apex/reference/engine-api)
@@ -0,0 +1,88 @@
---
title: Справочник API движка
description: API дескрипторов и ctx, доступные авторам действий.
weight: 34
---
Поверхность, с которой программирует автор [действия](/apex/glossary#action): дескрипторы аргументов в `engine.descriptor` и члены контекста выполнения. Концептуальная основа — в [Концепция: Контекст выполнения](/apex/concepts/execution-context).
## Дескрипторы {#descriptors}
```python
from engine.descriptor import Meta, Arg, Opt, Flag, Rest
```
| Класс | Сигнатура | Сопоставление с argparse |
|---|---|---|
| `Meta` | `Meta(summary, args=[])` | Управляет как кратким описанием в перечислении, так и парсером. |
| `Arg` | `Arg(name, help="", required=True)` | Позиционный; `required=False` → `nargs="?"`, по умолчанию `None`. |
| `Opt` | `Opt(name, help="", default=None)` | Опция со значением (`--name VALUE`) со значением по умолчанию. |
| `Flag` | `Flag(name, help="")` | Логический флаг `store_true`. |
| `Rest` | `Rest(name, help="")` | Жадный остаток (`argparse.REMAINDER`). |
Ошибки использования приводят к выходу `64` вместо стандартного для argparse `2`.
> [!WARNING]
> `Rest` захватывает *всё* после первого позиционного аргумента — включая флаги. `Flag`, объявленный рядом с `Rest`, распознаётся только *перед* позиционным аргументом; действие, которое должно принимать свой флаг в любом месте, обязано само вырезать его из остатка (действие `compose` делает именно это для `--dry-run`).
## ctx.log {#log}
`info(msg)` · `warn(msg)` · `success(msg)` → stdout; `error(msg)` → stderr. Все строки несут префикс `[<node>] [<action>] [LEVEL]`.
## ctx.sys {#sys}
| Метод | Поведение |
|---|---|
| `run(cmd, check=True, capture=False, cwd=None, env=None, input=None)` | Обёртка над `subprocess.run`, возвращающая `CompletedProcess`; `capture=True` собирает текстовые stdout/stderr. |
| `sudo(cmd, **kw)` | `run(["sudo", *cmd])`. |
| `ok(cmd, **kw)` | `True` тогда и только тогда, когда код возврата `0`; никогда не вызывает исключение. |
| `service_exists(name)` / `service_is_active(name)` / `service_is_enabled(name)` | Запросы к юнитам systemd. |
| `start_and_enable(name)` | Запуск + включение при необходимости; возвращает итоговое активное состояние. |
| `ensure_running(name)` | `enable --now`, когда неактивен. |
| `restart(name)` / `reload(name, dry_run=False)` / `daemon_reload()` | Жизненный цикл сервиса. |
| `wait_for(name, timeout=30, interval=2)` | Опрашивать, пока не станет активным. |
| `install_packages(packages)` | Для каждого пакета: пропустить, когда его бинарник в `PATH`, иначе `apt-get update -y` + `apt-get install -y`. |
## ctx.tpl {#tpl}
| Метод | Поведение |
|---|---|
| `render(path, vars)` | Отрендерить один файл (относительные пути разрешаются относительно `configs/`); семантика `safe_substitute` — неизвестные переменные остаются буквальными. |
| `render_dir(rel_dir, vars)` | Отрендерить каждый файл в поддереве `configs/`; возвращает `{relative_path: content}`. |
## ctx.host {#host}
| Метод | Поведение |
|---|---|
| `backup(path, sudo=False)` | Скопировать файл в `<path>.<epoch>.bak`; возвращает путь резервной копии или `None`. |
| `write_file(path, content, backup=True, dry_run=False, sudo=False)` | Атомарная установка через временный файл (режим `0644`); опциональная предварительная резервная копия; вариант `sudo` сохраняет режим существующей цели. |
| `inject_block(target, content, start, end, anchor=None, old_pairs=(), dry_run=False, sudo=True)` | Замена управляемого блока по маркерам; удаляет текущие и устаревшие блоки, вставляет после первой строки-якоря; `False`, когда якорь отсутствует; отсутствующая цель — ошибка. |
| `install_crontab(text, dry_run=False)` | Снимок, отказ при пустом рендере (`SystemExit(65)`), предпросмотр, установка через `crontab -`, восстановление снимка при сбое (`SystemExit(1)`). |
## ctx.ufw {#ufw}
`apply(rules_dir, config_dir=None, dry_run=False)` — полный проход межсетевого экрана: проверки каталогов (выход `66`), проверки инструментов (`ufw`, `ufw-docker`), правила хоста, внедрение управляемого блока в `/etc/ufw/before.rules` и `after.rules`, правила контейнеров из файлов `*.rules`.
## ctx.crowdsec {#crowdsec}
`ensure_installed()` · `deploy(configs_subdir, vars, dry_run=False)` · `install_collections(collections)`.
## ctx.systemd {#systemd}
`deploy_units(configs_subdir, vars, dry_run=False)` — отрендерить файлы верхнего уровня (не скрытые) поддерева в `/etc/systemd/system/`, затем выполнить daemon-reload и `enable --now` для каждого `.service`.
## ctx.notify {#notify}
`telegram(title, bot_url, message, node=None, level="INFO")` плюс сокращения `success` / `error` / `info` / `warn`. Все возвращают `bool` — `True` только когда сообщение было доставлено; пустой `bot_url` логирует предупреждение и возвращает `False`. Поле `instance` в оповещении по умолчанию равно публичному хосту узла (`APEX_NODE_HOST`) — префикс домена или узла не применяется.
## ctx.paths и ctx.node {#paths}
`ctx.paths`: `repo_root`, `commons`, `proprietaries`, `configs`, `compositions`, `core`, `tier1..3`, `stier1..3`, `root_tier1..3`. `ctx.node`: запись идентичности (`name`, `subnet`, `fqdn`, `host`) или `None` в checkout автономного commons. `ctx.vars()`: окружение шаблонов/compose (см. [Справочник: Переменные окружения](/apex/reference/environment)). `ctx.commons.run(args)`: делегирование затенённому действию commons — допустимо только в переопределяющем проприетарном модуле.
---
**См. также:**
- [Концепция: Контекст выполнения](/apex/concepts/execution-context)
- [Руководство: Разработка действий](/apex/guides/action-development)
- [GitHub: apex.ermnvldmr.com](https://github.com/deytenit/apex.ermnvldmr.com)
@@ -0,0 +1,78 @@
---
title: Справочник переменных окружения
description: Каждая переменная APEX_*, где она задаётся и кто её потребляет.
weight: 33
---
Пространство имён `APEX_*` — это вся поверхность конфигурации фреймворка. Переменные происходят из одного из четырёх слоёв; более поздние слои побеждают, когда действия оркестрации собирают окружение процесса.
## node.env — идентичность {#node-env}
Закоммичена в корне репозитория; читается при разрешении [идентичности узла](/apex/glossary#node-identity). Фреймворк агностичен к домену: узел объявляет свой собственный FQDN и публичное имя хоста — концепции кластера нет, и обязательной формы FQDN нет.
| Переменная | Значение | Примечания |
|---|---|---|
| `APEX_NODE_FQDN` | Реальный FQDN узла (маршрутизация Traefik `Host()`) | При отсутствии откатывается к имени хоста ОС (с предупреждением). |
| `APEX_NODE_HOST` | Публичное имя хоста узла (резервное копирование `--host`, `instance` для наблюдаемости/уведомлений) | По умолчанию равно `APEX_NODE_FQDN`. Его первая метка становится коротким `name`. |
| `APEX_SUBNET` | Сеть /24 узла для сети `direct` | Обязательна, единственный источник подсети. Отсутствие = выход `66`. |
| `APEX_GIT_AUTHOR_NAME` | Идентичность бота для коммитов capture-up | Общее значение по умолчанию (`apex [bot]`). |
| `APEX_GIT_AUTHOR_EMAIL` | Email идентичности бота для коммитов capture-up | Общее значение по умолчанию (`apex@localhost`). |
## apex.env — закоммиченные скаляры {#apex-env}
Закоммичены в `compositions/apex/`; несекретны по определению.
| Переменная | Значение | Примечания |
|---|---|---|
| `APEX_TRAEFIK_DIRECT_IP` | [Якорный IP](/apex/glossary#anchor-ip) краевого прокси в подсети узла | Требуется основной композицией (`?error`). |
| `APEX_RESTIC_REPOSITORY` | Локатор репозитория резервных копий | Обязательна (`?error`). |
| `APEX_RESTIC_COMPRESSION` | Уровень сжатия restic | Опциональна; по умолчанию `max` (политика флота — `backup/run` также принудительно задаёт `--compression max`). |
| `APEX_RESTIC_AWS_REGION` | Регион бэкенда S3 | Опциональна; по умолчанию `us-east-1`. |
| `APEX_RESTIC_AWS_ENDPOINT` | URL эндпоинта бэкенда S3 | Опциональна; по умолчанию пусто (AWS S3). Задавайте для любого не-AWS S3-совместимого хранилища. |
| `APEX_LOKI_PUSH_URL` | URL приёма Loki для агента [Alloy](/apex/concepts/architecture) (`loki.write`) | Требуется профилем `alloy` (`?error`). |
| `APEX_PROM_PUSH_URL` | URL `remote_write` Prometheus для агента Alloy | Требуется профилем `alloy` (`?error`). |
| `APEX_OBS_BASIC_AUTH_USER` | Пользователь basic-auth для целей отправки наблюдаемости | Требуется профилем `alloy` (`?error`). |
| `COMPOSE_PROFILES` | Подключаемые основные профили (`adguard`, `xray`, `alloy`) | Может быть пустой; `manual` активируется через `backup/run`, а не здесь. |
| `TZ` | Часовой пояс контейнера | Опциональна; по умолчанию `Etc/UTC`. |
## .env — секреты {#secrets}
Не закоммичен; символьная ссылка в хранилище уровня 1. Ключи документируются для каждого узла в `SECRETS.md`.
| Переменная | Значение | Примечания |
|---|---|---|
| `APEX_UID`, `APEX_GID`, `APEX_SHARED_GID` | Идентичность noroot проекта | Автоматически внедряются через `tiers/useradd`; обязательны (`?error`). |
| `APEX_RESTIC_PASSWORD` | Пароль репозитория резервных копий | Обязательна (`?error`). |
| `APEX_RESTIC_AWS_ACCESS_KEY_ID`, `APEX_RESTIC_AWS_SECRET_ACCESS_KEY` | Учётные данные объектного хранилища | Обязательны (`?error`). |
| `APEX_TRAEFIK_CF_DNS_API_TOKEN` | Токен DNS-challenge для ACME | Обязательна (`?error`). |
| `APEX_TRAEFIK_ACME_EMAIL` | Email учётной записи ACME | Обязательна (`?error`). |
| `APEX_OBS_BASIC_AUTH_PASS` | Пароль basic-auth для целей отправки наблюдаемости | Требуется профилем `alloy` (`?error`). |
| `APEX_LOKI_BASIC_AUTH` | Пользователи htpasswd для центрального приёма Loki/Prometheus | Не требуется основной композицией. Только на узле, который сам фронтит центральный приём наблюдаемости. |
## Экспорты движка — ctx.vars() {#engine}
Вычисляются [движком](/apex/glossary#engine) при вызове; экспортируются в `docker compose` действиями оркестрации и доступны каждому рендеру шаблона. Никогда не задавайте их вручную.
| Переменная | Значение |
|---|---|
| `APEX_NODE_HOST` | Публичное имя хоста узла (`node.host`); резервное копирование `--host` и `instance` для наблюдаемости/уведомлений. |
| `APEX_NODE_FQDN` | Реальный FQDN узла (`node.fqdn`); используется для правил краевой маршрутизации. |
| `APEX_SUBNET` | Подсеть узла. |
| `APEX_COMMONS` | Абсолютный каталог commons (`$APEX_COMMONS/apex` — это лаунчер, его используют crontab и юниты systemd). |
| `APEX_REPO_ROOT` | Абсолютный корень репозитория узла. |
| `APEX_TIER1`, `APEX_TIER2`, `APEX_TIER3` | Каталог данных основного проекта для каждого [уровня](/apex/glossary#tier). |
| `APEX_TIER1_SHARED`, `APEX_TIER2_SHARED`, `APEX_TIER3_SHARED` | Общая область для каждого уровня. |
| `APEX_TIER_ROOT1`, `APEX_TIER_ROOT2`, `APEX_TIER_ROOT3` | Корни хранилища уровней. |
> [!NOTE]
> Переменные, помеченные выше `?error`, обеспечиваются общей основной композицией: `docker compose` отказывается вычислять модель, пока они не заданы, что превращает забытый секрет в немедленный, явный сбой.
Несколько переменных основной композиции используют вместо этого `${VAR:-default}`, поэтому они опциональны и переопределяемы для каждого узла: `TZ` (`Etc/UTC`), `APEX_RESTIC_AWS_REGION` (`us-east-1`), `APEX_RESTIC_AWS_ENDPOINT` (пусто), `APEX_RESTIC_COMPRESSION` (`max`) и `APEX_ENCLAVE_SUBNET` (`198.18.0.0/16`, исходный диапазон ipallowlist для маршрутов панели Traefik/adguard).
---
**См. также:**
- [Концепция: Контекст выполнения](/apex/concepts/execution-context)
- [Концепция: Композиции и общее ядро](/apex/concepts/compositions)
- [Концепция: Разрешение идентичности узла](/apex/concepts/node-identity)
@@ -0,0 +1,79 @@
---
title: Устранение неполадок
description: Симптомы, диагностика и исправление типовых сбоев apex.
weight: 40
---
Режимы сбоев, о которых сообщает [движок](/apex/glossary#engine), с точными строками лога, которые он выводит.
## APEX_NODE_FQDN откатывается к имени хоста {#fqdn-fallback}
**Симптомы:** каждое действие пишет в лог `APEX_NODE_FQDN not set in node.env; using hostname '<fqdn>'.`
**Диагностика:** в `node.env` не задан `APEX_NODE_FQDN`, поэтому движок вывел FQDN из имени хоста ОС. На узле, чьё имя хоста и есть предполагаемый FQDN, это безобидно; в противном случае маршрутизация traefik `Host()` будет использовать неверное имя.
**Решение:** задайте `APEX_NODE_FQDN` явно в `node.env` в корне репозитория (например, `APEX_NODE_FQDN=node1.example.com`).
## Разрешение идентичности завершается с кодом 66 {#identity-66}
**Симптомы:** `identity: APEX_NODE_FQDN missing from node.env and no hostname` или `identity: APEX_SUBNET missing from node.env`, код выхода `66`.
**Диагностика:** обязательное поле идентичности не удаётся разрешить. Первая строка означает, что ни `APEX_NODE_FQDN`, ни имя хоста ОС не дают FQDN; вторая означает, что отсутствует `APEX_SUBNET`. Движок отказывается угадывать.
**Решение:** создайте или дополните `node.env` в корне репозитория — задайте `APEX_SUBNET` и задайте `APEX_NODE_FQDN` (либо убедитесь, что имя хоста ОС пригодно к использованию).
## configure/ufw завершается с кодом 66 {#ufw-66}
**Симптомы:** `Rules directory does not exist: ...` или `Config directory does not exist: ...`, код выхода `66`.
**Диагностика:** отсутствует ровно одна из директорий `configs/ufw/docker/` или `configs/ufw/host/` — это сломанная структура конфигурации, а не намеренно оставленный без правил узел.
**Решение:** восстановите отсутствующую директорию (пустая допустима: пустой `docker/` выводит предупреждение `No .rules files found ... nothing to apply.` и продолжает работу).
## Отсутствует инструментарий ufw {#ufw-tools}
**Симптомы:** `ufw not installed/working via sudo. Run configure/base first.` или `ufw-docker not found at ~/.local/bin/ufw-docker. Run configure/base first.`, код выхода `1`.
**Диагностика:** отсутствуют предварительные требования слоя фаервола.
**Решение:** запустите `apex configure/base`, затем повторите `apex configure/ufw`.
## Установка crontab отклонена или откачена {#cron}
**Симптомы:** `Rendered crontab is empty; aborting (previous kept).` (код выхода `65`) или `crontab install failed; restoring previous.` (код выхода `1`).
**Диагностика:** отрендеренный `configs/cron/crontab` оказался пустым (проблема шаблона/переменной — помните, что неизвестные `$VARS` остаются буквальными, они не обнуляют файл) или `crontab` отклонил синтаксис.
**Решение:** изучите выведенный предпросмотр, исправьте шаблон, повторно запустите `apex configure/cron`. Предыдущий crontab остаётся целым в обоих случаях.
## sync/repository прерывается {#sync-aborts}
**Симптомы:** `Commons submodule at <path> has uncommitted changes. Aborting.` или `Directory '<repo>/compositions' does not exist. Cannot sync.`, код выхода `1`.
**Диагностика:** коммит [capture-up](/apex/glossary#capture-up) отказывается фиксировать сломанный чекаут — грязный закреплённый субмодуль или отсутствующее дерево [композиций](/apex/glossary#compositions).
**Решение:** для грязного субмодуля — `git -C commons checkout -- .` (или закоммитьте намеренное перемещение пина); для отсутствующего дерева — восстановите чекаут до следующего планового запуска.
## Telegram не доставлен {#telegram}
**Симптомы:** `Telegram Bot URL not provided. Skipping notification.` или `Failed to send Telegram notification: <error>` — и действия, зависящие от уведомлений (`backup/run`, `sync/repository`, `sync/packages`), завершаются с кодом `1`, даже если их работа выполнена успешно.
**Диагностика:** аргумент с URL бота был пустым или отправка не удалась (сеть, прокси, отозванный токен). Ненулевой код выхода сделан намеренно: автоматический запуск, статус которого нельзя сообщить, считается неуспешным.
**Решение:** проверьте файл-источник URL, который читает строка cron, доступность сети/прокси и токен бота; повторите запуск вручную, чтобы подтвердить доставку.
## docker compose отклоняет базовую обёртку {#compose-include}
**Симптомы:** `docker compose` выдаёт ошибку на ключевом слове `include` при вычислении `compositions/apex/docker-compose.yml`.
**Диагностика:** [базовая композиция](/apex/glossary#core-composition) подключается через `include:`, что требует Docker Compose v2.20+.
**Решение:** обновите плагин Compose (`docker compose version` для проверки).
---
**См. также:**
- [Справочник: CLI](/apex/reference/cli)
- [Руководство: Эксплуатация узла](/apex/guides/host-operations)
- [GitHub: apex.ermnvldmr.com](https://github.com/deytenit/apex.ermnvldmr.com)
+6 -1
View File
@@ -1,12 +1,17 @@
---
title: Documentation of ermnvldmr.com root deployment commons
description: Документация общих модулей корневой инфраструктуры ermnvldmr.com.
weight: 1
weight: 2
type: docs
cascade:
type: docs
---
> [!WARNING]
> **Deprecated.** The root framework has been superseded by the
> [apex fleet operations framework](/en/apex) (documentation in English only). This
> section is retained for reference only and no longer receives updates.
Фреймворк `ermnvldmr.com-root-commons` предоставляет среду развертывания с минимальными зависимостями для независимых self-hosted проектов.
Общая логика, скрипты и шаблоны развертывания, используемые в различных приложениях, хранятся в репозитории [GitHub: ermnvldmr.com-root-commons](https://github.com/deytenit/ermnvldmr.com-root-commons).