fix: PageHead header alignment, analytics proxy, and infra improvements (#64)

🤖 Pull request created from `various-fixes` containing PageHead
breadcrumbs and navigation alignment fixes, Umami analytics proxy, and
infrastructure updates.
This commit is contained in:
deytenit authored and GitHub committed 2026-10-24 16:52:49 +03:00
1 parent ff5a50bad8
commit 9ee672504f
110 files changed
+2649 -1760

No files matched your search

@@ -1,20 +1,20 @@
---
title: Концепции
description: "Архитектура, идентичность, действия, контекст выполнения, композиции, хранилище, адресация и безопасность."
description: "Архитектура, идентичность, действия, контекст выполнения, композиции, хранилище, адресация и безопасность."
weight: 10
---
Теоретические основы фреймворка операций флота apex: как [узел](/apex/glossary#node) объявляет свою [идентичность](/apex/concepts/node-identity), как выполняются [действия](/apex/glossary#action) и как узел организует свои сервисы, хранилище и адресацию.
Теоретические основы фреймворка операций флота 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="Изоляция привилегий и усиление защиты сети." >}}
{{< 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 %}}
---
@@ -1,19 +1,19 @@
---
title: "Действия и оверлей"
description: "Контракт действия и как узло-локальные проприетарии затеняют действия commons."
description: "Контракт действия и затенение действий commons локальными проприетариями узла."
weight: 13
---
Каждая единица автоматизации в apex — это [действие](/apex/glossary#action): один Python-модуль, который [движок](/apex/glossary#engine) диспетчеризует по имени. [Оверлей](/apex/glossary#overlay) позволяет [проприетариям](/apex/glossary#proprietaries) узла добавлять, переопределять или отключать действия [«commons»](/apex/glossary#commons), не форкая их. Эта страница определяет контракт действия, декларативные дескрипторы аргументов и правила разрешения.
Каждая единица автоматизации в 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...]`. Нет реестра, который нужно обновлять — размещение файла в дереве публикует действие.
Действие — это отдельный файл `.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`.
- **`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
@@ -31,38 +31,38 @@ def run(ctx, args):
return 0
```
Возвращаемое значение `run` становится кодом завершения процесса. Нецелочисленный возврат (включая `None`) трактуется как успех (`0`). Необработанное исключение логируется с префиксом контекста и завершается с кодом `1`.
Возвращаемое значение `run` становится кодом завершения процесса. Нецелочисленный результат (включая `None`) трактуется как успешное завершение (`0`). Необработанное исключение логируется с префиксом контекста, после чего процесс завершается с кодом `1`.
> [!NOTE]
> `METADATA` технически необязательна — модуль без неё диспетчеризуется с пустым `Meta` и не принимает аргументов — но каждое настоящее действие должно её объявлять, поскольку вывод в перечислении и `--help` генерируется из неё.
> Технически `METADATA` необязательна: модуль без неё диспетчеризуется с пустым `Meta` и не принимает аргументов. Однако в реальных действиях её объявлять необходимо, поскольку вывод перечисления и справка `--help` генерируются именно из неё.
## Декларативные аргументы {#arguments}
`Meta.args` — это список дескрипторов. Каждый отображается ровно на одну конструкцию `argparse`:
`Meta.args` — это список дескрипторов. Каждый отображается ровно на одну конструкцию `argparse`:
| Дескриптор | Отображается на | Поведение |
| --- | --- | --- |
| `Arg("name")` | позиционный аргумент | Обязателен по умолчанию; `required=False` делает его опциональным позиционным (`nargs="?"`, по умолчанию `None`). |
| `Opt("--name", default=...)` | опция со значением | Опциональный флаг, принимающий значение, с объявленным значением по умолчанию. |
| `Arg("name")` | позиционный аргумент | Обязателен по умолчанию; `required=False` делает его опциональным позиционным (`nargs="?"`, по умолчанию `None`). |
| `Opt("--name", default=...)` | опция со значением | Опциональный флаг, принимающий значение, с объявленным значением по умолчанию. |
| `Flag("--name")` | булев флаг | `store_true`; наличие означает `True`. |
| `Rest("name")` | остаток | Жадная сквозная передача всех оставшихся argv (`argparse.REMAINDER`). |
| `Rest("name")` | остаток | Жадная сквозная передача всех оставшихся аргументов командной строки (`argparse.REMAINDER`). |
Движок строит парсер как `apex <group>/<action>` и парсит оставшиеся argv перед вызовом `run`. `--help` обрабатывается на этапе парсинга. Ошибка использования — неизвестный флаг, отсутствующий позиционный аргумент — печатает usage и завершается с кодом `64` (`EX_USAGE`) вместо стандартного для argparse `2`.
Движок строит парсер как `apex <group>/<action>` и разбирает оставшиеся аргументы перед вызовом `run`. Флаг `--help` обрабатывается на этапе парсинга. При ошибке использования (неизвестный флаг, отсутствующий позиционный аргумент) выводится справка по синтаксису (usage), а процесс завершается с кодом `64` (`EX_USAGE`) вместо стандартного для argparse кода `2`.
## Перечисление без импорта {#listing}
`apex` без аргументов (или `--help`) перечисляет каждое доступное действие. Перечисление никогда не импортирует модули действий: движок статически сканирует каждый файл модулем `ast` и извлекает только краткое описание `METADATA` и любую константу `DISABLED`. Поэтому код действия не может выполниться — или упасть — во время перечисления; файл, который не удаётся распарсить, перечисляется как `(unparseable)`, а не ломает перечисление.
Команда `apex` без аргументов (или с флагом `--help`) выводит список всех доступных действий. Перечисление никогда не импортирует модули действий: движок статически сканирует каждый файл с помощью модуля `ast` и извлекает только краткое описание `METADATA` и любую константу `DISABLED`. Поэтому код действия не выполняется и не может вызвать сбой во время перечисления: файл, который не удалось распарсить, помечается как `(unparseable)`, не нарушая работу всей команды.
Каждая запись помечается своим источником оверлея:
- нет метки &rarr; действие commons,
- `(local)` &rarr; существует только в проприетариях,
- `(local override)` &rarr; действие проприетариев, затеняющее действие commons,
- `[DISABLED: reason]` &rarr; действие объявляет `DISABLED`.
- нет метки &rarr; действие commons;
- `(local)` &rarr; существует только в проприетариях;
- `(local override)` &rarr; действие проприетариев, затеняющее действие commons;
- `[DISABLED: reason]` &rarr; действие объявляет константу `DISABLED`.
## Оверлей {#overlay}
Имена действий разрешаются по двум деревьям, сначала проприетарии:
Имена действий разрешаются по двум деревьям, начиная с проприетариев:
{{< filetree/container >}}
{{< filetree/folder name="node repository" state="open" >}}
@@ -81,26 +81,26 @@ def run(ctx, args):
{{< /filetree/folder >}}
{{< /filetree/container >}}
Движок определяет эту схему по своему собственному расположению: когда он запускается из каталога `commons/`, родитель которого содержит `node.env`, родитель является корнем репозитория [узла](/apex/glossary#node), а `proprietaries/actions/` — деревом оверлея. Автономный checkout commons является собственным корнем и не имеет слоя оверлея. Внутри репозитория узла [идентичность узла](/apex/glossary#node-identity) разрешается из FQDN хоста перед диспетчеризацией.
Движок определяет структуру размещения по своему расположению на диске: если запуск происходит из каталога `commons/`, а в родительском каталоге присутствует `node.env`, родительский каталог считается корнем репозитория [узла](/apex/glossary#node), а `proprietaries/actions/` — деревом оверлея. Автономный checkout commons сам выступает в качестве корня и не имеет слоя оверлея. Внутри репозитория узла [идентичность узла](/apex/glossary#node-identity) разрешается из FQDN хоста перед диспетчеризацией.
Разрешение для заданного имени проверяет `proprietaries/actions/<name>.py` первым и `commons/actions/<name>.py` вторым; ровно один модуль загружается по пути. Неизвестное имя — это ошибка (код завершения `1`). Оверлей поддерживает четыре приёма:
При разрешении имени движок сначала проверяет `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`; пустая строка не отключает.
- **Добавление (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` или нецелочисленное значение. |
| `0` | успех | Функция `run` вернула `0` или нецелочисленное значение. |
| `1` | общая ошибка | Неизвестное действие, отключённое действие, необработанное исключение, нецелочисленный `SystemExit`. |
| `64` | ошибка использования | Аргументы отклонены парсером (`EX_USAGE`). |
| `65` | ошибка данных | Некорректные входные данные (`EX_DATAERR`), поднимается библиотеками движка. |
| `66` | отсутствующий ввод | Требуемый ввод отсутствует (`EX_NOINPUT`), поднимается библиотеками движка. |
| `65` | ошибка данных | Некорректные входные данные (`EX_DATAERR`), выбрасывается библиотеками движка. |
| `66` | отсутствующий ввод | Требуемый ввод отсутствует (`EX_NOINPUT`), выбрасывается библиотеками движка. |
---
@@ -1,42 +1,42 @@
---
title: "Схема адресации флота"
description: "Подсети /24 для каждого узла и статические якорные IP внутри пространства анклава (по умолчанию 198.18.0.0/16)."
description: "Подсети /24 для каждого узла и статические якорные IP внутри пространства анклава (по умолчанию 198.18.0.0/16)."
weight: 17
---
Apex назначает каждому сервису фиксированный, предсказуемый IPv4-адрес внутри подсети узла. Каждый узел объявляет собственный `/24`, якоря сервисов — это статические адреса внутри него, а вся схема механически принудительно проверяется линтером якорных IP.
Фреймворк 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`.
Пространство [анклава](/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`), но фреймворк не накладывает на этот выбор никакой арифметики.
Каждый [узел](/apex/glossary#node) объявляет ровно один диапазон `/24` как `APEX_SUBNET` в своём файле `node.env`. Эта подсеть служит пулом IPAM сети `direct`. Оператор назначает её вручную: по соглашению она располагается внутри пространства анклава (`198.18.0.0/16` по умолчанию или в подсети из `APEX_ENCLAVE_SUBNET`), однако фреймворк не накладывает строгих ограничений на выбор адресов.
> [!NOTE]
> В качестве опционального соглашения оператора вы можете организовать `/24` узлов в непрерывные блоки (например, группируя связанные узлы), чтобы сохранять читаемость распределений. Это вопрос учёта, а не правило фреймворка.
> В качестве соглашения оператор может группировать подсети `/24` связанных узлов в непрерывные блоки, чтобы сохранять наглядность адресного плана. Это вопрос внутренней организации, а не жёсткое требование фреймворка.
## Распределение внутри `/24` узла {#node-allocation}
Внутри `/24` узла адреса хостов разделяются по ролям:
Внутри подсети `/24` узла адреса хостов разделяются по ролям:
| Диапазон | Назначение |
|---|---|
| `.10`–`.19` | Зарезервировано для якорей [базовой композиции](/apex/glossary#core-composition); `traefik` всегда `.10` (через `APEX_TRAEFIK_DIRECT_IP`) |
| `.20` и выше | Якоря сервисов, распределяемые по `<project>` |
| `.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`, который служит человекочитаемым реестром подсети узла.
Каждый якорь сервиса представляет собой статический `ipv4_address` в сети direct — фиксированный [якорный IP](/apex/glossary#anchor-ip), а не динамическую аренду (lease). Назначенные адреса фиксируются в `compositions/ADDRESSING.md`, который служит человекочитаемым реестром подсети узла.
## Принудительная проверка {#enforcement}
## Автоматическая проверка {#enforcement}
Линтер compose проверяет якорные IP по всем [композициям](/apex/glossary#compositions) на узле. Для каждого `ipv4_address`, найденного в `compositions/*/docker-compose.yml`, он проверяет, что адрес:
Линтер compose проверяет якорные IP-адреса во всех [композициях](/apex/glossary#compositions) узла. Для каждого значения `ipv4_address`, найденного в файлах `compositions/*/docker-compose.yml`, проверяются следующие условия:
- **Корректно сформирован** — некорректный литерал сообщается как ошибка линтера, а не крах.
- **Уникален** — один и тот же якорный IP не может встречаться в двух проектах; дубликаты сообщаются с указанием обоих конфликтующих проектов.
- **Внутри подсети узла** — каждый якорь должен попадать в объявленный `/24` узла.
- **Корректный формат** — о некорректном литерале линтер сообщает как об ошибке, не падая аварийно.
- **Уникальность** — один и тот же якорный 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).
При любом нарушении линтер завершается с ошибкой и кодом `1`. При успешной проверке в лог выводится сообщение `Anchor IP validation passed.` Логика проверки реализована в [действии](/apex/glossary#action) `utils/lint-docker-compose`; см. [GitHub: apex.ermnvldmr.com](https://github.com/deytenit/apex.ermnvldmr.com).
---
@@ -1,10 +1,10 @@
---
title: Архитектура фреймворка
description: Четыре опоры репозиториев узла, субмодуль «commons» и поток диспетчеризации.
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).
Фреймворк 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}
@@ -23,16 +23,16 @@ apex разделяет автоматизацию флота на один об
| Опора | Содержимое | Владение |
|---|---|---|
| `commons/` | Git-субмодуль → `apex.ermnvldmr.com`, закреплённый на релизном теге | Общее, версионируемое |
| `proprietaries/` | Узло-локальные действия, библиотеки и исходники инструментов ([накладываются оверлеем](/apex/glossary#overlay) поверх commons) | Узел |
| `commons/` | Git-субмодуль → `apex.ermnvldmr.com`, закреплённый на релизном теге | Общее, версионируемое |
| `proprietaries/` | Узло-локальные действия, библиотеки и исходники инструментов ([накладываются оверлеем](/apex/glossary#overlay) поверх commons) | Узел |
| `configs/` | Исходники конфигурации хоста: `ufw/`, `cron/crontab`, `systemd/`, `crowdsec/` | Узел |
| `compositions/` | По одному каталогу на проект плюс ядровая обёртка `apex/` и реестр адресации | Узел |
| `compositions/` | По одному каталогу на проект, ядровая обёртка `apex/` и реестр адресации | Узел |
Закрепление субмодуля — это версионный контракт фреймворка: узел выполняет ровно тот движок и ту ядровую композицию, которые записаны в его gitlink. Обновление узла — это осознанное действие: переключитесь на более новый тег внутри `commons/` и зафиксируйте перемещённый указатель.
Фиксация субмодуля — версионный контракт фреймворка: узел выполняет именно ту версию движка и ядровой композиции, которая зафиксирована в его gitlink. Обновление узла выполняется явно: достаточно переключиться на новый тег внутри `commons/` и закоммитить изменённый указатель.
## Поток диспетчеризации {#dispatch}
Лаунчер `commons/apex` действует на локальный checkout; аргумента узла нет нигде.
Лаунчер `commons/apex` работает с локальным чекаутом; передавать узел в качестве аргумента не нужно нигде.
```mermaid
flowchart TD
@@ -47,27 +47,27 @@ flowchart TD
H --> I["run(ctx, args)"]
```
Движок определяет схему размещения по своему собственному расположению: запуск из каталога с именем `commons/`, родитель которого содержит `node.env`, означает «checkout узла» — родитель становится корнем репозитория, а `proprietaries/actions/` становится деревом оверлея. Любое другое расположение — это автономный checkout commons, выступающий в роли собственного корня.
Движок определяет режим работы по своему расположению на диске. Если он запущен из каталога `commons/`, а в родительском каталоге присутствует `node.env`, то родительский каталог принимается за корень репозитория узла, а `proprietaries/actions/` становится деревом оверлея. При любом другом расположении commons считается автономным чекаутом и выступает собственным корнем.
Перечисление (`apex --help`) никогда не импортирует модули действий; краткие описания и маркеры отключённых действий извлекаются статическим сканированием через `ast`. Диспетчеризация загружает ровно один модуль по пути. Подробности в [Концепция: Действия и оверлей](/apex/concepts/actions-and-overlay).
Команда перечисления (`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` под идентичностью бота.
1. Регулярная задача `sync/repository` на хосте коммитит рабочее дерево в ветку `sync/<node>` (включая указатель субмодуля commons), делает rebase поверх `origin/main` и выполняет push с флагом `--force-with-lease`.
2. Запланированное CI-задание в репозитории узла вливает ветку `sync/<node>` обратно в `main` от имени бота.
Этот цикл «capture-up» ([capture-up](/apex/glossary#capture-up)) означает, что дрейф на хосте превращается в коммит — видимый, поддающийся сравнению через diff и откату — вместо тихого расхождения. Синхронизация прерывается вместо фиксации, если субмодуль commons «грязный» или дерево `compositions/` отсутствует, поэтому сломанный checkout никогда не становится записанным состоянием.
Благодаря циклу [«capture-up»](/apex/glossary#capture-up) любой дрейф конфигурации на хосте превращается в коммит: изменение наглядно отображается в diff и легко откатывается, исключая незаметные расхождения. Если субмодуль commons имеет незакоммиченные изменения («грязный») или каталог `compositions/` отсутствует, синхронизация прерывается без коммита, поэтому повреждённое состояние чекаута никогда не попадёт в историю.
> [!NOTE]
> CI-задание слияния выполняет checkout репозитория *без* субмодулей: субмодуль commons использует SSH-URL, который стандартный CI-токен не может получить, и слияние в любом случае лишь перемещает указатель gitlink.
> CI-задание слияния клонирует репозиторий *без* субмодулей: субмодуль commons подключён по SSH-URL, недоступному стандартному токену CI, а самому слиянию требуется лишь обновить указатель gitlink.
## Проектные ограничения {#constraints}
- **Только stdlib.** Движок работает на штатном Python 3 из Debian — без `pip`, без сторонних импортов. Опциональный каталог `vendor/` существует для закреплённого чистого Python-кода, но поставляется пустым.
- **Composes, написанные вручную.** Нет слоя манифестов или генерации кода; compose-файлы являются источником истины.
- **Свидетельство важнее соглашения.** Всё, что может быть обеспечено принудительно, обеспечивается действием (линт якорного IP, откат crontab, самовосстановление блоков-маркеров), а не документацией.
- **Только стандартная библиотека (stdlib).** Движок работает на штатном Python 3 из Debian — без `pip` и сторонних зависимостей. Каталог `vendor/` зарезервирован для зафиксированного чистого Python-кода, но по умолчанию поставляется пустым.
- **Ручные compose-файлы.** Никаких промежуточных манифестов или кодогенерации: файлы compose служат единственным источником истины.
- **Автоматическая проверка вместо соглашений.** Всё, что можно проверить или применить автоматически, выполняется действием (проверка якорных IP, откат crontab, самовосстановление блоков-маркеров), а не остаётся на совести документации.
---
@@ -1,14 +1,14 @@
---
title: Композиции и общее ядро
description: Ядровая композиция на основе include, профили, сети и наслоение окружения.
title: Композиции и общее ядро
description: Ядровая композиция на основе include, профили, сети и наслоение окружения.
weight: 15
---
Среда выполнения узла объявляется как [композиции](/apex/glossary#compositions): по одному docker-compose-проекту на сервис в каталоге `compositions/<project>/`, плюс [ядровая композиция](/apex/glossary#core-composition) — граничные и инфраструктурные сервисы, необходимые каждому узлу. Ядро располагается в [«commons»](/apex/glossary#commons) и потребляется каждым узлом через тонкую обёртку, поэтому его определение написано один раз и версионируется вместе с фреймворком.
Среда выполнения узла описывается в виде [композиций](/apex/glossary#compositions): по одному docker-compose-проекту на сервис в каталоге `compositions/<project>/`, плюс [ядровая композиция](/apex/glossary#core-composition) — граничные и инфраструктурные сервисы, необходимые каждому узлу. Ядро находится в [«commons»](/apex/glossary#commons) и подключается каждым узлом через тонкую обёртку. Благодаря этому его манифест пишется один раз и версионируется вместе с фреймворком.
## Обёртка include {#include}
Файл `compositions/apex/docker-compose.yml` каждого узла состоит из трёх строк:
Файл `compositions/apex/docker-compose.yml` каждого узла состоит всего из трёх строк:
```yaml {filename="compositions/apex/docker-compose.yml"}
name: apex
@@ -16,9 +16,9 @@ include:
- ../../commons/compositions/apex/docker-compose.yml
```
Общий файл полностью параметризован через `${APEX_*}` и не содержит относительных хостовых путей — каждое монтирование разрешается через переменные, которые [движок](/apex/glossary#engine) экспортирует при вызове, поэтому один и тот же файл корректен на каждом узле.
Общий файл полностью параметризован переменными `${APEX_*}` и не содержит относительных путей хоста: все точки монтирования разрешаются через переменные, экспортируемые [движком](/apex/glossary#engine) при вызове, поэтому один и тот же файл корректно работает на каждом узле.
Структурное исключение — узел, которому действительно нужно изменить ядровой сервис, например опубликовать дополнительный хостовый порт, — наслаивает переопределение через форму include со списком путей вместо редактирования общего файла:
Если узлу требуется изменить ядровой сервис (например, опубликовать дополнительный порт хоста), переопределение накладывается через список путей в директиве `include:` без редактирования общего файла:
```yaml {filename="compositions/apex/docker-compose.yml"}
name: apex
@@ -29,54 +29,54 @@ include:
```
> [!NOTE]
> Директива `include:` требует Docker Compose v2.20 или новее.
> Директива `include:` требует Docker Compose версии v2.20 или новее.
## Ядровые сервисы и профили {#core-services}
Ядровая композиция определяет общий набор сервисов флота. Всегда включённые сервисы:
- **traefik** — граничный обратный прокси и якорь сети `direct` (всегда `.10` в подсети узла). Он завершает TLS через DNS-челлендж, обнаруживает бэкенды через docker-метки и выставляет свою панель управления на FQDN узла за списком разрешённых IP [анклава](/apex/glossary#enclave).
- **socket-proxy** — отфильтрованный прокси для docker-сокета; traefik читает состояние контейнеров через него и никогда не обращается к сокету напрямую.
- **traefik** — граничный обратный прокси и якорь сети `direct` (всегда занимает `.10` в подсети узла). Он терминирует TLS через DNS-челлендж, обнаруживает бэкенды по docker-меткам и открывает панель управления на FQDN узла, ограничивая доступ списком разрешённых IP-адресов [анклава](/apex/glossary#enclave).
- **socket-proxy** — отфильтрованный прокси для docker-сокета: traefik получает состояние контейнеров через него и никогда не обращается к сокету напрямую.
Опциональные сервисы, управляемые через compose-профили, которые узел включает в `COMPOSE_PROFILES` (задаётся в `apex.env`):
Опциональные сервисы управляются 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.*`. |
| `xray` | xray | Прокси-шлюз с режимом host-network для анклава. |
| `adguard` | adguard | DNS-фильтрация в сетевом пространстве имён traefik; маршрутизируется по пути `/adguard` на FQDN узла. |
| `alloy` | alloy | Агент наблюдаемости на каждом узле (Grafana Alloy). Собирает метрики хоста и контейнеров, а также логи systemd 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`.
Ядровая композиция создаёт три сети узла, а сервисные композиции подключаются к ним через директиву `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. |
| `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>`) вместо самостоятельного подключения к сетям.
Внутри проекта зависимые контейнеры обычно используют сетевое пространство имён якоря (`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()`, внедряемые в окружение процесса действиями оркестрации и потому выигрывающие у обоих файлов.
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` в общем файле, поэтому отсутствующий секрет громко проваливает загрузку конфигурации композиции вместо запуска неправильно настроенного контейнера.
Критические переменные снабжены суффиксом `?error` в общем файле: если секрет не задан, загрузка конфигурации композиции завершается явной ошибкой, предотвращая запуск некорректно настроенного контейнера.
## Оркестрация {#orchestration}
`apex compose <action>` управляет каждой композицией на узле:
Команда `apex compose <action>` управляет всеми композициями на узле:
- **Порядок** — сначала ядровой проект (он владеет сетями), затем каждая сервисная композиция в алфавитном порядке; `down` разворачивает последовательность.
- **Env-файлы** — для каждого проекта `.env` и `apex.env` передаются как `--env-file`, если они присутствуют.
- **`up`** подразумевает `-d`; любые дополнительные аргументы передаются в `docker compose`.
- **`--dry-run`** печатает каждую команду с её рабочим каталогом вместо выполнения — принимается в любом месте командной строки.
- **Порядок:** сначала ядровой проект (он владеет сетями), затем остальные сервисные композиции по алфавиту; команда `down` выполняет действия в обратном порядке.
- **Env-файлы:** для каждого проекта существующие файлы `.env` и `apex.env` передаются через флаг `--env-file`.
- **Команда `up`:** по умолчанию подразумевает флаг `-d`; любые дополнительные аргументы передаются напрямую в `docker compose`.
- **Флаг `--dry-run`:** выводит каждую команду с её рабочим каталогом без выполнения; поддерживается в любом месте командной строки.
---
@@ -1,66 +1,66 @@
---
title: Контекст выполнения
description: ctx — единый шов между действием и хостом.
description: ctx — единый интерфейс взаимодействия между действием и хостом.
weight: 14
---
Каждое [действие](/apex/glossary#action) получает объект `ctx` в качестве первого аргумента. Это единственный санкционированный способ взаимодействовать с хостом: логирование, выполнение процессов, рендеринг шаблонов, изменение файлов, управление фаерволом и сервисами, а также уведомления — всё проходит через него. Поэтому действия остаются декларативными, а поведение, которое должно быть согласованным по всему флоту (формат логов, дисциплина резервного копирования, полезная нагрузка уведомлений), находится в одном месте — [движке](/apex/glossary#engine).
Каждое [действие](/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. |
| `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).
Сигнатуры методов подробно описаны в разделе [Справочник: API движка](/apex/reference/engine-api).
## Семантика шаблонов {#templates}
`ctx.tpl` рендерит файлы из `configs/` с помощью питоновского `string.Template.safe_substitute`:
Модуль `ctx.tpl` рендерит файлы из `configs/` с помощью `string.Template.safe_substitute`:
- Известные ссылки `$VAR` и `${VAR}` заменяются.
- **Неизвестные ссылки остаются буквальными.** Опечатка в шаблоне сохраняется в отрендеренном выводе, где она видна, вместо того чтобы молча свернуться в пустую строку.
- Подстановка команд shell (`$(...)`) проходит нетронутой, что сохраняет целостность записей crontab со встроенным `$(cat ...)`.
- Известные переменные `$VAR` и `${VAR}` подставляются.
- **Неизвестные переменные сохраняются буквально:** опечатка в имени переменной останется в готовом файле на виду, а не превратится незаметно в пустую строку.
- Подстановки команд shell (`$(cmd)`) не затрагиваются, что сохраняет целостность строк crontab со встроенными командами вида `$(cat ...)`.
Набор подстановки — это `ctx.vars()`: полное окружение процесса плюс предоставляемые движком значения `APEX_*`, приведённые ниже. Циклы и условия никогда не живут в шаблонах; их место — в действии.
Подстановка выполняется по словарю `ctx.vars()`: это полное окружение процесса плюс переменные `APEX_*`, предоставляемые движком (перечислены ниже). В шаблонах нет циклов и условий — вся логика ветвления находится в действиях.
## Экспорт переменных {#vars}
`ctx.vars()` возвращает окружение, используемое для каждого рендеринга шаблона, а его подмножество `APEX_*` экспортируется в `docker compose` действиями оркестрации. На узле оно содержит:
Функция `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_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_TIER_ROOT1`…`APEX_TIER_ROOT3` | Корневые каталоги хранилищ уровней (ссылки уровня узла). |
Полная матрица источников/потребителей приведена в [Справочник: Переменные окружения](/apex/reference/environment).
Полная матрица источников и потребителей переменных приведена в разделе [Справочник: Переменные окружения](/apex/reference/environment).
## Дисциплина изменения хоста {#host}
`ctx.host` кодирует правила записи фреймворка:
Объект `ctx.host` реализует строгие правила записи:
- **Атомарная запись.** `write_file` рендерит во временный файл (режим `0644`, соответствующий тому, что создал бы редирект root-шелла) и устанавливает его через `mv` или `sudo cp`; перезапись существующего файла сохраняет режим этого файла.
- **Резервные копии с меткой времени.** По запросу предыдущий файл сохраняется рядом с целевым как `<name>.<epoch>.bak` перед записью.
- **Инъекция блоков-маркеров.** Управляемые блоки внутри системных файлов (например, `/etc/ufw/before.rules`) ограничиваются начальными/конечными комментариями-маркерами. Инъекция удаляет каждый предыдущий управляемый блок — включая блоки, оставленные предшествующим инструментарием, — перед вставкой свежего после его якорной строки, поэтому повторные запуски никогда не накапливаются.
- **Откат crontab.** `install_crontab` делает снимок текущего crontab, отказывается от пустого рендера (код выхода `65`), показывает предпросмотр результата и восстанавливает снимок, если установка не удалась.
- **Атомарная запись:** метод `write_file` сначала формирует временный файл (права `0644`, аналогичные перенаправлению вывода из-под root) и перемещает его на место через `mv` или `sudo cp`. При перезаписи существующего файла его исходные права сохраняются.
- **Резервные копии с временной меткой:** по запросу перед записью создаётся копия старого файла рядом с целевым: `<name>.<epoch>.bak`.
- **Внедрение блоков-маркеров:** изменения внутри системных файлов (например, `/etc/ufw/before.rules`) обрамляются комментариями-маркерами начала и конца. Перед вставкой нового блока после якорной строки движок удаляет все предыдущие управляемые блоки (включая оставшиеся от старого инструментария), предотвращая накопление дубликатов при повторных запусках.
- **Откат crontab:** метод `install_crontab` делает снимок текущего crontab, отклоняет пустой результат рендеринга (код выхода `65`), показывает diff изменений и автоматически восстанавливает снимок при ошибке установки.
## Уведомления {#notifications}
`ctx.notify` отправляет сообщения Telegram через стандартную библиотеку (`urllib`). Полезная нагрузка несёт имя инстанса — публичный хост узла `APEX_NODE_HOST` — статус (`firing`/`resolved`), уровень эскалации и сообщение, усечённое до 200 символов. Каждый метод возвращает `True` только тогда, когда отправка удалась; автоматические действия трактуют неудачную отправку своего финального сообщения о статусе как сбой (код выхода `1`), поэтому сломанный канал уведомлений замечается, а не молча терпится.
Модуль `ctx.notify` отправляет сообщения в Telegram средствами стандартной библиотеки (`urllib`). Полезная нагрузка содержит имя хоста (`APEX_NODE_HOST`), статус (`firing`/`resolved`), уровень важности и текст сообщения, обрезанный до 200 символов. Методы возвращают `True` только при успешной доставке; автоматические действия трактуют сбой отправки итогового статуса как критическую ошибку (код завершения `1`), поэтому проблемы со связью сразу становятся заметны.
---
@@ -1,22 +1,22 @@
---
title: "Разрешение идентичности узла"
description: "Как узел узнаёт, кто он: ключи идентичности node.env и запасной вариант FQDN."
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.
Каждая операция apex выполняется в контексте строго одного [узла](/apex/glossary#node). [Движок](/apex/glossary#engine) извлекает [идентичность узла](/apex/glossary#node-identity) — имя, подсеть, FQDN и публичный адрес — из файла `node.env` в корне репозитория. Системное имя хоста используется исключительно как резервный вариант для FQDN. Фреймворк не делает предположений о домене или наличии кластера: узел заявляет о себе явно. Этот механизм пришёл на смену функции `root_require_node` bash-фреймворка «root» commons, развитием которого стал apex.
## Источники идентичности {#sources}
### node.env
`node.env` — это обычный файл вида `KEY=value` (пригодный для `source` в shell), расположенный в корне репозитория. При разборе комментарии и пустые строки игнорируются, а окружающие значения одинарные или двойные кавычки удаляются.
Файл `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)).
- `APEX_NODE_FQDN` — канонический FQDN узла для правила `Host()` в traefik. Если ключ не задан, движок берёт имя хоста из ОС и выводит предупреждение.
- `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
@@ -26,49 +26,49 @@ APEX_SUBNET=198.18.16.0/24
### Имя хоста ОС
Имя хоста ОС используется только как запасной вариант для `APEX_NODE_FQDN`. Когда в `node.env` не задан `APEX_NODE_FQDN`, движок использует имя хоста и выводит предупреждение. Оно никогда не разбирается для получения имени узла или любой другой координаты.
Имя хоста операционной системы используется только как резервный вариант для `APEX_NODE_FQDN`. Если в `node.env` значение `APEX_NODE_FQDN` не указано, движок берёт имя хоста ОС и выводит предупреждение. Это значение никогда не парсится для вычисления короткого имени узла или других параметров.
## Порядок разрешения {#resolution}
{{% steps %}}
### Разрешите FQDN
### Определение FQDN
`fqdn` — это `APEX_NODE_FQDN` из `node.env` или имя хоста ОС, когда этот ключ не задан. Если ни то ни другое не даёт значения, разрешение завершается неудачей (см. [Режимы сбоев](#failures)).
Параметр `fqdn` принимает значение `APEX_NODE_FQDN` из `node.env` либо системное имя хоста ОС, если ключ не задан. Если оба источника пусты, разрешение завершается фатальной ошибкой (см. [Режимы сбоев](#failures)).
### Разрешите публичный хост
### Определение публичного хоста
`host` — это `APEX_NODE_HOST` из `node.env` или `fqdn`, когда этот ключ не задан.
Параметр `host` берётся из `APEX_NODE_HOST` в `node.env` либо приравнивается к вычисленному `fqdn`.
### Выведите короткое имя
### Вычисление короткого имени
`name` — это **первая метка** `host` (`host.split(".")[0]`) — например, `node1` для `node1.example.com`. Оно используется для логов, ветки `sync/<node>` и сообщений.
Значение `name` — это **первая часть** имени `host` (`host.split(".")[0]`), например `node1` для `node1.example.com`. Оно используется в логах, имени ветки `sync/<node>` и уведомлениях.
### Прочитайте подсеть
### Получение подсети
`subnet` всегда читается из `APEX_SUBNET` в `node.env`. Она обязательна.
Значение `subnet` всегда читается из переменной `APEX_SUBNET` файла `node.env`. Этот параметр строго обязателен.
{{% /steps %}}
Результат — запись `Node` с четырьмя полями: `name`, `subnet`, `fqdn` и `host`.
В результате формируется структура `Node` с четырьмя полями: `name`, `subnet`, `fqdn` и `host`.
## Предупреждения {#warnings}
Разрешение идентичности выводит ровно одно предупреждение; оно сигнализирует об исправимой несогласованности, которую следует устранить.
При разрешении идентичности может выводиться одно штатное предупреждение: оно указывает на неполную конфигурацию, которую стоит исправить.
| Условие | Сообщение предупреждения |
|---|---|
| `APEX_NODE_FQDN` не задан в `node.env` (вместо него используется имя хоста) | `APEX_NODE_FQDN not set in node.env; using hostname '<fqdn>'.` |
| `APEX_NODE_FQDN` не задан в `node.env` (используется имя хоста ОС) | `APEX_NODE_FQDN not set in node.env; using hostname '<fqdn>'.` |
## Режимы сбоев {#failures}
Разрешение отказывается угадывать. Два условия фатальны, и оба завершаются с кодом `66` — соглашение движка о коде выхода для отсутствующего ввода:
Движок никогда не пытается угадывать параметры наугад. Две ситуации считаются критическими и приводят к завершению с кодом `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 и завершается.
- **FQDN не определён:** ключ `APEX_NODE_FQDN` отсутствует в `node.env`, *а также* не задано имя хоста в ОС. Движок выводит в stderr сообщение `identity: APEX_NODE_FQDN missing from node.env and no hostname` и завершает работу.
- **Подсеть не задана:** ключ `APEX_SUBNET` отсутствует в `node.env` (либо сам файл отсутствует). Движок выводит в stderr сообщение `identity: APEX_SUBNET missing from node.env` и завершает работу.
> [!WARNING]
> Действительный `node.env`, несущий `APEX_SUBNET`, обязателен на каждом узле. `APEX_NODE_FQDN` можно опустить только тогда, когда имя хоста ОС предоставляет пригодный FQDN.
> Корректный файл `node.env` с заданным параметром `APEX_SUBNET` обязателен на каждом узле. Параметр `APEX_NODE_FQDN` допустимо опускать только в том случае, если системное имя хоста ОС возвращает валидный FQDN.
---
@@ -1,48 +1,48 @@
---
title: Модель безопасности
description: Изоляция привилегий, управление межсетевым экраном, обнаружение вторжений и обработка секретов.
description: Изоляция привилегий, управление межсетевым экраном, обнаружение вторжений и защита секретов.
weight: 18
---
apex укрепляет [узел](/apex/glossary#node) в независимых слоях: изоляция привилегий на уровне файловой системы, управляемый межсетевой экран хоста, обнаружение вторжений, ограничение на уровне контейнеров и строгая граница секретов. Каждый слой применяется [действием](/apex/glossary#action) и потому воспроизводим и повторно запускаем.
Фреймворк apex защищает [узел](/apex/glossary#node) на нескольких независимых уровнях: изоляция привилегий в файловой системе, управляемый межсетевой экран хоста, обнаружение вторжений, разграничение прав в контейнерах и строгая изоляция секретов. Каждый уровень настраивается отдельным [действием](/apex/glossary#action), что гарантирует воспроизводимость и идемпотентность конфигурации.
## Изоляция привилегий {#privilege}
Каждый проект выполняется под собственным заблокированным системным пользователем `noroot-<project>`; общее хранилище доступно на запись группе с установленным sticky-битом, поэтому проекты могут обмениваться файлами, не имея возможности удалять файлы друг друга. Полный механизм — пользователи, группы и матрица владения — описан в [Концепция: Многоуровневое хранилище](/apex/concepts/tiered-storage).
Каждый проект работает под изолированным системным пользователем `noroot-<project>` без права входа. Общее хранилище открыто на запись группе с установленным sticky-битом: проекты могут обмениваться файлами, но не могут удалять чужие данные. Подробное устройство механизма (пользователи, группы и матрица прав) описано в разделе [Концепция: Многоуровневое хранилище](/apex/concepts/tiered-storage).
## Межсетевой экран хоста {#firewall}
`apex configure/ufw` применяет межсетевой экран узла из опоры `configs/ufw/` в трёх частях:
Команда `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`.
- **Правила хоста** (`configs/ufw/host/host.rules`) — список команд `ufw` (по одной на строку), применяемых последовательно.
- **Правила уровня ядра** (`configs/ufw/host/before.rules`, `after.rules`) — внедряются в `/etc/ufw/before.rules` и `/etc/ufw/after.rules` в виде управляемых блоков-маркеров. Процесс внедрения **самовосстанавливающийся**: любые ранее добавленные блоки (включая оставшиеся от предыдущих утилит) удаляются перед вставкой актуального блока после якорной строки `# End required lines`. Рядом с изменяемым файлом сохраняется резервная копия `.bak` с временной меткой.
- **Правила для контейнеров** (`configs/ufw/docker/*.rules`) — применяются через утилиту `ufw-docker`, которая формирует правила iptables, устойчивые к перезапускам и манипуляциям самого Docker. Команда `configure/base` устанавливает этот инструмент в `~/.local/bin/ufw-docker`.
Отсутствующая конфигурация — это ошибка, а не тихий пропуск: отсутствующий каталог `docker/` или `host/` завершается с кодом `66`, если существует его аналог, а отсутствующий целевой файл `/etc/ufw` прерывает запуск. Действие завершается напоминанием, что `ufw` должен быть перезапущен, чтобы изменения вступили в силу, — и что SSH-порт должен быть разрешён в первую очередь.
Отсутствующая конфигурация считается ошибкой, а не поводом для тихого пропуска: отсутствие каталогов `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, замыкая цикл от обнаружения к блокировке.
Команда `apex configure/crowdsec` автоматически устанавливает CrowdSec через официальный инсталлятор (если утилита `cscli` отсутствует), генерирует конфигурацию из `configs/crowdsec/` в `/etc/crowdsec/` (источники логов, парсеры, параметры bouncer) и устанавливает коллекции `crowdsecurity/linux` и `crowdsecurity/traefik`. Если конфигурация узла предусматривает firewall-bouncer, соответствующий пакет bouncer устанавливается автоматически, связывая обнаружение атак с немедленной блокировкой на уровне сети.
## Ограничение на уровне контейнеров {#containers}
[Ядровая композиция](/apex/glossary#core-composition) кодирует укрепление на уровне контейнеров:
[Ядровая композиция](/apex/glossary#core-composition) задаёт строгие ограничения для контейнеров:
- Граничный прокси никогда не обращается к Docker-сокету: он обнаруживает бэкенды через **отфильтрованный socket-proxy**, который выставляет состояние контейнеров/сервисов в режиме только для чтения в изолированной сети.
- `enclave` и `socket` — сети с `internal: true` — без исходящего трафика, без выставления хостовых портов.
- Ядровые сервисы устанавливают `no-new-privileges` и работают под noroot-идентичностью проекта везде, где это позволяет нагрузка; capabilities выдаются для каждого сервиса, а не глобально.
- Граничный прокси изолирован от сокета Docker: он получает данные о сервисах через **отфильтрованный socket-proxy**, который отдаёт состояние контейнеров только для чтения в замкнутой сети.
- Сети `enclave` и `socket` объявлены с параметром `internal: true`: у них нет прямого доступа в интернет и опубликованных портов хоста.
- Ядровые сервисы запускаются с опцией `no-new-privileges` под noroot-пользователем везде, где позволяет рабочая нагрузка; Linux 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`, который нужно было бы защищать.
Система мониторинга построена по модели **исходящей отправки (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`). Поскольку трафик отправляется наружу, Alloy подключён к сети `direct` с доступом в интернет, а не к закрытой сети `enclave`. На самих узлах логи не накапливаются, и открывать внешний доступ к эндпоинтам сервисов не требуется.
## Граница секретов {#secrets}
## Изоляция секретов {#secrets}
- Секреты живут только в файлах `.env` для каждого проекта, которые являются **символьными ссылками в хранилище уровня 1** — за пределами рабочего дерева репозитория и покрытыми `.gitignore` (`**/.env`).
- Каждый репозиторий узла документирует ожидаемые ключи в `SECRETS.md`; файл документирует имена, но никогда — значения.
- Закоммиченная конфигурация (`apex.env`) ограничена несекретными скалярными значениями.
- Общая композиция помечает критические секреты через `?error`, поэтому отсутствующее значение быстро проваливается на этапе загрузки конфигурации compose.
- Коммиты «capture-up» безопасны по построению: игнорируемые ссылки `.env` и данные уровней никогда не попадают в git-индекс.
- Секреты хранятся исключительно в файлах `.env` каждого проекта. Эти файлы представляют собой **символьные ссылки на хранилище уровня 1**, расположены вне рабочего дерева git и внесены в `.gitignore` (`**/.env`).
- Для каждого узла перечень обязательных переменных задокументирован в файле `SECRETS.md`, причём указываются только имена ключей, но ни в коем случае не их значения.
- Версионируемый файл `apex.env` содержит только несекретные параметры.
- В ядровых compose-файлах критически важные секреты снабжены маркером `?error`: при отсутствии переменной запуск немедленно останавливается с ошибкой ещё на этапе разбора конфигурации.
- Механизм [«capture-up»](/apex/glossary#capture-up) безопасен конструктивно: игнорируемые файлы `.env` и данные уровней хранилища никогда не попадают в git-индекс.
---
@@ -1,25 +1,25 @@
---
title: Многоуровневое хранилище и изоляция привилегий
description: Структура ссылок @tier, общее хранилище и понодовые noroot-пользователи.
description: Структура ссылок @tier, разделяемое хранилище и локальные noroot-пользователи для проектов.
weight: 16
---
Данные сервисов никогда не живут внутри репозитория [узла](/apex/glossary#node). Они живут в трёх [уровнях](/apex/glossary#tier) хранилища — раздельных расположениях, выбираемых для каждого хоста, — и достигают [композиций](/apex/glossary#compositions) через стандартизированную структуру символьных ссылок. Владение этими данными делегируется системным пользователям для каждого проекта, поэтому ни один сервисный контейнер не пишет от имени root.
Данные сервисов никогда не хранятся внутри репозитория [узла](/apex/glossary#node). Они распределяются по трём [уровням](/apex/glossary#tier) хранилища — отдельным каталогам, настраиваемым индивидуально для каждого хоста, — и подключаются к [композициям](/apex/glossary#compositions) через стандартизированную структуру символьных ссылок. Права на эти каталоги делегируются системным пользователям проектов, благодаря чему ни один контейнер не ведёт запись от имени root.
## Структура ссылок уровней {#links}
`apex tiers/link <tier1> <tier2> <tier3>` строит два слоя ссылок:
Команда `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/@tierN` → корень хранилища соответствующего уровня. Именно эти пути движок предоставляет в переменных `APEX_TIER_ROOT1..3`.
- **Для каждого проекта:** в каталоге `compositions/<project>/` создаётся путь `<tier-root>/<project>/`, на который указывает ссылка `compositions/<project>/@tierN`. Внутри каждого уровня поддерживается общий каталог `shared/` с поддиректорией для каждого проекта, а внутри каталога уровня проекта создаётся относительная ссылка `shared`, ведущая обратно в общую зону.
Каждый проект дополнительно получает `compositions/<project>/.env` → `<tier1-root>/<project>/.env` (создаётся пустым, если отсутствует) — файл незакоммиченных секретов, описанный в [Концепция: Композиции](/apex/concepts/compositions). Ссылки `@tierN` и файлы `.env` игнорируются git; репозиторий остаётся чистым источником.
Каждый проект дополнительно получает символьную ссылку `compositions/<project>/.env` → `<tier1-root>/<project>/.env` (создаётся пустым, если файл отсутствует). В нём хранятся незакоммиченные секреты (см. [Концепция: Композиции и общее ядро](/apex/concepts/compositions)). Ссылки `@tierN` и файлы `.env` исключены из git через `.gitignore`, благодаря чему репозиторий остаётся чистым источником кода.
Если путь, который должен быть символьной ссылкой, существует как настоящий файл или каталог, действие предупреждает и запрашивает подтверждение перед его удалением; отказ (включая неинтерактивный EOF) пропускает эту ссылку и продолжает.
Если по пути предполагаемой символьной ссылки уже существует обычный файл или каталог, действие предупреждает об этом и запрашивает подтверждение перед удалением. При отказе (включая неинтерактивный EOF) создание ссылки пропускается, а выполнение продолжается дальше.
## Пользователи и группы {#users}
`apex tiers/useradd` провизионирует по одному заблокированному системному пользователю на проект — `noroot-<project>` (имя санитизируется, собственная первичная группа) — плюс общефлотовую группу `noroot-shared`, к которой присоединяется каждый пользователь проекта. Затем оно внедряет разрешённые ID в `.env` проекта между комментариями-маркерами, заменяя любой предыдущий блок при повторных запусках:
Команда `apex tiers/useradd` создаёт по одному заблокированному системному пользователю на проект — `noroot-<project>` (с санитизацией имени и собственной первичной группой), а также общефлотовую группу `noroot-shared`, в которую входят все пользователи проектов. Затем сгенерированные идентификаторы прописываются в файл `.env` соответствующего проекта между комментариями-маркерами, перезаписывая старый блок при повторных запусках:
```ini {filename="compositions/<project>/.env"}
# START AUTO GENERATED APEX-NOROOT-USERS
@@ -29,42 +29,42 @@ APEX_SHARED_GID=997
# END AUTO GENERATED APEX-NOROOT-USERS
```
Композиции потребляют их как `user: "${APEX_UID?error}:${APEX_GID?error}"` и `group_add: ["${APEX_SHARED_GID?error}"]`.
Композиции подключают эти идентификаторы через переменные `user: "${APEX_UID?error}:${APEX_GID?error}"` и `group_add: ["${APEX_SHARED_GID?error}"]`.
## Матрица владения {#ownership}
`apex tiers/chown` обеспечивает владение по всем трём уровням. Без аргументов оно работает с корнями уровней (`APEX_TIER_ROOT1..3`); явные пути уровней могут переопределить это.
Действие `apex tiers/chown` выставляет корректные права по всем трём уровням хранилища. Без аргументов оно обрабатывает корни уровней (`APEX_TIER_ROOT1..3`); при необходимости можно указать конкретные пути вручную.
| Путь | Владение | Примечания |
|---|---|---|
| `<tier-root>` | `root:root` | Только inode каталога. |
| `<tier-root>/shared` | `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-бит — участники могут добавлять файлы, но удалять только свои. |
| `<tier-root>/<project>` | `uid:gid` | Рекурсивно; идентификаторы читаются из `.env` проекта. |
| `<tier-root>/shared/<project>` | `uid:shared_gid` | Рекурсивно, с установкой sticky-бита: участники могут добавлять файлы, но удалять — только собственные. |
Проекты с неполным набором `APEX_UID`/`APEX_GID`/`APEX_SHARED_GID` пропускаются с предупреждением, указывающим на `tiers/useradd`.
Проекты с неполным набором переменных `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` | Общая область внутри уровней ядрового проекта. |
| `APEX_TIER_ROOT1..3` | Корни хранилищ уровней (`compositions/@tierN`, абсолютные пути). |
| `APEX_TIER1..3` | Каталоги данных ядрового проекта на каждом уровне (`compositions/apex/@tierN`). |
| `APEX_TIER1..3_SHARED` | Общие области внутри уровней ядрового проекта. |
Ядровая композиция монтирует исключительно через них; исполнитель резервного копирования вместо этого монтирует корень данных флота `/srv` в режиме только для чтения и разрешает контейнерные монтирования каждого помеченного сервиса обратно в их хостовые пути.
Ядровая композиция монтирует каталоги исключительно через эти переменные. Контейнер резервного копирования, напротив, монтирует общий корень данных флота `/srv` в режиме «только для чтения» и сопоставляет контейнерные пути каждого сервиса с их реальными путями на хосте.
## Резервные копии {#backups}
## Резервное копирование {#backups}
`apex backup/run <telegram-bot-url>` управляет ядровым сервисом `resticontainer` (профиль `manual`, одноразовый) от начала до конца. Намерение резервного копирования **управляется метками**: resticontainer обнаруживает каждый сервис, несущий `restic.enable=true`, читает его `restic.backup.paths` (и опциональные `restic.backup.stop` / `restic.hooks.*`), разрешает эти контейнерные пути в хостовые пути и делает снимок их объединения в одном запуске restic.
Команда `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`, чтобы планировщики это заметили.
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_RESTIC_*` (см. [Справочник: Переменные окружения](/apex/reference/environment)). Состав сохраняемых данных определяется метками каждого отдельного сервиса. Например, для ядровых сервисов копируются только файлы конфигурации (динамические правила traefik, настройки xray и adguard), тогда как легко восстановимые данные уровня 3 (кэши и журналы) в снимок не включаются.
---