fix: cyrillic fonts, dev certs, page head, and ru-text typography (#67)

🤖 PR generated for branch `fix/cyrillic-fonts-dev-certs-and-page-head`.

### Changes Summary
- **Fonts**: Add Cyrillic glyph subsets for Lato, EB Garamond, and Fira
Code.
- **Dev**: Support remote Root CA and encrypted dev CA key in
`.env.dev`.
- **UI & Kits**: Add size variant to `Breadcrumbs` component and enlarge
`PageHead` breadcrumbs with tuned collapse scroll threshold.
- **ESLint**: Add `no-hanging-particles` i18n & markdown typography rule
with auto-fixer.
- **Content & i18n**: Apply `ru-text` editorial and typography polish
(em dashes, number ranges, «ё» restoration, non-breaking spaces) across
`services/www` and `services/docs`.
This commit is contained in:
deytenit authored and GitHub committed 2026-10-25 15:21:09 +03:00
1 parent c6487dc258
commit b3638f4d63
111 files changed
+2350 -1512

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@ cascade:
- **Единая точка входа в локальном чекауте** — `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#overlay) поверх общих действий; commons содержит только то, что объединяет все узлы.
- **Многоуровневое хранилище с изоляцией привилегий** — стандартизированные [уровни](/apex/glossary#tier) хранилища с изоляцией прав на уровне каждого проекта.
- **Общая базовая композиция** — [базовая композиция](/apex/glossary#core-composition) (`compositions/apex/docker-compose.yml`) параметризуется через `${env}` и включается в композиции каждого узла.
@@ -4,7 +4,7 @@ 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: как они устроены и сочетаются друг с другом." >}}
@@ -22,4 +22,4 @@ weight: 10
**См. также:**
- [Руководства: Эксплуатация флота apex](/apex/guides)
- [Справочник: Команды и интерфейсы](/apex/reference)
- [Справочник: Команды и интерфейсы](/apex/reference)
@@ -1,18 +1,18 @@
---
title: "Действия и оверлей"
title: "Действия и оверлей"
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.
- **`METADATA = Meta(...)`** &rarr; краткое описание и дескрипторы аргументов, управляющие как текстом справки, так и парсингом. Система дескрипторов пришла на смену heredoc-блоку `action_metadata()` bash-фреймворка «root» commons, предшественника apex.
- **`run(ctx, args)`** &rarr; точка входа. `ctx` — сформированный движком контекст выполнения; `args` — распарсенное пространство имён `argparse`.
```python {filename="actions/<group>/<action>.py"}
@@ -31,27 +31,27 @@ 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`:
| Дескриптор | Отображается на | Поведение |
| Дескриптор | Отображается на | Поведение |
| --- | --- | --- |
| `Arg("name")` | позиционный аргумент | Обязателен по умолчанию; `required=False` делает его опциональным позиционным (`nargs="?"`, по умолчанию `None`). |
| `Opt("--name", default=...)` | опция со значением | Опциональный флаг, принимающий значение, с объявленным значением по умолчанию. |
| `Flag("--name")` | булев флаг | `store_true`; наличие означает `True`. |
| `Rest("name")` | остаток | Жадная сквозная передача всех оставшихся аргументов командной строки (`argparse.REMAINDER`). |
Движок строит парсер как `apex <group>/<action>` и разбирает оставшиеся аргументы перед вызовом `run`. Флаг `--help` обрабатывается на этапе парсинга. При ошибке использования (неизвестный флаг, отсутствующий позиционный аргумент) выводится справка по синтаксису (usage), а процесс завершается с кодом `64` (`EX_USAGE`) вместо стандартного для argparse кода `2`.
Движок строит парсер как `apex <group>/<action>` и разбирает оставшиеся аргументы перед вызовом `run`. Флаг `--help` обрабатывается на этапе парсинга. При ошибке использования (неизвестный флаг, отсутствующий позиционный аргумент) выводится справка по синтаксису (usage), а процесс завершается с кодом `64` (`EX_USAGE`) вместо стандартного для argparse кода `2`.
## Перечисление без импорта {#listing}
## Перечисление без импорта {#listing}
Команда `apex` без аргументов (или с флагом `--help`) выводит список всех доступных действий. Перечисление никогда не импортирует модули действий: движок статически сканирует каждый файл с помощью модуля `ast` и извлекает только краткое описание `METADATA` и любую константу `DISABLED`. Поэтому код действия не выполняется и не может вызвать сбой во время перечисления: файл, который не удалось распарсить, помечается как `(unparseable)`, не нарушая работу всей команды.
Команда `apex` без аргументов (или с флагом `--help`) выводит список всех доступных действий. Перечисление никогда не импортирует модули действий: движок статически сканирует каждый файл с помощью модуля `ast` и извлекает только краткое описание `METADATA` и любую константу `DISABLED`. Поэтому код действия не выполняется и не может вызвать сбой во время перечисления: файл, который не удалось распарсить, помечается как `(unparseable)`, не нарушая работу всей команды.
Каждая запись помечается своим источником оверлея:
@@ -81,14 +81,14 @@ 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}
@@ -96,7 +96,7 @@ def run(ctx, args):
| Код | Значение | Типичная причина |
| --- | --- | --- |
| `0` | успех | Функция `run` вернула `0` или нецелочисленное значение. |
| `0` | успех | Функция `run` вернула `0` или нецелочисленное значение. |
| `1` | общая ошибка | Неизвестное действие, отключённое действие, необработанное исключение, нецелочисленный `SystemExit`. |
| `64` | ошибка использования | Аргументы отклонены парсером (`EX_USAGE`). |
| `65` | ошибка данных | Некорректные входные данные (`EX_DATAERR`), выбрасывается библиотеками движка. |
@@ -1,6 +1,6 @@
---
title: "Схема адресации флота"
description: "Подсети /24 для каждого узла и статические якорные IP внутри пространства анклава (по умолчанию 198.18.0.0/16)."
description: "Подсети /24 для каждого узла и статические якорные IP внутри пространства анклава (по умолчанию 198.18.0.0/16)."
weight: 17
---
@@ -8,11 +8,11 @@ weight: 17
## Адресное пространство {#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` связанных узлов в непрерывные блоки, чтобы сохранять наглядность адресного плана. Это вопрос внутренней организации, а не жёсткое требование фреймворка.
@@ -23,20 +23,20 @@ weight: 17
| Диапазон | Назначение |
|---|---|
| `.10`–`.19` | Зарезервировано для якорей [базовой композиции](/apex/glossary#core-composition); сервис `traefik` всегда занимает адрес `.10` (через `APEX_TRAEFIK_DIRECT_IP`) |
| `.10`–`.19` | Зарезервировано для якорей [базовой композиции](/apex/glossary#core-composition); сервис `traefik` всегда занимает адрес `.10` (через `APEX_TRAEFIK_DIRECT_IP`) |
| `.20` и выше | Якоря сервисов, распределяемые по проектам (`<project>`) |
Каждый якорь сервиса представляет собой статический `ipv4_address` в сети direct — фиксированный [якорный IP](/apex/glossary#anchor-ip), а не динамическую аренду (lease). Назначенные адреса фиксируются в `compositions/ADDRESSING.md`, который служит человекочитаемым реестром подсети узла.
## Автоматическая проверка {#enforcement}
Линтер compose проверяет якорные IP-адреса во всех [композициях](/apex/glossary#compositions) узла. Для каждого значения `ipv4_address`, найденного в файлах `compositions/*/docker-compose.yml`, проверяются следующие условия:
Линтер compose проверяет якорные IP-адреса во всех [композициях](/apex/glossary#compositions) узла. Для каждого значения `ipv4_address`, найденного в файлах `compositions/*/docker-compose.yml`, проверяются следующие условия:
- **Корректный формат** — о некорректном литерале линтер сообщает как об ошибке, не падая аварийно.
- **Уникальность** — один и тот же якорный IP не может использоваться в двух разных проектах; при обнаружении дубликатов выводятся имена обоих конфликтующих проектов.
- **Корректный формат** — о некорректном литерале линтер сообщает как об ошибке, не падая аварийно.
- **Уникальность** — один и тот же якорный 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).
---
@@ -4,7 +4,7 @@ description: Четыре опоры репозиториев узла, субм
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}
@@ -47,32 +47,32 @@ flowchart TD
H --> I["run(ctx, args)"]
```
Движок определяет режим работы по своему расположению на диске. Если он запущен из каталога `commons/`, а в родительском каталоге присутствует `node.env`, то родительский каталог принимается за корень репозитория узла, а `proprietaries/actions/` становится деревом оверлея. При любом другом расположении 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` и выполняет push с флагом `--force-with-lease`.
2. Запланированное CI-задание в репозитории узла вливает ветку `sync/<node>` обратно в `main` от имени бота.
Благодаря циклу [«capture-up»](/apex/glossary#capture-up) любой дрейф конфигурации на хосте превращается в коммит: изменение наглядно отображается в diff и легко откатывается, исключая незаметные расхождения. Если субмодуль commons имеет незакоммиченные изменения («грязный») или каталог `compositions/` отсутствует, синхронизация прерывается без коммита, поэтому повреждённое состояние чекаута никогда не попадёт в историю.
Благодаря циклу [«capture-up»](/apex/glossary#capture-up) любой дрейф конфигурации на хосте превращается в коммит: изменение наглядно отображается в diff и легко откатывается, исключая незаметные расхождения. Если субмодуль commons имеет незакоммиченные изменения («грязный») или каталог `compositions/` отсутствует, синхронизация прерывается без коммита, поэтому повреждённое состояние чекаута никогда не попадёт в историю.
> [!NOTE]
> CI-задание слияния клонирует репозиторий *без* субмодулей: субмодуль commons подключён по SSH-URL, недоступному стандартному токену CI, а самому слиянию требуется лишь обновить указатель gitlink.
## Проектные ограничения {#constraints}
- **Только стандартная библиотека (stdlib).** Движок работает на штатном Python 3 из Debian — без `pip` и сторонних зависимостей. Каталог `vendor/` зарезервирован для зафиксированного чистого Python-кода, но по умолчанию поставляется пустым.
- **Ручные compose-файлы.** Никаких промежуточных манифестов или кодогенерации: файлы compose служат единственным источником истины.
- **Автоматическая проверка вместо соглашений.** Всё, что можно проверить или применить автоматически, выполняется действием (проверка якорных IP, откат crontab, самовосстановление блоков-маркеров), а не остаётся на совести документации.
- **Только стандартная библиотека (stdlib).** Движок работает на штатном Python 3 из Debian — без `pip` и сторонних зависимостей. Каталог `vendor/` зарезервирован для зафиксированного чистого Python-кода, но по умолчанию поставляется пустым.
- **Ручные compose-файлы.** Никаких промежуточных манифестов или кодогенерации: файлы compose служат единственным источником истины.
- **Автоматическая проверка вместо соглашений.** Всё, что можно проверить или применить автоматически, выполняется действием (проверка якорных IP, откат crontab, самовосстановление блоков-маркеров), а не остаётся на совести документации.
---
**См. также:**
- [Концепция: Действия и оверлей](/apex/concepts/actions-and-overlay)
- [Концепция: Действия и оверлей](/apex/concepts/actions-and-overlay)
- [Концепция: Разрешение идентичности узла](/apex/concepts/node-identity)
- [Руководство: Сборка репозитория узла](/apex/guides/node-repository)
@@ -4,7 +4,7 @@ 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}
@@ -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,14 +29,14 @@ include:
```
> [!NOTE]
> Директива `include:` требует Docker Compose версии v2.20 или новее.
> Директива `include:` требует Docker Compose версии v2.20 или новее.
## Ядровые сервисы и профили {#core-services}
## Ядровые сервисы и профили {#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`):
@@ -49,12 +49,12 @@ include:
## Сети {#networks}
Ядровая композиция создаёт три сети узла, а сервисные композиции подключаются к ним через директиву `external: true`:
Ядровая композиция создаёт три сети узла, а сервисные композиции подключаются к ним через директиву `external: true`:
| Сеть | Тип | Назначение |
|---|---|---|
| `direct` | bridge, подсеть = `APEX_SUBNET` | Сеть со статическими [якорными IP](/apex/glossary#anchor-ip); якорь каждого проекта удерживает здесь фиксированный адрес (см. [Концепция: Схема адресации флота](/apex/concepts/addressing)). |
| `enclave` | bridge, `internal: true` | Бэкенд-сеть обратного прокси: через неё traefik направляет трафик к целевым сервисам. |
| `enclave` | bridge, `internal: true` | Бэкенд-сеть обратного прокси: через неё traefik направляет трафик к целевым сервисам. |
| `socket` | bridge, `internal: true` | Изолирует трафик между traefik и socket-proxy. |
Внутри проекта зависимые контейнеры обычно используют сетевое пространство имён якоря (`network_mode: service:<anchor>`) вместо прямого подключения к сетям.
@@ -65,7 +65,7 @@ include:
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()`. Они внедряются в окружение процесса действиями оркестрации и потому имеют наивысший приоритет над обоими файлами.
3. **Экспорт движка** — переменные идентичности `APEX_*` и пути [уровней](/apex/glossary#tier) из `ctx.vars()`. Они внедряются в окружение процесса действиями оркестрации и потому имеют наивысший приоритет над обоими файлами.
Критические переменные снабжены суффиксом `?error` в общем файле: если секрет не задан, загрузка конфигурации композиции завершается явной ошибкой, предотвращая запуск некорректно настроенного контейнера.
@@ -74,9 +74,9 @@ include:
Команда `apex compose <action>` управляет всеми композициями на узле:
- **Порядок:** сначала ядровой проект (он владеет сетями), затем остальные сервисные композиции по алфавиту; команда `down` выполняет действия в обратном порядке.
- **Env-файлы:** для каждого проекта существующие файлы `.env` и `apex.env` передаются через флаг `--env-file`.
- **Env-файлы:** для каждого проекта существующие файлы `.env` и `apex.env` передаются через флаг `--env-file`.
- **Команда `up`:** по умолчанию подразумевает флаг `-d`; любые дополнительные аргументы передаются напрямую в `docker compose`.
- **Флаг `--dry-run`:** выводит каждую команду с её рабочим каталогом без выполнения; поддерживается в любом месте командной строки.
- **Флаг `--dry-run`:** выводит каждую команду с её рабочим каталогом без выполнения; поддерживается в любом месте командной строки.
---
@@ -4,7 +4,7 @@ description: ctx — единый интерфейс взаимодействи
weight: 14
---
Каждое [действие](/apex/glossary#action) получает объект `ctx` первым аргументом. Это единственный штатный способ взаимодействия с хостом: логирование, запуск процессов, рендеринг шаблонов, правка файлов, настройка межсетевого экрана и сервисов, а также отправка уведомлений — всё выполняется через него. Благодаря этому действия остаются компактными и декларативными, а общие для всего флота правила (формат логов, регламент резервного копирования, структура уведомлений) сосредоточены в одном месте — [движке](/apex/glossary#engine).
Каждое [действие](/apex/glossary#action) получает объект `ctx` первым аргументом. Это единственный штатный способ взаимодействия с хостом: логирование, запуск процессов, рендеринг шаблонов, правка файлов, настройка межсетевого экрана и сервисов, а также отправка уведомлений — всё выполняется через него. Благодаря этому действия остаются компактными и декларативными, а общие для всего флота правила (формат логов, регламент резервного копирования, структура уведомлений) сосредоточены в одном месте — [движке](/apex/glossary#engine).
## Члены {#members}
@@ -30,20 +30,20 @@ weight: 14
- Известные переменные `$VAR` и `${VAR}` подставляются.
- **Неизвестные переменные сохраняются буквально:** опечатка в имени переменной останется в готовом файле на виду, а не превратится незаметно в пустую строку.
- Подстановки команд shell (`$(cmd)`) не затрагиваются, что сохраняет целостность строк crontab со встроенными командами вида `$(cat ...)`.
- Подстановки команд shell (`$(cmd)`) не затрагиваются, что сохраняет целостность строк crontab со встроенными командами вида `$(cat ...)`.
Подстановка выполняется по словарю `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_TIER1`…`APEX_TIER3` | Каталоги данных базового проекта для каждого [уровня](/apex/glossary#tier). |
| `APEX_TIER1_SHARED`…`APEX_TIER3_SHARED` | Общие области внутри каждого уровня. |
| `APEX_TIER_ROOT1`…`APEX_TIER_ROOT3` | Корневые каталоги хранилищ уровней (ссылки уровня узла). |
@@ -53,14 +53,14 @@ weight: 14
Объект `ctx.host` реализует строгие правила записи:
- **Атомарная запись:** метод `write_file` сначала формирует временный файл (права `0644`, аналогичные перенаправлению вывода из-под root) и перемещает его на место через `mv` или `sudo cp`. При перезаписи существующего файла его исходные права сохраняются.
- **Резервные копии с временной меткой:** по запросу перед записью создаётся копия старого файла рядом с целевым: `<name>.<epoch>.bak`.
- **Внедрение блоков-маркеров:** изменения внутри системных файлов (например, `/etc/ufw/before.rules`) обрамляются комментариями-маркерами начала и конца. Перед вставкой нового блока после якорной строки движок удаляет все предыдущие управляемые блоки (включая оставшиеся от старого инструментария), предотвращая накопление дубликатов при повторных запусках.
- **Откат crontab:** метод `install_crontab` делает снимок текущего crontab, отклоняет пустой результат рендеринга (код выхода `65`), показывает diff изменений и автоматически восстанавливает снимок при ошибке установки.
- **Атомарная запись:** метод `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`), поэтому проблемы со связью сразу становятся заметны.
---
@@ -68,4 +68,4 @@ weight: 14
- [Справочник: API движка](/apex/reference/engine-api)
- [Справочник: Переменные окружения](/apex/reference/environment)
- [Концепция: Действия и оверлей](/apex/concepts/actions-and-overlay)
- [Концепция: Действия и оверлей](/apex/concepts/actions-and-overlay)
@@ -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-фреймворка «root» commons, развитием которого стал 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 узла для правила `Host()` в traefik. Если ключ не задан, движок берёт имя хоста из ОС и выводит предупреждение.
- `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,7 +26,7 @@ APEX_SUBNET=198.18.16.0/24
### Имя хоста ОС
Имя хоста операционной системы используется только как резервный вариант для `APEX_NODE_FQDN`. Если в `node.env` значение `APEX_NODE_FQDN` не указано, движок берёт имя хоста ОС и выводит предупреждение. Это значение никогда не парсится для вычисления короткого имени узла или других параметров.
Имя хоста операционной системы используется только как резервный вариант для `APEX_NODE_FQDN`. Если в `node.env` значение `APEX_NODE_FQDN` не указано, движок берёт имя хоста ОС и выводит предупреждение. Это значение никогда не парсится для вычисления короткого имени узла или других параметров.
## Порядок разрешения {#resolution}
@@ -42,7 +42,7 @@ APEX_SUBNET=198.18.16.0/24
### Вычисление короткого имени
Значение `name` — это **первая часть** имени `host` (`host.split(".")[0]`), например `node1` для `node1.example.com`. Оно используется в логах, имени ветки `sync/<node>` и уведомлениях.
Значение `name` — это **первая часть** имени `host` (`host.split(".")[0]`), например `node1` для `node1.example.com`. Оно используется в логах, имени ветки `sync/<node>` и уведомлениях.
### Получение подсети
@@ -54,7 +54,7 @@ APEX_SUBNET=198.18.16.0/24
## Предупреждения {#warnings}
При разрешении идентичности может выводиться одно штатное предупреждение: оно указывает на неполную конфигурацию, которую стоит исправить.
При разрешении идентичности может выводиться одно штатное предупреждение: оно указывает на неполную конфигурацию, которую стоит исправить.
| Условие | Сообщение предупреждения |
|---|---|
@@ -62,7 +62,7 @@ APEX_SUBNET=198.18.16.0/24
## Режимы сбоев {#failures}
Движок никогда не пытается угадывать параметры наугад. Две ситуации считаются критическими и приводят к завершению с кодом `66` (соглашение движка о коде выхода при отсутствии обязательного ввода):
Движок никогда не пытается угадывать параметры наугад. Две ситуации считаются критическими и приводят к завершению с кодом `66` (соглашение движка о коде выхода при отсутствии обязательного ввода):
- **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` и завершает работу.
@@ -4,50 +4,50 @@ 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/` по трём направлениям:
- **Правила хоста** (`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`.
- **Правила уровня ядра** (`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}
## Ограничение на уровне контейнеров {#containers}
[Ядровая композиция](/apex/glossary#core-composition) задаёт строгие ограничения для контейнеров:
[Ядровая композиция](/apex/glossary#core-composition) задаёт строгие ограничения для контейнеров:
- Граничный прокси изолирован от сокета Docker: он получает данные о сервисах через **отфильтрованный socket-proxy**, который отдаёт состояние контейнеров только для чтения в замкнутой сети.
- Граничный прокси изолирован от сокета Docker: он получает данные о сервисах через **отфильтрованный socket-proxy**, который отдаёт состояние контейнеров только для чтения в замкнутой сети.
- Сети `enclave` и `socket` объявлены с параметром `internal: true`: у них нет прямого доступа в интернет и опубликованных портов хоста.
- Ядровые сервисы запускаются с опцией `no-new-privileges` под noroot-пользователем везде, где позволяет рабочая нагрузка; Linux capabilities выдаются точечно каждому сервису, а не глобально.
- Ядровые сервисы запускаются с опцией `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`). Поскольку трафик отправляется наружу, Alloy подключён к сети `direct` с доступом в интернет, а не к закрытой сети `enclave`. На самих узлах логи не накапливаются, и открывать внешний доступ к эндпоинтам сервисов не требуется.
Система мониторинга построена по модели **исходящей отправки (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}
- Секреты хранятся исключительно в файлах `.env` каждого проекта. Эти файлы представляют собой **символьные ссылки на хранилище уровня 1**, расположены вне рабочего дерева git и внесены в `.gitignore` (`**/.env`).
- Для каждого узла перечень обязательных переменных задокументирован в файле `SECRETS.md`, причём указываются только имена ключей, но ни в коем случае не их значения.
- Для каждого узла перечень обязательных переменных задокументирован в файле `SECRETS.md`, причём указываются только имена ключей, но ни в коем случае не их значения.
- Версионируемый файл `apex.env` содержит только несекретные параметры.
- В ядровых compose-файлах критически важные секреты снабжены маркером `?error`: при отсутствии переменной запуск немедленно останавливается с ошибкой ещё на этапе разбора конфигурации.
- В ядровых compose-файлах критически важные секреты снабжены маркером `?error`: при отсутствии переменной запуск немедленно останавливается с ошибкой ещё на этапе разбора конфигурации.
- Механизм [«capture-up»](/apex/glossary#capture-up) безопасен конструктивно: игнорируемые файлы `.env` и данные уровней хранилища никогда не попадают в git-индекс.
---
**См. также:**
- [Концепция: Многоуровневое хранилище и изоляция привилегий](/apex/concepts/tiered-storage)
- [Концепция: Композиции и общее ядро](/apex/concepts/compositions)
- [Концепция: Многоуровневое хранилище и изоляция привилегий](/apex/concepts/tiered-storage)
- [Концепция: Композиции и общее ядро](/apex/concepts/compositions)
- [Справочник: Действия](/apex/reference/actions)
@@ -1,25 +1,25 @@
---
title: Многоуровневое хранилище и изоляция привилегий
description: Структура ссылок @tier, разделяемое хранилище и локальные noroot-пользователи для проектов.
title: Многоуровневое хранилище и изоляция привилегий
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>` формирует два уровня символьных ссылок:
- **На уровне узла:** `compositions/@tierN` → корень хранилища соответствующего уровня. Именно эти пути движок предоставляет в переменных `APEX_TIER_ROOT1..3`.
- **Для каждого проекта:** в каталоге `compositions/<project>/` создаётся путь `<tier-root>/<project>/`, на который указывает ссылка `compositions/<project>/@tierN`. Внутри каждого уровня поддерживается общий каталог `shared/` с поддиректорией для каждого проекта, а внутри каталога уровня проекта создаётся относительная ссылка `shared`, ведущая обратно в общую зону.
- **Для каждого проекта:** в каталоге `compositions/<project>/` создаётся путь `<tier-root>/<project>/`, на который указывает ссылка `compositions/<project>/@tierN`. Внутри каждого уровня поддерживается общий каталог `shared/` с поддиректорией для каждого проекта, а внутри каталога уровня проекта создаётся относительная ссылка `shared`, ведущая обратно в общую зону.
Каждый проект дополнительно получает символьную ссылку `compositions/<project>/.env` → `<tier1-root>/<project>/.env` (создаётся пустым, если файл отсутствует). В нём хранятся незакоммиченные секреты (см. [Концепция: Композиции и общее ядро](/apex/concepts/compositions)). Ссылки `@tierN` и файлы `.env` исключены из git через `.gitignore`, благодаря чему репозиторий остаётся чистым источником кода.
Каждый проект дополнительно получает символьную ссылку `compositions/<project>/.env` → `<tier1-root>/<project>/.env` (создаётся пустым, если файл отсутствует). В нём хранятся незакоммиченные секреты (см. [Концепция: Композиции и общее ядро](/apex/concepts/compositions)). Ссылки `@tierN` и файлы `.env` исключены из git через `.gitignore`, благодаря чему репозиторий остаётся чистым источником кода.
Если по пути предполагаемой символьной ссылки уже существует обычный файл или каталог, действие предупреждает об этом и запрашивает подтверждение перед удалением. При отказе (включая неинтерактивный EOF) создание ссылки пропускается, а выполнение продолжается дальше.
Если по пути предполагаемой символьной ссылки уже существует обычный файл или каталог, действие предупреждает об этом и запрашивает подтверждение перед удалением. При отказе (включая неинтерактивный EOF) создание ссылки пропускается, а выполнение продолжается дальше.
## Пользователи и группы {#users}
## Пользователи и группы {#users}
Команда `apex tiers/useradd` создаёт по одному заблокированному системному пользователю на проект — `noroot-<project>` (с санитизацией имени и собственной первичной группой), а также общефлотовую группу `noroot-shared`, в которую входят все пользователи проектов. Затем сгенерированные идентификаторы прописываются в файл `.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` | Рекурсивно; идентификаторы читаются из `.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` | Общие области внутри уровней ядрового проекта. |
Ядровая композиция монтирует каталоги исключительно через эти переменные. Контейнер резервного копирования, напротив, монтирует общий корень данных флота `/srv` в режиме «только для чтения» и сопоставляет контейнерные пути каждого сервиса с их реальными путями на хосте.
Ядровая композиция монтирует каталоги исключительно через эти переменные. Контейнер резервного копирования, напротив, монтирует общий корень данных флота `/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. Запуск не засчитывается как успешный, если в репозитории не появился новый снимок (защита от ложного успеха при отсутствии помеченных сервисов).
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` даже при успешном бэкапе, чтобы сбой в канале оповещения не остался незамеченным.
4. Отправляет статус выполнения в Telegram. Если уведомление не удалось доставить, операция завершается с кодом ошибки `1` даже при успешном бэкапе, чтобы сбой в канале оповещения не остался незамеченным.
Параметры подключения и учётные данные репозитория считываются из конфигурации `APEX_RESTIC_*` (см. [Справочник: Переменные окружения](/apex/reference/environment)). Состав сохраняемых данных определяется метками каждого отдельного сервиса. Например, для ядровых сервисов копируются только файлы конфигурации (динамические правила traefik, настройки xray и adguard), тогда как легко восстановимые данные уровня 3 (кэши и журналы) в снимок не включаются.
Параметры подключения и учётные данные репозитория считываются из конфигурации `APEX_RESTIC_*` (см. [Справочник: Переменные окружения](/apex/reference/environment)). Состав сохраняемых данных определяется метками каждого отдельного сервиса. Например, для ядровых сервисов копируются только файлы конфигурации (динамические правила traefik, настройки xray и adguard), тогда как легко восстановимые данные уровня 3 (кэши и журналы) в снимок не включаются.
---
@@ -4,13 +4,13 @@ description: Начальная настройка репозитория узл
weight: 5
---
В этом руководстве показано, как настроить новый хост: от клонирования репозитория [узла](/apex/glossary#node) до запуска рабочего стека сервисов. Предполагается, что репозиторий узла уже создан; если его нужно собрать с нуля, см. [Руководство: Сборка репозитория узла](/apex/guides/node-repository).
В этом руководстве показано, как настроить новый хост: от клонирования репозитория [узла](/apex/glossary#node) до запуска рабочего стека сервисов. Предполагается, что репозиторий узла уже создан; если его нужно собрать с нуля, см. [Руководство: Сборка репозитория узла](/apex/guides/node-repository).
## Предварительные требования
- Хост на базе Debian. Подойдёт любой FQDN: [идентичность узла](/apex/glossary#node-identity) задаётся явно в `node.env` (`APEX_NODE_FQDN`, `APEX_NODE_HOST`, `APEX_SUBNET`), а не выводится из имени хоста.
- Хост на базе 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 версии 2.20 или новее ([базовая композиция](/apex/glossary#core-composition) подключается через директиву `include:`).
- Docker с Compose версии 2.20 или новее ([базовая композиция](/apex/glossary#core-composition) подключается через директиву `include:`).
- Учётная запись с правами sudo для входа на хост.
- Три хранилища под [уровни](/apex/glossary#tier) (например, три точки монтирования).
@@ -35,7 +35,7 @@ source ~/.bashrc
apex --help
```
В списке отображается каждое действие [commons](/apex/glossary#commons) плюс любые локальные [проприетарные модули](/apex/glossary#proprietaries) узла с меткой `(local)`. Если в `node.env` не задан `APEX_NODE_FQDN`, движок выводит предупреждение и использует имя хоста ОС. На рабочей станции это безобидно, но на самом узле задайте `APEX_NODE_FQDN` явно. Параметр `APEX_SUBNET` обязателен: без него движок завершит работу с кодом `66`.
В списке отображается каждое действие [commons](/apex/glossary#commons) плюс любые локальные [проприетарные модули](/apex/glossary#proprietaries) узла с меткой `(local)`. Если в `node.env` не задан `APEX_NODE_FQDN`, движок выводит предупреждение и использует имя хоста ОС. На рабочей станции это безобидно, но на самом узле задайте `APEX_NODE_FQDN` явно. Параметр `APEX_SUBNET` обязателен: без него движок завершит работу с кодом `66`.
### Свяжите уровни хранилища
@@ -67,7 +67,7 @@ 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)).
Переменные `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)).
### Настройте хост
+3 -3
View File
@@ -16,7 +16,7 @@ weight: 100
## Capture-up {#capture-up}
**Capture-up** — механизм фиксации состояния хоста в репозитории: регулярный коммит в ветку `sync/<node>`, который перебазируется на `main` и сливается средствами CI. В результате любой дрейф конфигурации становится наглядным diff, а не скрытым расхождением.
**Capture-up** — механизм фиксации состояния хоста в репозитории: регулярный коммит в ветку `sync/<node>`, который перебазируется на `main` и сливается средствами CI. В результате любой дрейф конфигурации становится наглядным diff, а не скрытым расхождением.
## Commons {#commons}
@@ -56,11 +56,11 @@ weight: 100
## Proprietaries {#proprietaries}
**Proprietaries (проприетарные модули)** — каталог репозитория для локального кода конкретного узла: действий, библиотек и инструментов, которые нужны только этому узлу и не входят в commons.
**Proprietaries (проприетарные модули)** — каталог репозитория для локального кода конкретного узла: действий, библиотек и инструментов, которые нужны только этому узлу и не входят в commons.
## Tier {#tier}
**Tier (уровень)** — один из трёх стандартных классов хранилища узла (конфигурация, ценные данные, воспроизводимые данные), доступный сервисам через символьные ссылки вида `@tierN`.
**Tier (уровень)** — один из трёх стандартных классов хранилища узла (конфигурация, ценные данные, воспроизводимые данные), доступный сервисам через символьные ссылки вида `@tierN`.
---
@@ -1,10 +1,10 @@
---
title: Руководства
description: Практические руководства по сборке репозиториев узлов, разработке действий и настройке композиций.
description: Практические руководства по сборке репозиториев узлов, разработке действий и настройке композиций.
weight: 20
---
Практические руководства для&nbsp;повседневной работы с&nbsp;apex. Каждое руководство решает одну прикладную задачу: сборку репозитория [узла](/apex/glossary#node), разработку [действий](/apex/glossary#action) для&nbsp;[движка](/apex/glossary#engine), определение [композиций](/apex/glossary#compositions) сервисов и&nbsp;эксплуатацию базового хоста.
Практические руководства для повседневной работы с apex. Каждое руководство решает одну прикладную задачу: сборку репозитория [узла](/apex/glossary#node), разработку [действий](/apex/glossary#action) для [движка](/apex/glossary#engine), определение [композиций](/apex/glossary#compositions) сервисов и эксплуатацию базового хоста.
## Доступные руководства {#available-guides}
@@ -17,7 +17,7 @@ weight: 20
---
**См.&nbsp;также:**
**См. также:**
- [Docs: Getting started](/apex/getting-started)
- [Docs: Concepts](/apex/concepts)
@@ -1,10 +1,10 @@
---
title: Разработка действий
description: Создавайте, переопределяйте и тестируйте действия движка в commons или proprietaries.
description: Создавайте, переопределяйте и тестируйте действия движка в commons или proprietaries.
weight: 22
---
**Контекст:** вся автоматизация устроена в&nbsp;виде [действий](/apex/glossary#action)&nbsp;— по&nbsp;одному Python-модулю на&nbsp;каждое действие, диспетчеризуемых по&nbsp;пути к&nbsp;файлу. Общий код находится в&nbsp;каталоге [commons](/apex/glossary#commons) `actions/` («commons»&nbsp;— общий репозиторий), а&nbsp;специфика конкретного узла&nbsp;— в&nbsp;его `proprietaries/actions/`. Оба дерева используют единый контракт, а&nbsp;запускатель добавляет оба каталога в&nbsp;`sys.path`. Поэтому локальное действие импортирует `engine.*` точно так&nbsp;же, как и&nbsp;действие из&nbsp;commons.
**Контекст:** вся автоматизация устроена в виде [действий](/apex/glossary#action) — по одному Python-модулю на каждое действие, диспетчеризуемых по пути к файлу. Общий код находится в каталоге [commons](/apex/glossary#commons) `actions/` («commons» — общий репозиторий), а специфика конкретного узла — в его `proprietaries/actions/`. Оба дерева используют единый контракт, а запускатель добавляет оба каталога в `sys.path`. Поэтому локальное действие импортирует `engine.*` точно так же, как и действие из commons.
## Минимальное действие {#minimal}
@@ -29,14 +29,14 @@ def run(ctx, args):
ctx.log.success("Конфигурация развёрнута.")
```
Путь к&nbsp;файлу задаёт имя действия: модуль выше вызывается как `apex utils/example`. Верните `0` или `None` при&nbsp;успехе, целое число&nbsp;— для&nbsp;конкретного кода выхода, либо выбросьте `SystemExit(code)`. [Соглашение о&nbsp;кодах выхода](/apex/concepts/actions-and-overlay#exit-codes): `64`&nbsp;— ошибка использования, `65`&nbsp;— ошибка данных, `66`&nbsp;— отсутствие входных данных, `1`&nbsp;— общая ошибка.
Путь к файлу задаёт имя действия: модуль выше вызывается как `apex utils/example`. Верните `0` или `None` при успехе, целое число — для конкретного кода выхода, либо выбросьте `SystemExit(code)`. [Соглашение о кодах выхода](/apex/concepts/actions-and-overlay#exit-codes): `64` — ошибка использования, `65` — ошибка данных, `66` — отсутствие входных данных, `1` — общая ошибка.
## Соглашения для фоновых действий {#unattended}
## Соглашения для фоновых действий {#unattended}
Действия, которые запускаются по&nbsp;cron без&nbsp;надзора оператора, следуют двум правилам, чтобы сбои отправляли оповещение, а&nbsp;не&nbsp;исчезали бесследно:
Действия, которые запускаются по cron без надзора оператора, следуют двум правилам, чтобы сбои отправляли оповещение, а не исчезали бесследно:
- **Оберните тело** в&nbsp;`try/except Exception` и&nbsp;отправьте `ctx.notify.error(...)` перед тем как выбросить `SystemExit(1)`&nbsp;— это Python-аналог перехвата `ERR` в&nbsp;shell.
- **Привяжите код выхода к&nbsp;итоговому уведомлению**: методы `ctx.notify.*` возвращают `False`, если доставить сообщение не&nbsp;удалось. Запуск, чьё статусное сообщение не&nbsp;дошло, всё равно должен завершиться с&nbsp;кодом&nbsp;1.
- **Оберните тело** в `try/except Exception` и отправьте `ctx.notify.error(...)` перед тем как выбросить `SystemExit(1)` — это Python-аналог перехвата `ERR` в shell.
- **Привяжите код выхода к итоговому уведомлению**: методы `ctx.notify.*` возвращают `False`, если доставить сообщение не удалось. Запуск, чьё статусное сообщение не дошло, всё равно должен завершиться с кодом 1.
```python
try:
@@ -54,7 +54,7 @@ def run(ctx, args):
## Переопределение, обёртывание, отключение {#override}
Проприетарный файл с&nbsp;тем&nbsp;же относительным путём, что и&nbsp;действие из&nbsp;commons, полностью перекрывает его. Внутри переопределяющего модуля вызов `ctx.commons.run(args)` обращается к&nbsp;исходной реализации из&nbsp;commons&nbsp;— по&nbsp;паттерну обёртки:
Проприетарный файл с тем же относительным путём, что и действие из commons, полностью перекрывает его. Внутри переопределяющего модуля вызов `ctx.commons.run(args)` обращается к исходной реализации из commons — по паттерну обёртки:
```python {filename="proprietaries/actions/sync/packages.py"}
from engine.descriptor import Meta, Arg
@@ -67,23 +67,23 @@ def run(ctx, args):
return ctx.commons.run(args) # делегирование реализации из commons
```
Чтобы отключить действие на&nbsp;конкретном узле, укажите `DISABLED = "reason"` на&nbsp;верхнем уровне модуля. Диспетчер отклонит запуск такого действия с&nbsp;кодом выхода&nbsp;1, а&nbsp;в&nbsp;общем списке появится отметка `[DISABLED: reason]`.
Чтобы отключить действие на конкретном узле, укажите `DISABLED = "reason"` на верхнем уровне модуля. Диспетчер отклонит запуск такого действия с кодом выхода 1, а в общем списке появится отметка `[DISABLED: reason]`.
## Тестирование {#testing}
Набор тестов commons построен на&nbsp;стандартном модуле `unittest`:
Набор тестов commons построен на стандартном модуле `unittest`:
```bash
./tests/run.sh # python3 -m unittest discover -s tests -p 'test_*.py' -v
```
Изменения в&nbsp;движке должны сохранять тесты зелёными. Быстрая smoke-проверка для&nbsp;любого действия (как из&nbsp;commons, так и&nbsp;проприетарного)&nbsp;— запуск со&nbsp;справкой: команда `apex <name> --help` должна завершаться с&nbsp;кодом&nbsp;0. Это подтверждает, что модуль синтаксически корректен, объект `METADATA` сформирован без&nbsp;ошибок, а&nbsp;механизм обнаружения находит действие.
Изменения в движке должны сохранять тесты зелёными. Быстрая smoke-проверка для любого действия (как из commons, так и проприетарного) — запуск со справкой: команда `apex <name> --help` должна завершаться с кодом 0. Это подтверждает, что модуль синтаксически корректен, объект `METADATA` сформирован без ошибок, а механизм обнаружения находит действие.
Хук pre-commit в&nbsp;commons (скрипт `init.sh` подключает его через `core.hooksPath`) запускает `apex utils/lint-docker-compose --hook` для&nbsp;подготовленных (staged) compose-файлов, поэтому ошибки в&nbsp;композициях отлавливаются ещё на&nbsp;этапе коммита.
Хук pre-commit в commons (скрипт `init.sh` подключает его через `core.hooksPath`) запускает `apex utils/lint-docker-compose --hook` для подготовленных (staged) compose-файлов, поэтому ошибки в композициях отлавливаются ещё на этапе коммита.
---
**См.&nbsp;также:**
**См. также:**
- [Concept: Actions and the overlay](/apex/concepts/actions-and-overlay)
- [Concept: The execution context](/apex/concepts/execution-context)
@@ -1,25 +1,25 @@
---
title: Эксплуатация узла
description: "Операции day-2: конфигурирование, композиции, резервные копии и синхронизация репозитория."
description: "Операции day-2: конфигурирование, композиции, резервные копии и синхронизация репозитория."
weight: 24
---
**Контекст:** после первоначальной настройки [узел](/apex/glossary#node) управляется исключительно через [движок](/apex/glossary#engine). В&nbsp;этом руководстве описаны регулярные операции и&nbsp;их фактические действия на&nbsp;хосте.
**Контекст:** после первоначальной настройки [узел](/apex/glossary#node) управляется исключительно через [движок](/apex/glossary#engine). В этом руководстве описаны регулярные операции и их фактические действия на хосте.
## Конфигурирование хоста {#configure}
Команда `apex configure` выполняет все шаги настройки в&nbsp;строгой последовательности. Если изменился только один слой, нужный шаг можно вызвать отдельно:
Команда `apex configure` выполняет все шаги настройки в строгой последовательности. Если изменился только один слой, нужный шаг можно вызвать отдельно:
| Шаг | Эффект |
|---|---|
| `configure/base` | Готовит базовую систему Debian: обновляет списки пакетов, ставит базовые утилиты (`curl`, `wget`, `git`, `rsyslog`), запускает службу rsyslog, устанавливает `ufw` и&nbsp;вспомогательный скрипт `ufw-docker` в&nbsp;`~/.local/bin/ufw-docker`. |
| `configure/ufw` | Применяет правила хоста, настраивает цепочки правил ядра и&nbsp;пробрасывает правила для&nbsp;контейнеров через `ufw-docker`. Поддерживает флаг `--dry-run`. |
| `configure/crowdsec` | Устанавливает CrowdSec (если он ещё не&nbsp;поставлен), копирует `configs/crowdsec/` в&nbsp;`/etc/crowdsec/`, ставит коллекции и&nbsp;firewall bouncer (если включён в&nbsp;настройках). Поддерживает флаг `--dry-run`. |
| `configure/cron` | Рендерит `configs/cron/crontab` через шаблонизатор и&nbsp;устанавливает расписание. Предусмотрены предпросмотр, аварийная остановка при&nbsp;пустом результате (код выхода&nbsp;65) и&nbsp;откат при&nbsp;ошибке установки. Поддерживает флаг `--dry-run`. |
| `configure/systemd` | Рендерит файлы первого уровня из&nbsp;`configs/systemd/` в&nbsp;`/etc/systemd/system/`, перезагружает конфигурацию systemd (daemon-reload) и&nbsp;активирует каждый юнит `.service`. Поддерживает флаг `--dry-run`. |
| `configure/routing` | Включает пересылку IP-пакетов (forwarding), задаёт правило и&nbsp;таблицу маршрутизации (policy-routing) по&nbsp;метке fwmark для&nbsp;границы анклава. Шаг идемпотентен. |
| `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/`, перезагружает конфигурацию systemd (daemon-reload) и активирует каждый юнит `.service`. Поддерживает флаг `--dry-run`. |
| `configure/routing` | Включает пересылку IP-пакетов (forwarding), задаёт правило и таблицу маршрутизации (policy-routing) по метке fwmark для границы анклава. Шаг идемпотентен. |
После `configure/ufw` обязательно перезапустите службу: `sudo systemctl restart ufw`. Действие выведет напоминание и&nbsp;проверит, что порт SSH остаётся открытым.
После `configure/ufw` обязательно перезапустите службу: `sudo systemctl restart ufw`. Действие выведет напоминание и проверит, что порт SSH остаётся открытым.
## Жизненный цикл композиции {#compose}
@@ -30,7 +30,7 @@ apex compose restart
apex compose up --dry-run # только напечатать команды
```
Все дополнительные аргументы движок передаёт напрямую в&nbsp;`docker compose` каждого проекта. Файлы `.env` и&nbsp;`apex.env` проектных каталогов подставляются автоматически при&nbsp;их наличии.
Все дополнительные аргументы движок передаёт напрямую в `docker compose` каждого проекта. Файлы `.env` и `apex.env` проектных каталогов подставляются автоматически при их наличии.
## Резервные копии {#backups}
@@ -38,14 +38,14 @@ apex compose up --dry-run # только напечатать команды
apex backup/run "$(cat <path-to-telegram-url-file>)"
```
Команда запускает [resticontainer](https://github.com/deytenit/resticontainer) в&nbsp;режиме one-shot под&nbsp;профилем compose `manual`. Процесс проходит следующие этапы:
Команда запускает [resticontainer](https://github.com/deytenit/resticontainer) в режиме one-shot под профилем compose `manual`. Процесс проходит следующие этапы:
1. Инициализирует репозиторий (если он ещё не&nbsp;создан).
2. Считывает compose-метки `restic.*` (`restic.enable`, `restic.backup.paths` и&nbsp;т.&nbsp;д.), чтобы определить, какие сервисы и&nbsp;пути нужно сохранять.
3. Создаёт единый снимок restic для&nbsp;всех выбранных путей хоста (`--host`&nbsp;= `APEX_NODE_HOST`, `--compression max`).
4. Применяет политику хранения (`forget --prune`) и&nbsp;отправляет уведомление.
1. Инициализирует репозиторий (если он ещё не создан).
2. Считывает compose-метки `restic.*` (`restic.enable`, `restic.backup.paths` и т. д.), чтобы определить, какие сервисы и пути нужно сохранять.
3. Создаёт единый снимок restic для всех выбранных путей хоста (`--host` = `APEX_NODE_HOST`, `--compression max`).
4. Применяет политику хранения (`forget --prune`) и отправляет уведомление.
Если ни&nbsp;один сервис с&nbsp;меткой `restic.*` не&nbsp;сформировал нового снимка, действие завершается с&nbsp;ошибкой. Полный процесс описан в&nbsp;разделе [Concept: Tiered storage](/apex/concepts/tiered-storage#backups). Чтобы резервное копирование и&nbsp;отправка отчётов в&nbsp;Telegram выполнялись автономно, добавьте команду в&nbsp;`configs/cron/crontab`.
Если ни один сервис с меткой `restic.*` не сформировал нового снимка, действие завершается с ошибкой. Полный процесс описан в разделе [Concept: Tiered storage](/apex/concepts/tiered-storage#backups). Чтобы резервное копирование и отправка отчётов в Telegram выполнялись автономно, добавьте команду в `configs/cron/crontab`.
## Синхронизация репозитория (capture-up) {#sync}
@@ -53,9 +53,9 @@ apex backup/run "$(cat <path-to-telegram-url-file>)"
apex sync/repository <telegram-bot-url>
```
Действие фиксирует изменения рабочей копии в&nbsp;ветке `sync/<node>`, выполняет rebase на&nbsp;`origin/main` и&nbsp;отправляет ветку через force-push с&nbsp;lease. Затем CI автоматически вливает изменения обратно в&nbsp;`main`. Этот механизм (capture-up) сохраняет актуальное состояние узла в&nbsp;репозиторий.
Действие фиксирует изменения рабочей копии в ветке `sync/<node>`, выполняет rebase на `origin/main` и отправляет ветку через force-push с lease. Затем CI автоматически вливает изменения обратно в `main`. Этот механизм (capture-up) сохраняет актуальное состояние узла в репозиторий.
Действие прерывается с&nbsp;подробным сообщением, если подмодуль commons содержит незакоммиченные изменения или каталог `compositions/` отсутствует. При&nbsp;конфликтах rebase выполнение останавливается для&nbsp;ручного вмешательства. Подробнее архитектурная модель описана в&nbsp;разделе [Concept: Framework architecture](/apex/concepts/architecture#capture-up).
Действие прерывается с подробным сообщением, если подмодуль commons содержит незакоммиченные изменения или каталог `compositions/` отсутствует. При конфликтах rebase выполнение останавливается для ручного вмешательства. Подробнее архитектурная модель описана в разделе [Concept: Framework architecture](/apex/concepts/architecture#capture-up).
## Мониторинг обновлений {#updates}
@@ -63,7 +63,7 @@ apex sync/repository <telegram-bot-url>
apex sync/packages <telegram-bot-url>
```
Формирует единую сводку в&nbsp;Telegram о&nbsp;доступных обновлениях пакетов apt и&nbsp;устаревших образах контейнеров (локальные дайджесты сравниваются с&nbsp;реестром через `skopeo`). Действие ничего не&nbsp;меняет на&nbsp;хосте&nbsp;— это исключительно плановая проверка.
Формирует единую сводку в Telegram о доступных обновлениях пакетов apt и устаревших образах контейнеров (локальные дайджесты сравниваются с реестром через `skopeo`). Действие ничего не меняет на хосте — это исключительно плановая проверка.
## Извлечение сертификатов {#certs}
@@ -71,11 +71,11 @@ apex sync/packages <telegram-bot-url>
apex utils/extract-traefik-certs <acme.json> <dest-dir> [telegram-bot-url]
```
Экспортирует сертификаты из&nbsp;ACME-хранилища граничного прокси в&nbsp;пары `fullchain.pem` и&nbsp;`privkey.pem` для&nbsp;каждого домена (с&nbsp;помощью утилиты в&nbsp;контейнере). Скрипт ограничивает права доступа к&nbsp;ключам до&nbsp;`600` и&nbsp;передаёт владение файлами запустившему пользователю. Это нужно сервисам, которым требуются исходные PEM-файлы вместо готового TLS-соединения от&nbsp;прокси.
Экспортирует сертификаты из ACME-хранилища граничного прокси в пары `fullchain.pem` и `privkey.pem` для каждого домена (с помощью утилиты в контейнере). Скрипт ограничивает права доступа к ключам до `600` и передаёт владение файлами запустившему пользователю. Это нужно сервисам, которым требуются исходные PEM-файлы вместо готового TLS-соединения от прокси.
---
**См.&nbsp;также:**
**См. также:**
- [Getting started with an apex node](/apex/getting-started)
- [Reference: Actions](/apex/reference/actions)
@@ -1,14 +1,14 @@
---
title: Сборка репозитория узла
description: Соберите четыре опоры, зафиксируйте commons и подключите поток capture-up.
description: Соберите четыре опоры, зафиксируйте commons и подключите поток capture-up.
weight: 21
---
**Контекст:** у&nbsp;каждого [узла](/apex/glossary#node)&nbsp;— ровно один репозиторий. Название репозитория может быть любым: [идентичность](/apex/glossary#node-identity) задаётся явно в&nbsp;файле `node.env`, поэтому имя репозитория служит лишь удобным обозначением. В&nbsp;этом руководстве мы пошагово соберём такой репозиторий с&nbsp;нуля.
**Контекст:** у каждого [узла](/apex/glossary#node) — ровно один репозиторий. Название репозитория может быть любым: [идентичность](/apex/glossary#node-identity) задаётся явно в файле `node.env`, поэтому имя репозитория служит лишь удобным обозначением. В этом руководстве мы пошагово соберём такой репозиторий с нуля.
{{% steps %}}
### Инициализируйте репозиторий и&nbsp;зафиксируйте commons
### Инициализируйте репозиторий и зафиксируйте commons
```bash
git init <node-repo> && cd <node-repo>
@@ -18,7 +18,7 @@ git add .gitmodules commons
mkdir -p proprietaries/actions proprietaries/lib configs compositions/apex .github/workflows
```
Ссылка gitlink фиксирует точную версию движка, на&nbsp;которой работает узел. Чтобы обновить движок в&nbsp;будущем, достаточно переключиться на&nbsp;новый тег внутри каталога `commons/` и&nbsp;закоммитить смещение указателя.
Ссылка gitlink фиксирует точную версию движка, на которой работает узел. Чтобы обновить движок в будущем, достаточно переключиться на новый тег внутри каталога `commons/` и закоммитить смещение указателя.
### Объявите идентичность
@@ -30,14 +30,14 @@ APEX_GIT_AUTHOR_NAME=apex [bot]
APEX_GIT_AUTHOR_EMAIL=apex@localhost
```
Файл `node.env` явно задаёт параметры идентификации узла (доменная зона не&nbsp;подразумевается заранее):
Файл `node.env` явно задаёт параметры идентификации узла (доменная зона не подразумевается заранее):
- `APEX_NODE_FQDN`&nbsp;— FQDN хоста для&nbsp;маршрутизации Traefik по&nbsp;правилу `Host()` (если не&nbsp;указан, движок выведет предупреждение и&nbsp;возьмёт имя хоста из&nbsp;ОС).
- `APEX_NODE_HOST`&nbsp;— публичный адрес хоста для&nbsp;параметра `--host` резервного копирования и&nbsp;метки `instance` в&nbsp;системе мониторинга и&nbsp;уведомлениях (по&nbsp;умолчанию совпадает с&nbsp;`APEX_NODE_FQDN`). Первая часть FQDN до&nbsp;точки используется как короткое имя (`name`) узла.
- `APEX_SUBNET`&nbsp;— выделенная подсеть /24 для&nbsp;внутренней сети `direct`. Параметр **обязателен**: без&nbsp;него запуск завершится с&nbsp;кодом&nbsp;66 (см. [Concept: Fleet addressing scheme](/apex/concepts/addressing)).
- `APEX_GIT_AUTHOR_NAME` и&nbsp;`APEX_GIT_AUTHOR_EMAIL`&nbsp;— имя и&nbsp;почта сервисного бота capture-up (если не&nbsp;заданы, применяются значения по&nbsp;умолчанию).
- `APEX_NODE_FQDN` — FQDN хоста для маршрутизации Traefik по правилу `Host()` (если не указан, движок выведет предупреждение и возьмёт имя хоста из ОС).
- `APEX_NODE_HOST` — публичный адрес хоста для параметра `--host` резервного копирования и метки `instance` в системе мониторинга и уведомлениях (по умолчанию совпадает с `APEX_NODE_FQDN`). Первая часть FQDN до точки используется как короткое имя (`name`) узла.
- `APEX_SUBNET` — выделенная подсеть /24 для внутренней сети `direct`. Параметр **обязателен**: без него запуск завершится с кодом 66 (см. [Concept: Fleet addressing scheme](/apex/concepts/addressing)).
- `APEX_GIT_AUTHOR_NAME` и `APEX_GIT_AUTHOR_EMAIL` — имя и почта сервисного бота capture-up (если не заданы, применяются значения по умолчанию).
### Напишите базовую обёртку и&nbsp;файлы переменных окружения
### Напишите базовую обёртку и файлы переменных окружения
```yaml {filename="compositions/apex/docker-compose.yml"}
name: apex
@@ -60,11 +60,11 @@ APEX_OBS_BASIC_AUTH_USER=<node-user>
# APEX_RESTIC_AWS_ENDPOINT= # напр., URL S3-совместимого эндпоинта
```
Файл `apex.env` хранит только публичные параметры конфигурации, которые безопасно коммитить в&nbsp;репозиторий. Переменная `COMPOSE_PROFILES` определяет набор опциональных базовых сервисов: `adguard`, `xray`, `alloy` (агент сбора метрик и&nbsp;логов) и&nbsp;`manual` (разовые запуски бэкапа); список может быть пустым.
Файл `apex.env` хранит только публичные параметры конфигурации, которые безопасно коммитить в репозиторий. Переменная `COMPOSE_PROFILES` определяет набор опциональных базовых сервисов: `adguard`, `xray`, `alloy` (агент сбора метрик и логов) и `manual` (разовые запуски бэкапа); список может быть пустым.
Все секреты, которые нельзя хранить в&nbsp;Git, задокументируйте в&nbsp;`compositions/apex/SECRETS.md`. Это поможет при&nbsp;развёртывании нового узла заполнить локальный файл `.env`: пароль `APEX_RESTIC_PASSWORD`, ключи доступа к&nbsp;S3, токен `APEX_TRAEFIK_CF_DNS_API_TOKEN`, почту `APEX_TRAEFIK_ACME_EMAIL`, а&nbsp;также `APEX_OBS_BASIC_AUTH_PASS` (если включён мониторинг).
Все секреты, которые нельзя хранить в Git, задокументируйте в `compositions/apex/SECRETS.md`. Это поможет при развёртывании нового узла заполнить локальный файл `.env`: пароль `APEX_RESTIC_PASSWORD`, ключи доступа к S3, токен `APEX_TRAEFIK_CF_DNS_API_TOKEN`, почту `APEX_TRAEFIK_ACME_EMAIL`, а также `APEX_OBS_BASIC_AUTH_PASS` (если включён мониторинг).
### Настройте правила игнорирования для&nbsp;Git
### Настройте правила игнорирования для Git
```gitignore {filename=".gitignore"}
**/@tier1
@@ -78,15 +78,15 @@ __pycache__/
### Добавьте bootstrap-скрипт
Скрипт `init.sh` подготавливает свежий клон репозитория на&nbsp;хосте: выполняет `git submodule update --init --recursive`, настраивает хуки `git config core.hooksPath commons/githooks`, добавляет репозиторий в&nbsp;доверенные (`git config --global --add safe.directory <repo>`) и&nbsp;прописывает `<repo>/commons` в&nbsp;переменную `PATH` командной оболочки, чтобы утилита `apex` была доступна глобально. Не&nbsp;забудьте выставить флаг исполняемого файла: `chmod +x init.sh`.
Скрипт `init.sh` подготавливает свежий клон репозитория на хосте: выполняет `git submodule update --init --recursive`, настраивает хуки `git config core.hooksPath commons/githooks`, добавляет репозиторий в доверенные (`git config --global --add safe.directory <repo>`) и прописывает `<repo>/commons` в переменную `PATH` командной оболочки, чтобы утилита `apex` была доступна глобально. Не забудьте выставить флаг исполняемого файла: `chmod +x init.sh`.
### Заведите реестр адресации
Создайте файл `compositions/ADDRESSING.md` с&nbsp;подсетью узла (`APEX_SUBNET`) и&nbsp;схемой распределения адресов: диапазоны `.10`–`.19` зарезервированы за&nbsp;базовыми сервисами (core), а&nbsp;`.20` и&nbsp;выше выделяются под&nbsp;проекты. Все назначаемые в&nbsp;будущем [якорные IP](/apex/glossary#anchor-ip) фиксируются в&nbsp;этом документе.
Создайте файл `compositions/ADDRESSING.md` с подсетью узла (`APEX_SUBNET`) и схемой распределения адресов: диапазоны `.10`–`.19` зарезервированы за базовыми сервисами (core), а `.20` и выше выделяются под проекты. Все назначаемые в будущем [якорные IP](/apex/glossary#anchor-ip) фиксируются в этом документе.
### Подключите CI capture-up
Настройте периодический CI-workflow, который автоматически вливает ветку `sync/<node>` в&nbsp;`main` (если ветка существует) с&nbsp;авторством бота. Шаг checkout должен выполняться **без** загрузки подмодулей: слияние только сдвигает указатель `commons`, а&nbsp;SSH-ссылка на&nbsp;подмодуль недоступна по&nbsp;стандартному CI-токену. Полный цикл описан в&nbsp;разделе [Concept: Framework architecture](/apex/concepts/architecture) ([capture-up](/apex/glossary#capture-up)).
Настройте периодический CI-workflow, который автоматически вливает ветку `sync/<node>` в `main` (если ветка существует) с авторством бота. Шаг checkout должен выполняться **без** загрузки подмодулей: слияние только сдвигает указатель `commons`, а SSH-ссылка на подмодуль недоступна по стандартному CI-токену. Полный цикл описан в разделе [Concept: Framework architecture](/apex/concepts/architecture) ([capture-up](/apex/glossary#capture-up)).
### Проверьте работу
@@ -94,18 +94,18 @@ __pycache__/
./commons/apex --help
```
В&nbsp;списке должны появиться все базовые действия из&nbsp;commons, а&nbsp;позже&nbsp;— и&nbsp;локальные действия из&nbsp;proprietaries. При&nbsp;запуске вне хоста (до&nbsp;заполнения `node.env`) появится предупреждение о&nbsp;том, что `APEX_NODE_FQDN` не&nbsp;задан: в&nbsp;этом случае движок подставит системное имя хоста. При&nbsp;этом переменная `APEX_SUBNET` обязательно должна быть указана, иначе выполнение завершится с&nbsp;кодом&nbsp;66. Закоммитьте подготовленный репозиторий в&nbsp;ветку `main`.
В списке должны появиться все базовые действия из commons, а позже — и локальные действия из proprietaries. При запуске вне хоста (до заполнения `node.env`) появится предупреждение о том, что `APEX_NODE_FQDN` не задан: в этом случае движок подставит системное имя хоста. При этом переменная `APEX_SUBNET` обязательно должна быть указана, иначе выполнение завершится с кодом 66. Закоммитьте подготовленный репозиторий в ветку `main`.
{{% /steps %}}
## Заметки
- Каталог `proprietaries/` изначально может быть пустым (сохраните структуру с&nbsp;помощью файлов `.gitkeep`): движок корректно работает без&nbsp;пользовательских переопределений.
- Переносите конфигурационные файлы хоста в&nbsp;`configs/` (`ufw/`, `cron/crontab`, `systemd/`, `crowdsec/`) по&nbsp;мере подключения шагов `configure/*`. Действия считывают шаблоны из&nbsp;`configs/` напрямую.
- Каталог `proprietaries/` изначально может быть пустым (сохраните структуру с помощью файлов `.gitkeep`): движок корректно работает без пользовательских переопределений.
- Переносите конфигурационные файлы хоста в `configs/` (`ufw/`, `cron/crontab`, `systemd/`, `crowdsec/`) по мере подключения шагов `configure/*`. Действия считывают шаблоны из `configs/` напрямую.
---
**См.&nbsp;также:**
**См. также:**
- [Getting started with an apex node](/apex/getting-started)
- [Concept: Framework architecture](/apex/concepts/architecture)
@@ -1,16 +1,16 @@
---
title: Добавление композиции сервисов
description: "Подключите новый проект к узлу: якорный IP, сети, уровни и линтинг."
description: "Подключите новый проект к узлу: якорный IP, сети, уровни и линтинг."
weight: 23
---
**Контекст:** проект подключается к&nbsp;[узлу](/apex/glossary#node) в&nbsp;виде отдельного каталога в&nbsp;`compositions/` со&nbsp;своим compose-файлом. Требования фреймворка к&nbsp;этому файлу минимальны: статический [якорный IP](/apex/glossary#anchor-ip) в&nbsp;сети `direct`, подключение к&nbsp;внешним сетям по&nbsp;имени и&nbsp;пути к&nbsp;данным относительно уровней хранения.
**Контекст:** проект подключается к [узлу](/apex/glossary#node) в виде отдельного каталога в `compositions/` со своим compose-файлом. Требования фреймворка к этому файлу минимальны: статический [якорный IP](/apex/glossary#anchor-ip) в сети `direct`, подключение к внешним сетям по имени и пути к данным относительно уровней хранения.
{{% steps %}}
### Выделите якорный адрес
Выберите следующий свободный адрес `.20` или выше в&nbsp;подсети узла и&nbsp;зафиксируйте его в&nbsp;`compositions/ADDRESSING.md`. Диапазон `.10`–`.19` зарезервирован для&nbsp;[базовой композиции](/apex/glossary#core-composition) (см. [Concept: Fleet addressing scheme](/apex/concepts/addressing)).
Выберите следующий свободный адрес `.20` или выше в подсети узла и зафиксируйте его в `compositions/ADDRESSING.md`. Диапазон `.10`–`.19` зарезервирован для [базовой композиции](/apex/glossary#core-composition) (см. [Concept: Fleet addressing scheme](/apex/concepts/addressing)).
### Напишите файл compose
@@ -52,14 +52,14 @@ networks:
Применяемые соглашения:
- **Один якорь на&nbsp;проект**: контейнер-якорь получает постоянный `ipv4_address` в&nbsp;сети `direct`, а&nbsp;вспомогательные sidecar-контейнеры используют его сетевой стек через `network_mode: service:<anchor>`.
- **Внешние сети подключаются по&nbsp;имени**: для&nbsp;сетей `direct`, `enclave` и&nbsp;`socket` указывается `external: true`, поскольку они объявляются в&nbsp;базовой композиции.
- **Публикация через `enclave` и&nbsp;метки Traefik**: подключение к&nbsp;сети `enclave` вместе с&nbsp;метками маршрутизации открывает сервис через граничный прокси. Прокси связывается с&nbsp;ним через socket-прокси, без&nbsp;проброса портов наружу.
- **Пути к&nbsp;данным привязаны к&nbsp;уровням**: пути вида `./@tierN/...` разрешаются через символические ссылки, которые создаёт действие `tiers/link`.
- **Резервное копирование настраивается метками `restic.*`**: действие `apex backup/run` запускает утилиту [resticontainer](https://github.com/deytenit/resticontainer). Сервис помечается меткой `restic.enable=true`, а&nbsp;пути монтирования для&nbsp;бэкапа перечисляются в&nbsp;`restic.backup.paths=<container-path>` (resticontainer сопоставляет их с&nbsp;путями на&nbsp;хосте). Сервисы без&nbsp;этих меток игнорируются. Сохраняйте только восстановимые данные и&nbsp;конфигурацию.
- **Идентификаторы UID и&nbsp;GID**: переменные `${APEX_UID...}` берутся из&nbsp;файла `.env` проекта, который формирует действие `tiers/useradd`.
- **Один якорь на проект**: контейнер-якорь получает постоянный `ipv4_address` в сети `direct`, а вспомогательные sidecar-контейнеры используют его сетевой стек через `network_mode: service:<anchor>`.
- **Внешние сети подключаются по имени**: для сетей `direct`, `enclave` и `socket` указывается `external: true`, поскольку они объявляются в базовой композиции.
- **Публикация через `enclave` и метки Traefik**: подключение к сети `enclave` вместе с метками маршрутизации открывает сервис через граничный прокси. Прокси связывается с ним через socket-прокси, без проброса портов наружу.
- **Пути к данным привязаны к уровням**: пути вида `./@tierN/...` разрешаются через символические ссылки, которые создаёт действие `tiers/link`.
- **Резервное копирование настраивается метками `restic.*`**: действие `apex backup/run` запускает утилиту [resticontainer](https://github.com/deytenit/resticontainer). Сервис помечается меткой `restic.enable=true`, а пути монтирования для бэкапа перечисляются в `restic.backup.paths=<container-path>` (resticontainer сопоставляет их с путями на хосте). Сервисы без этих меток игнорируются. Сохраняйте только восстановимые данные и конфигурацию.
- **Идентификаторы UID и GID**: переменные `${APEX_UID...}` берутся из файла `.env` проекта, который формирует действие `tiers/useradd`.
### Настройте хранилище и&nbsp;права доступа
### Настройте хранилище и права доступа
```bash
apex tiers/link <tier1> <tier2> <tier3>
@@ -67,7 +67,7 @@ apex tiers/useradd
apex tiers/chown
```
Повторный запуск этих команд идемпотентен: движок находит новый проект, создаёт для&nbsp;него каталоги хранения и&nbsp;файл `.env`, заводит системного пользователя `noroot-<project>` и&nbsp;выставляет права доступа.
Повторный запуск этих команд идемпотентен: движок находит новый проект, создаёт для него каталоги хранения и файл `.env`, заводит системного пользователя `noroot-<project>` и выставляет права доступа.
### Проверьте конфигурацию
@@ -75,7 +75,7 @@ apex tiers/chown
apex utils/lint-docker-compose
```
Действие проверяет корректность, уникальность и&nbsp;принадлежность подсети для&nbsp;каждого якорного IP, после чего запускает проверку compose-файлов по&nbsp;всему репозиторию. Тот&nbsp;же линтинг автоматически выполняется хуком pre-commit для&nbsp;всех подготовленных к&nbsp;коммиту файлов.
Действие проверяет корректность, уникальность и принадлежность подсети для каждого якорного IP, после чего запускает проверку compose-файлов по всему репозиторию. Тот же линтинг автоматически выполняется хуком pre-commit для всех подготовленных к коммиту файлов.
### Разверните сервисы
@@ -83,18 +83,18 @@ apex utils/lint-docker-compose
apex compose up
```
Базовая композиция уже работает, поэтому новый проект запустится в&nbsp;порядке своей очереди по&nbsp;алфавиту. Проверьте статус контейнеров через `docker ps`, а&nbsp;доступность маршрута&nbsp;— через граничный прокси.
Базовая композиция уже работает, поэтому новый проект запустится в порядке своей очереди по алфавиту. Проверьте статус контейнеров через `docker ps`, а доступность маршрута — через граничный прокси.
{{% /steps %}}
## Заметки
- Секретные данные проекта (например, пароль к&nbsp;базе данных) хранятся в&nbsp;собственном `.env` на&nbsp;уровне 1&nbsp;(@tier1), рядом с&nbsp;автоматически сгенерированным блоком UID.
- Не&nbsp;объявляйте сети `direct`, `enclave` и&nbsp;`socket` заново внутри файла проекта: дублирование определений приведёт к&nbsp;конфликту с&nbsp;базовой композицией за&nbsp;управление сетью.
- Секретные данные проекта (например, пароль к базе данных) хранятся в собственном `.env` на уровне 1 (@tier1), рядом с автоматически сгенерированным блоком UID.
- Не объявляйте сети `direct`, `enclave` и `socket` заново внутри файла проекта: дублирование определений приведёт к конфликту с базовой композицией за управление сетью.
---
**См.&nbsp;также:**
**См. также:**
- [Concept: Compositions and the shared core](/apex/concepts/compositions)
- [Concept: Tiered storage and privilege isolation](/apex/concepts/tiered-storage)
@@ -18,37 +18,37 @@ weight: 32
`apex configure/base`
Проверяет, что операционная система — Debian (иначе завершается с кодом `1`). Обновляет индекс пакетов, устанавливает необходимые пакеты (`curl`, `wget`, `git`, `rsyslog`), проверяет статус службы `rsyslog`, устанавливает `ufw`, если его нет, и загружает вспомогательную утилиту `ufw-docker` в `~/.local/bin/ufw-docker` (если утилита уже есть, загрузка пропускается).
Проверяет, что операционная система — 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`). В конце выводит напоминание о необходимости перезагрузки правил.
Применяет правила межсетевого экрана узла из `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` выполняет рендеринг без записи на диск и пропускает установку.
Устанавливает 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`. Результат рендеринга всегда выводится для проверки.
Рендерит `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`.
Рендерит каждый открытый файл (кроме скрытых) верхнего уровня из `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 и локальный маршрут по умолчанию в этой таблице. Обе операции идемпотентны: повторные запуски ничего не меняют.
Включает `net.ipv4.ip_forward`, настраивает правило политики fwmark `0x1` → routing-table-100 и локальный маршрут по умолчанию в этой таблице. Обе операции идемпотентны: повторные запуски ничего не меняют.
## sync {#sync}
@@ -56,13 +56,13 @@ weight: 32
`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 операция отменяется и отправляется оповещение в Telegram. Об успехе и каждом сбое отправляется уведомление; сбой при отправке уведомления об успехе также завершает команду с кодом `1`.
Коммит [«capture-up»](/apex/glossary#capture-up) (фиксация локальных изменений): переключается на ветку `sync/<node>` (или создаёт её), индексирует всё дерево вместе с обновлённым указателем на подмодуль commons, создаёт коммит от имени бота (если есть изменения), выполняет rebase на `origin/main`, обновляет подмодули и отправляет изменения командой с флагом `--force-with-lease`. Прерывается с кодом `1`, если подмодуль commons содержит незакоммиченные правки или отсутствует каталог `compositions/`. При конфликте во время rebase операция отменяется и отправляется оповещение в Telegram. Об успехе и каждом сбое отправляется уведомление; сбой при отправке уведомления об успехе также завершает команду с кодом `1`.
### sync/packages
`apex sync/packages <telegram_bot_url>`
Только для Debian. Собирает список доступных обновлений apt (`--just-print`) и устаревших образов запущенных контейнеров (сравнивая локальный RepoDigest с дайджестом в реестре через `skopeo`), после чего отправляет единую сводку в Telegram. Работает исключительно в режиме чтения: пакеты и образы не изменяются. При необработанных сбоях отправляет оповещение в Telegram и завершается с кодом `1`; так же действие ведёт себя и при ошибке доставки сводки.
Только для Debian. Собирает список доступных обновлений apt (`--just-print`) и устаревших образов запущенных контейнеров (сравнивая локальный RepoDigest с дайджестом в реестре через `skopeo`), после чего отправляет единую сводку в Telegram. Работает исключительно в режиме чтения: пакеты и образы не изменяются. При необработанных сбоях отправляет оповещение в Telegram и завершается с кодом `1`; так же действие ведёт себя и при ошибке доставки сводки.
## backup {#backup}
@@ -70,7 +70,7 @@ weight: 32
`apex backup/run <telegram_bot_url>`
Управляет однократным запуском основного сервиса `resticontainer`, работающего по меткам (профиль `manual`). Проверяет доступность репозитория (при необходимости выполняет инициализацию), затем запускает процедуру `backup`. При этом resticontainer находит каждый сервис с метками compose вида `restic.*`, вызывает их предварительные хуки, останавливает контейнеры с меткой `restic.backup.stop`, сопоставляет пути монтирования контейнеров с путями на хосте и создаёт единый снимок за один вызов restic. Снимок помечается флагами `--host <APEX_NODE_HOST>`, `--tag biweekly`, `--compression max`. Затем защитная проверка удостоверяется, что свежий снимок для этого хоста действительно создан: если метки `restic.*` не были найдены, резервное копирование завершилось бы вхолостую, поэтому действие откажется сообщать об успехе. Следом применяется политика хранения `forget --prune` (7 daily / 4 weekly / 12 monthly). При любом сбое restic отправляет оповещение и завершается с кодом `1`. При успешном завершении также отправляется оповещение, а ошибка его доставки приводит к выходу с кодом `1`.
Управляет однократным запуском основного сервиса `resticontainer`, работающего по меткам (профиль `manual`). Проверяет доступность репозитория (при необходимости выполняет инициализацию), затем запускает процедуру `backup`. При этом resticontainer находит каждый сервис с метками compose вида `restic.*`, вызывает их предварительные хуки, останавливает контейнеры с меткой `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}
@@ -78,13 +78,13 @@ weight: 32
`apex tiers/link <tier1_path> <tier2_path> <tier3_path>`
Создаёт полную структуру символьных ссылок для [уровней хранения](/apex/glossary#tier): ссылки уровня узла `compositions/@tierN`, каталоги уровней для каждого проекта, общие области `shared/` с обратными ссылками, проектные ссылки `@tierN` и символьную ссылку `.env` на первый уровень (если файл назначения отсутствует, он создаётся). Если существующий путь не является символьной ссылкой, действие запрашивает подтверждение; отказ пропускает создание ссылки.
Создаёт полную структуру символьных ссылок для [уровней хранения](/apex/glossary#tier): ссылки уровня узла `compositions/@tierN`, каталоги уровней для каждого проекта, общие области `shared/` с обратными ссылками, проектные ссылки `@tierN` и символьную ссылку `.env` на первый уровень (если файл назначения отсутствует, он создаётся). Если существующий путь не является символьной ссылкой, действие запрашивает подтверждение; отказ пропускает создание ссылки.
### tiers/useradd
`apex tiers/useradd`
Проверяет наличие группы `noroot-shared`, затем создаёт по одному заблокированному системному пользователю `noroot-<project>` для каждого проекта композиций (собственная основная группа, без домашнего каталога, оболочка `nologin`, участник общей группы) и прописывает переменные `APEX_UID`/`APEX_GID`/`APEX_SHARED_GID` в `.env` каждого проекта между служебными маркерами.
Проверяет наличие группы `noroot-shared`, затем создаёт по одному заблокированному системному пользователю `noroot-<project>` для каждого проекта композиций (собственная основная группа, без домашнего каталога, оболочка `nologin`, участник общей группы) и прописывает переменные `APEX_UID`/`APEX_GID`/`APEX_SHARED_GID` в `.env` каждого проекта между служебными маркерами.
### tiers/chown
@@ -98,7 +98,7 @@ weight: 32
`apex utils/extract-traefik-certs <source_acme> <dest_dir> [telegram_bot_url]`
Извлекает сертификаты из хранилища ACME в файлы `fullchain.pem`/`privkey.pem` для каждого домена с помощью контейнера-дампера, выставляет права `chmod 600` на ключи и передаёт права владения выходным каталогом вызывающему пользователю. Если исходный файл отсутствует, действие завершается с кодом `1`. При ошибках извлечения отправляет оповещение в Telegram (если указан URL).
Извлекает сертификаты из хранилища ACME в файлы `fullchain.pem`/`privkey.pem` для каждого домена с помощью контейнера-дампера, выставляет права `chmod 600` на ключи и передаёт права владения выходным каталогом вызывающему пользователю. Если исходный файл отсутствует, действие завершается с кодом `1`. При ошибках извлечения отправляет оповещение в Telegram (если указан URL).
### utils/lint-docker-compose
@@ -112,7 +112,7 @@ weight: 32
`apex compose <action> [--dry-run] [extra...]`
Оркеструет запуск `docker compose <action>` по всему узлу: сначала основной проект, затем сервисы в алфавитном порядке (в обратном — для команды `down`). Команда `up` по умолчанию использует ключ `-d`; файлы `.env` и `apex.env` подключаются для каждого проекта при их наличии; дополнительные аргументы пробрасываются дальше; флаг `--dry-run` (принимается в любой позиции) выводит сформированные команды без их выполнения.
Оркеструет запуск `docker compose <action>` по всему узлу: сначала основной проект, затем сервисы в алфавитном порядке (в обратном — для команды `down`). Команда `up` по умолчанию использует ключ `-d`; файлы `.env` и `apex.env` подключаются для каждого проекта при их наличии; дополнительные аргументы пробрасываются дальше; флаг `--dry-run` (принимается в любой позиции) выводит сформированные команды без их выполнения.
---
+13 -13
View File
@@ -4,38 +4,38 @@ description: "Лаунчер apex: перечисление, диспетчер
weight: 31
---
Лаунчер — это скрипт на Python из стандартной библиотеки, `commons/apex` (попадает в `PATH` после выполнения `init.sh`). Все операции выполняются над локальным checkout: аргумента узла не существует.
Лаунчер — это скрипт на Python из стандартной библиотеки, `commons/apex` (попадает в `PATH` после выполнения `init.sh`). Все операции выполняются над локальным checkout: аргумента узла не существует.
## Формы вызова {#invocation}
| Команда | Поведение |
|---|---|
| `apex` или `apex --help` | Перечислить каждое доступное [действие](/apex/glossary#action) с его кратким описанием; выход с кодом `0`. |
| `apex` или `apex --help` | Перечислить каждое доступное [действие](/apex/glossary#action) с его кратким описанием; выход с кодом `0`. |
| `apex <group>/<action> [args...]` | Выполнить одно действие. |
| `apex <group>/<action> --help` | Вывести справку argparse для действия, сгенерированную из `METADATA`; выход с кодом `0`. |
| `apex <group>/<action> --help` | Вывести справку argparse для действия, сгенерированную из `METADATA`; выход с кодом `0`. |
Перечисление помечает каждую запись источником [оверлея](/apex/glossary#overlay) — без метки (commons), `(local)`, `(local override)` — и добавляет `[DISABLED: reason]`, если модуль объявляет `DISABLED`. Перечисление использует статическое сканирование через `ast`, поэтому код самих действий не выполняется.
Перечисление помечает каждую запись источником [оверлея](/apex/glossary#overlay) — без метки (commons), `(local)`, `(local override)` — и добавляет `[DISABLED: reason]`, если модуль объявляет `DISABLED`. Перечисление использует статическое сканирование через `ast`, поэтому код самих действий не выполняется.
## Раскладка и разрешение путей {#layout}
При запуске лаунчер определяет, в какой из двух раскладок он находится:
При запуске лаунчер определяет, в какой из двух раскладок он находится:
- **Checkout узла** — лаунчер находится в каталоге `commons/`, родительский каталог которого содержит `node.env`. Родительский каталог служит корнем репозитория; `proprietaries/actions/` — дерево оверлея; [идентичность узла](/apex/glossary#node-identity) разрешается перед диспетчеризацией, а любые предупреждения об идентичности выводятся первыми в логе.
- **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/`).
`sys.path` формируется следующим образом: каталог commons (позволяет импортировать `engine`), затем `commons/vendor/` при наличии и `../proprietaries/` в checkout узла (позволяет импортировать проприетарный код из `lib/`).
## Коды возврата {#exit-codes}
| Код | Источник |
|---|---|
| `0` | Действие вернуло `0` или нецелое значение; вызовы `--help`. |
| `1` | Неизвестное действие; отключённое действие; необработанное исключение (выводится в лог как `Type: message` с префиксом контекста); нецелый `SystemExit`. |
| `64` | Ошибка использования при разборе аргументов (`EX_USAGE`). |
| `0` | Действие вернуло `0` или нецелое значение; вызовы `--help`. |
| `1` | Неизвестное действие; отключённое действие; необработанное исключение (выводится в лог как `Type: message` с префиксом контекста); нецелый `SystemExit`. |
| `64` | Ошибка использования при разборе аргументов (`EX_USAGE`). |
| `65` | Ошибка данных, вызванная библиотеками движка (`EX_DATAERR`) — например, пустой результат рендеринга crontab. |
| `66` | Неполный или отсутствующий ввод (`EX_NOINPUT`) — например, не заданы поля идентичности или отсутствует каталог конфигурации ufw. |
| `66` | Неполный или отсутствующий ввод (`EX_NOINPUT`) — например, не заданы поля идентичности или отсутствует каталог конфигурации ufw. |
Любой другой код — это значение, которое вернуло или вызвало само действие.
Любой другой код — это значение, которое вернуло или вызвало само действие.
## Формат лога {#log-format}
@@ -45,7 +45,7 @@ weight: 31
[<node>] [<group>/<action>] [LEVEL] message
```
`LEVEL` — это `INFO`, `WARN`, `ERROR` или `SUCCESS`. Строки `ERROR` выводятся в stderr, а остальные — в stdout. Такой формат удобен для cron: упрощает сбор логов и отправку оповещений.
`LEVEL` — это `INFO`, `WARN`, `ERROR` или `SUCCESS`. Строки `ERROR` выводятся в stderr, а остальные — в stdout. Такой формат удобен для cron: упрощает сбор логов и отправку оповещений.
---
@@ -4,7 +4,7 @@ description: API дескрипторов и ctx, доступные автор
weight: 34
---
Инструменты для разработки [действий](/apex/glossary#action): дескрипторы аргументов в `engine.descriptor` и методы контекста выполнения. Концептуальные основы описаны в разделе [Концепция: Контекст выполнения](/apex/concepts/execution-context).
Инструменты для разработки [действий](/apex/glossary#action): дескрипторы аргументов в `engine.descriptor` и методы контекста выполнения. Концептуальные основы описаны в разделе [Концепция: Контекст выполнения](/apex/concepts/execution-context).
## Дескрипторы {#descriptors}
@@ -14,16 +14,16 @@ from engine.descriptor import Meta, Arg, Opt, Flag, Rest
| Класс | Сигнатура | Сопоставление с argparse |
|---|---|---|
| `Meta` | `Meta(summary, args=[])` | Управляет как кратким описанием в перечислении, так и парсером. |
| `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`.
Ошибки использования приводят к выходу с кодом `64` вместо стандартного для argparse кода `2`.
> [!WARNING]
> `Rest` захватывает *всё* после первого позиционного аргумента — включая флаги. `Flag`, объявленный рядом с `Rest`, распознаётся только *перед* позиционным аргументом; действие, которое должно принимать свой флаг в любом месте, обязано само извлечь его из остатка (действие `compose` делает именно так для `--dry-run`).
> `Rest` захватывает *всё* после первого позиционного аргумента — включая флаги. `Flag`, объявленный рядом с `Rest`, распознаётся только *перед* позиционным аргументом; действие, которое должно принимать свой флаг в любом месте, обязано само извлечь его из остатка (действие `compose` делает именно так для `--dry-run`).
## ctx.log {#log}
@@ -33,31 +33,31 @@ from engine.descriptor import Meta, Arg, Opt, Flag, Rest
| Метод | Поведение |
|---|---|
| `run(cmd, check=True, capture=False, cwd=None, env=None, input=None)` | Обёртка над `subprocess.run`, возвращающая `CompletedProcess`; `capture=True` собирает текстовые потоки stdout/stderr. |
| `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)` | Запуск и включение при необходимости; возвращает итоговое состояние активности. |
| `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`. |
| `install_packages(packages)` | Для каждого пакета: пропускает, если бинарный файл уже есть в `PATH`, иначе выполняет `apt-get update -y` и `apt-get install -y`. |
## ctx.tpl {#tpl}
| Метод | Поведение |
|---|---|
| `render(path, vars)` | Рендерит один файл (относительные пути отсчитываются от `configs/`); семантика `safe_substitute` — неизвестные переменные сохраняются как есть. |
| `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=True` сохраняются права целевого файла. |
| `backup(path, sudo=False)` | Копирует файл в `<path>.<epoch>.bak`; возвращает путь к резервной копии или `None`. |
| `write_file(path, content, backup=True, dry_run=False, sudo=False)` | Атомарная запись через временный файл (права `0644`); опциональное создание резервной копии; при `sudo=True` сохраняются права целевого файла. |
| `inject_block(target, content, start, end, anchor=None, old_pairs=(), dry_run=False, sudo=True)` | Заменяет управляемый блок по маркерам: удаляет текущие и устаревшие блоки, вставляет новый блок после первой строки-якоря. Возвращает `False`, если якорь отсутствует. Если целевой файл не найден, завершается с ошибкой. |
| `install_crontab(text, dry_run=False)` | Создаёт резервную копию crontab, завершается с ошибкой при пустом результате рендеринга (`SystemExit(65)`), выводит предпросмотр, устанавливает crontab через `crontab -` и восстанавливает прежнюю версию при сбое (`SystemExit(1)`). |
| `install_crontab(text, dry_run=False)` | Создаёт резервную копию crontab, завершается с ошибкой при пустом результате рендеринга (`SystemExit(65)`), выводит предпросмотр, устанавливает crontab через `crontab -` и восстанавливает прежнюю версию при сбое (`SystemExit(1)`). |
## ctx.ufw {#ufw}
@@ -69,15 +69,15 @@ from engine.descriptor import Meta, Arg, Opt, Flag, Rest
## ctx.systemd {#systemd}
`deploy_units(configs_subdir, vars, dry_run=False)` — рендерит файлы верхнего уровня (не скрытые) из подкаталога в `/etc/systemd/system/`, затем выполняет daemon-reload и `enable --now` для каждого сервиса `.service`.
`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`), префикс домена или узла к нему не добавляется.
`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 — допустимо только в переопределяющем проприетарном модуле.
`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 — допустимо только в переопределяющем проприетарном модуле.
---
@@ -4,70 +4,70 @@ description: "Переменные окружения APEX_*: источники
weight: 33
---
Пространство имён `APEX_*` охватывает всю конфигурацию фреймворка. Переменные формируются на четырёх уровнях: при сборке окружения процесса действиями оркестрации значения из более поздних слоёв переопределяют предыдущие.
Пространство имён `APEX_*` охватывает всю конфигурацию фреймворка. Переменные формируются на четырёх уровнях: при сборке окружения процесса действиями оркестрации значения из более поздних слоёв переопределяют предыдущие.
## node.env — идентичность {#node-env}
## node.env — идентичность {#node-env}
Закоммичен в корне репозитория; считывается при разрешении [идентичности узла](/apex/glossary#node-identity). Фреймворк не привязан к доменной структуре: узел объявляет собственный FQDN и публичное имя хоста. Концепция кластера отсутствует, а формат FQDN ничем не ограничен.
Закоммичен в корне репозитория; считывается при разрешении [идентичности узла](/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_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}
## apex.env — закоммиченные скаляры {#apex-env}
Закоммичен в `compositions/apex/`; не содержит секретных данных.
| Переменная | Значение | Примечания |
|---|---|---|
| `APEX_TRAEFIK_DIRECT_IP` | [Якорный IP](/apex/glossary#anchor-ip) краевого прокси в подсети узла | Обязательна для основной композиции (`?error`). |
| `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). Задаётся для любого 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`, а не здесь. |
| `APEX_RESTIC_AWS_ENDPOINT` | URL эндпоинта бэкенда S3 | Опциональна; по умолчанию не задан (AWS S3). Задаётся для любого 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}
## .env — секреты {#secrets}
Не коммитится в репозиторий; представляет собой символьную ссылку в хранилище первого уровня. Список ключей каждого узла задокументирован в `SECRETS.md`.
| Переменная | Значение | Примечания |
|---|---|---|
| `APEX_UID`, `APEX_GID`, `APEX_SHARED_GID` | Идентификаторы пользователя noroot для проекта | Внедряются автоматически через `tiers/useradd`; обязательны (`?error`). |
| `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_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 | Не требуется для основной композиции. Задаётся только на узле, принимающем данные наблюдаемости. |
| `APEX_OBS_BASIC_AUTH_PASS` | Пароль basic-auth для отправки метрик и логов наблюдаемости | Требуется для профиля `alloy` (`?error`). |
| `APEX_LOKI_BASIC_AUTH` | Пользователи htpasswd для центрального шлюза Loki/Prometheus | Не требуется для основной композиции. Задаётся только на узле, принимающем данные наблюдаемости. |
## Экспорты движка — ctx.vars() {#engine}
## Экспорты движка — ctx.vars() {#engine}
Вычисляются [движком](/apex/glossary#engine) при запуске; экспортируются в `docker compose` действиями оркестрации и доступны при рендеринге любого шаблона. Не задавайте их вручную.
Вычисляются [движком](/apex/glossary#engine) при запуске; экспортируются в `docker compose` действиями оркестрации и доступны при рендеринге любого шаблона. Не задавайте их вручную.
| Переменная | Значение |
|---|---|
| `APEX_NODE_HOST` | Публичное имя хоста узла (`node.host`); используется для `--host` в резервном копировании и как `instance` для наблюдаемости и уведомлений. |
| `APEX_NODE_FQDN` | Действительный FQDN узла (`node.fqdn`); используется для правил краевой маршрутизации. |
| `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_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` прерывает разбор конфигурации, если хотя бы одна из них не задана. Благодаря этому пропущенный секрет приводит к немедленной и понятной ошибке.
> Для переменных, отмеченных выше как `?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` — разрешённый диапазон IP-адресов в ipallowlist для маршрутов к панелям Traefik и AdGuard).
Ряд переменных основной композиции использует конструкцию `${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` — разрешённый диапазон IP-адресов в ipallowlist для маршрутов к панелям Traefik и AdGuard).
---
@@ -14,19 +14,19 @@ weight: 40
**Решение:** явно укажите `APEX_NODE_FQDN` в файле `node.env` в корне репозитория (например, `APEX_NODE_FQDN=node1.example.com`).
## Разрешение идентичности завершается с кодом 66 {#identity-66}
## Разрешение идентичности завершается с кодом 66 {#identity-66}
**Симптомы:** `identity: APEX_NODE_FQDN missing from node.env and no hostname` или `identity: APEX_SUBNET missing from node.env`, код выхода `66`.
**Диагностика:** не удалось определить обязательный параметр идентичности. Первое сообщение указывает, что ни в `node.env`, ни в имени хоста ОС нет FQDN; второе — что не задана подсеть `APEX_SUBNET`. Движок не пытается угадывать эти значения.
**Диагностика:** не удалось определить обязательный параметр идентичности. Первое сообщение указывает, что ни в `node.env`, ни в имени хоста ОС нет FQDN; второе — что не задана подсеть `APEX_SUBNET`. Движок не пытается угадывать эти значения.
**Решение:** укажите нужные переменные в файле `node.env` в корне репозитория: задайте `APEX_SUBNET` и `APEX_NODE_FQDN` (либо настройте корректное имя хоста в ОС).
## configure/ufw завершается с кодом 66 {#ufw-66}
## configure/ufw завершается с кодом 66 {#ufw-66}
**Симптомы:** `Rules directory does not exist: ...` или `Config directory does not exist: ...`, код выхода `66`.
**Диагностика:** отсутствует одна из директорий: `configs/ufw/docker/` либо `configs/ufw/host/`. Это свидетельствует о нарушении структуры каталогов, а не об осознанном отключении правил.
**Диагностика:** отсутствует одна из директорий: `configs/ufw/docker/` либо `configs/ufw/host/`. Это свидетельствует о нарушении структуры каталогов, а не об осознанном отключении правил.
**Решение:** создайте недостающую директорию (она может быть пустой: в этом случае `docker/` лишь выведет предупреждение `No .rules files found ... nothing to apply.` и продолжит выполнение).
@@ -38,11 +38,11 @@ weight: 40
**Решение:** запустите `apex configure/base`, затем повторите вызов `apex configure/ufw`.
## Установка crontab отклонена или откачена {#cron}
## Установка crontab отклонена или откачена {#cron}
**Симптомы:** `Rendered crontab is empty; aborting (previous kept).` (код выхода `65`) или `crontab install failed; restoring previous.` (код выхода `1`).
**Диагностика:** сгенерированный файл `configs/cron/crontab` оказался пустым (ошибка в шаблоне или переменных: неизвестные `$VARS` остаются как есть и не очищают файл) либо утилита `crontab` отклонила синтаксис.
**Диагностика:** сгенерированный файл `configs/cron/crontab` оказался пустым (ошибка в шаблоне или переменных: неизвестные `$VARS` остаются как есть и не очищают файл) либо утилита `crontab` отклонила синтаксис.
**Решение:** проверьте вывод предпросмотра, исправьте шаблон и запустите `apex configure/cron` заново. В обоих случаях предыдущая версия crontab остаётся без изменений.
@@ -54,7 +54,7 @@ weight: 40
**Решение:** при наличии изменений в субмодуле сбросьте их командой `git -C commons checkout -- .` (либо зафиксируйте намеренное обновление субмодуля в коммите). Если отсутствует дерево каталогов, восстановите чекаут до следующего планового запуска.
## Telegram не доставлен {#telegram}
## Telegram не доставлен {#telegram}
**Симптомы:** в логе появляется `Telegram Bot URL not provided. Skipping notification.` или `Failed to send Telegram notification: <error>`. При этом действия, отправляющие уведомления (`backup/run`, `sync/repository`, `sync/packages`), завершаются с кодом `1`, даже если основная задача выполнена.
@@ -66,7 +66,7 @@ weight: 40
**Симптомы:** команда `docker compose` сообщает об ошибке на директиве `include` при обработке `compositions/apex/docker-compose.yml`.
**Диагностика:** [базовая композиция](/apex/glossary#core-composition) подключается через директиву `include:`, для которой требуется Docker Compose версии 2.20 или новее.
**Диагностика:** [базовая композиция](/apex/glossary#core-composition) подключается через директиву `include:`, для которой требуется Docker Compose версии 2.20 или новее.
**Решение:** обновите плагин Compose (проверить текущую версию можно командой `docker compose version`).