fix: PageHead header alignment, analytics proxy, and infra improvements (#64)
🤖 Pull request created from `various-fixes` containing PageHead
breadcrumbs and navigation alignment fixes, Umami analytics proxy, and
infrastructure updates.
This commit is contained in:
1 parent
ff5a50bad8
commit
9ee672504f
110 files changed
+2649
-1760
No files matched your search
@@ -1,20 +1,20 @@
|
||||
---
|
||||
title: Концепции
|
||||
description: "Архитектура, идентичность, действия, контекст выполнения, композиции, хранилище, адресация и безопасность."
|
||||
description: "Архитектура, идентичность, действия, контекст выполнения, композиции, хранилище, адресация и безопасность."
|
||||
weight: 10
|
||||
---
|
||||
|
||||
Теоретические основы фреймворка операций флота apex: как [узел](/apex/glossary#node) объявляет свою [идентичность](/apex/concepts/node-identity), как выполняются [действия](/apex/glossary#action) и как узел организует свои сервисы, хранилище и адресацию.
|
||||
Теоретические основы фреймворка операций флота apex: как [узел](/apex/glossary#node) объявляет свою [идентичность](/apex/concepts/node-identity), как выполняются [действия](/apex/glossary#action) и как узел организует свои сервисы, хранилище и адресацию.
|
||||
|
||||
{{% cards %}}
|
||||
{{< card link="architecture" title="Архитектура" icon="share" subtitle="Слои движка, commons и configs и как они сочетаются друг с другом." >}}
|
||||
{{< card link="node-identity" title="Идентичность узла" icon="finger-print" subtitle="Как узел объявляет, кто он и где ему место." >}}
|
||||
{{< card link="actions-and-overlay" title="Действия и оверлей" icon="lightning-bolt" subtitle="Общие действия и локальные переопределения узла." >}}
|
||||
{{< card link="execution-context" title="Контекст выполнения" icon="terminal" subtitle="Окружение, которое каждое действие получает во время выполнения." >}}
|
||||
{{< card link="compositions" title="Композиции" icon="cube" subtitle="Как стеки сервисов объявляются и разворачиваются на каждом узле." >}}
|
||||
{{< card link="tiered-storage" title="Многоуровневое хранилище" icon="database" subtitle="Стандартизированная классификация хранилища для данных сервисов." >}}
|
||||
{{< card link="addressing" title="Адресация" icon="globe-alt" subtitle="Подсеть узла, якорные IP и соглашение об анклаве." >}}
|
||||
{{< card link="security-model" title="Модель безопасности" icon="shield-check" subtitle="Изоляция привилегий и усиление защиты сети." >}}
|
||||
{{< card link="architecture" title="Архитектура" icon="share" subtitle="Слои движка, commons и configs: как они устроены и сочетаются друг с другом." >}}
|
||||
{{< card link="node-identity" title="Идентичность узла" icon="finger-print" subtitle="Как узел объявляет, кто он и где его место во флоте." >}}
|
||||
{{< card link="actions-and-overlay" title="Действия и оверлей" icon="lightning-bolt" subtitle="Общие действия и локальные переопределения узла." >}}
|
||||
{{< card link="execution-context" title="Контекст выполнения" icon="terminal" subtitle="Окружение, которое каждое действие получает во время выполнения." >}}
|
||||
{{< card link="compositions" title="Композиции" icon="cube" subtitle="Как стеки сервисов объявляются и разворачиваются на каждом узле." >}}
|
||||
{{< card link="tiered-storage" title="Многоуровневое хранилище" icon="database" subtitle="Стандартизированная классификация хранилища для данных сервисов." >}}
|
||||
{{< card link="addressing" title="Адресация" icon="globe-alt" subtitle="Подсеть узла, якорные IP-адреса и соглашение об анклаве." >}}
|
||||
{{< card link="security-model" title="Модель безопасности" icon="shield-check" subtitle="Изоляция привилегий и усиление защиты сети." >}}
|
||||
{{% /cards %}}
|
||||
|
||||
---
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
---
|
||||
title: "Действия и оверлей"
|
||||
description: "Контракт действия и как узло-локальные проприетарии затеняют действия commons."
|
||||
description: "Контракт действия и затенение действий commons локальными проприетариями узла."
|
||||
weight: 13
|
||||
---
|
||||
|
||||
Каждая единица автоматизации в apex — это [действие](/apex/glossary#action): один Python-модуль, который [движок](/apex/glossary#engine) диспетчеризует по имени. [Оверлей](/apex/glossary#overlay) позволяет [проприетариям](/apex/glossary#proprietaries) узла добавлять, переопределять или отключать действия [«commons»](/apex/glossary#commons), не форкая их. Эта страница определяет контракт действия, декларативные дескрипторы аргументов и правила разрешения.
|
||||
Каждая единица автоматизации в apex — это [действие](/apex/glossary#action): один Python-модуль, который [движок](/apex/glossary#engine) диспетчеризует по имени. [Оверлей](/apex/glossary#overlay) позволяет [проприетариям](/apex/glossary#proprietaries) узла добавлять, переопределять или отключать действия [«commons»](/apex/glossary#commons) без создания форков. На этой странице описаны контракт действия, декларативные дескрипторы аргументов и правила разрешения.
|
||||
|
||||
## Контракт действия {#contract}
|
||||
|
||||
Действие — это один файл `.py` в дереве `actions/`. Путь к файлу **является** именем действия: `actions/<group>/<action>.py` вызывается как `apex <group>/<action> [args...]`. Нет реестра, который нужно обновлять — размещение файла в дереве публикует действие.
|
||||
Действие — это отдельный файл `.py` в дереве `actions/`. Путь к файлу служит именем действия: `actions/<group>/<action>.py` вызывается как `apex <group>/<action> [args...]`. Обновлять реестр не нужно: появление файла в дереве сразу публикует действие.
|
||||
|
||||
Модуль должен предоставлять две вещи:
|
||||
|
||||
- **`METADATA = Meta(...)`** → краткое описание и дескрипторы аргументов, которые управляют как текстом справки, так и парсингом. Система дескрипторов пришла на смену heredoc-у `action_metadata()` bash-фреймворка «root» commons, предшественника apex.
|
||||
- **`run(ctx, args)`** → точка входа. `ctx` — это построенный движком контекст выполнения; `args` — распарсенное пространство имён `argparse`.
|
||||
- **`METADATA = Meta(...)`** → краткое описание и дескрипторы аргументов, управляющие как текстом справки, так и парсингом. Система дескрипторов пришла на смену heredoc-блоку `action_metadata()` bash-фреймворка «root» commons, предшественника apex.
|
||||
- **`run(ctx, args)`** → точка входа. `ctx` — сформированный движком контекст выполнения; `args` — распарсенное пространство имён `argparse`.
|
||||
|
||||
```python {filename="actions/<group>/<action>.py"}
|
||||
from engine.descriptor import Meta, Arg, Flag
|
||||
@@ -31,38 +31,38 @@ def run(ctx, args):
|
||||
return 0
|
||||
```
|
||||
|
||||
Возвращаемое значение `run` становится кодом завершения процесса. Нецелочисленный возврат (включая `None`) трактуется как успех (`0`). Необработанное исключение логируется с префиксом контекста и завершается с кодом `1`.
|
||||
Возвращаемое значение `run` становится кодом завершения процесса. Нецелочисленный результат (включая `None`) трактуется как успешное завершение (`0`). Необработанное исключение логируется с префиксом контекста, после чего процесс завершается с кодом `1`.
|
||||
|
||||
> [!NOTE]
|
||||
> `METADATA` технически необязательна — модуль без неё диспетчеризуется с пустым `Meta` и не принимает аргументов — но каждое настоящее действие должно её объявлять, поскольку вывод в перечислении и `--help` генерируется из неё.
|
||||
> Технически `METADATA` необязательна: модуль без неё диспетчеризуется с пустым `Meta` и не принимает аргументов. Однако в реальных действиях её объявлять необходимо, поскольку вывод перечисления и справка `--help` генерируются именно из неё.
|
||||
|
||||
## Декларативные аргументы {#arguments}
|
||||
|
||||
`Meta.args` — это список дескрипторов. Каждый отображается ровно на одну конструкцию `argparse`:
|
||||
`Meta.args` — это список дескрипторов. Каждый отображается ровно на одну конструкцию `argparse`:
|
||||
|
||||
| Дескриптор | Отображается на | Поведение |
|
||||
| --- | --- | --- |
|
||||
| `Arg("name")` | позиционный аргумент | Обязателен по умолчанию; `required=False` делает его опциональным позиционным (`nargs="?"`, по умолчанию `None`). |
|
||||
| `Opt("--name", default=...)` | опция со значением | Опциональный флаг, принимающий значение, с объявленным значением по умолчанию. |
|
||||
| `Arg("name")` | позиционный аргумент | Обязателен по умолчанию; `required=False` делает его опциональным позиционным (`nargs="?"`, по умолчанию `None`). |
|
||||
| `Opt("--name", default=...)` | опция со значением | Опциональный флаг, принимающий значение, с объявленным значением по умолчанию. |
|
||||
| `Flag("--name")` | булев флаг | `store_true`; наличие означает `True`. |
|
||||
| `Rest("name")` | остаток | Жадная сквозная передача всех оставшихся argv (`argparse.REMAINDER`). |
|
||||
| `Rest("name")` | остаток | Жадная сквозная передача всех оставшихся аргументов командной строки (`argparse.REMAINDER`). |
|
||||
|
||||
Движок строит парсер как `apex <group>/<action>` и парсит оставшиеся argv перед вызовом `run`. `--help` обрабатывается на этапе парсинга. Ошибка использования — неизвестный флаг, отсутствующий позиционный аргумент — печатает usage и завершается с кодом `64` (`EX_USAGE`) вместо стандартного для argparse `2`.
|
||||
Движок строит парсер как `apex <group>/<action>` и разбирает оставшиеся аргументы перед вызовом `run`. Флаг `--help` обрабатывается на этапе парсинга. При ошибке использования (неизвестный флаг, отсутствующий позиционный аргумент) выводится справка по синтаксису (usage), а процесс завершается с кодом `64` (`EX_USAGE`) вместо стандартного для argparse кода `2`.
|
||||
|
||||
## Перечисление без импорта {#listing}
|
||||
|
||||
`apex` без аргументов (или `--help`) перечисляет каждое доступное действие. Перечисление никогда не импортирует модули действий: движок статически сканирует каждый файл модулем `ast` и извлекает только краткое описание `METADATA` и любую константу `DISABLED`. Поэтому код действия не может выполниться — или упасть — во время перечисления; файл, который не удаётся распарсить, перечисляется как `(unparseable)`, а не ломает перечисление.
|
||||
Команда `apex` без аргументов (или с флагом `--help`) выводит список всех доступных действий. Перечисление никогда не импортирует модули действий: движок статически сканирует каждый файл с помощью модуля `ast` и извлекает только краткое описание `METADATA` и любую константу `DISABLED`. Поэтому код действия не выполняется и не может вызвать сбой во время перечисления: файл, который не удалось распарсить, помечается как `(unparseable)`, не нарушая работу всей команды.
|
||||
|
||||
Каждая запись помечается своим источником оверлея:
|
||||
|
||||
- нет метки → действие commons,
|
||||
- `(local)` → существует только в проприетариях,
|
||||
- `(local override)` → действие проприетариев, затеняющее действие commons,
|
||||
- `[DISABLED: reason]` → действие объявляет `DISABLED`.
|
||||
- нет метки → действие commons;
|
||||
- `(local)` → существует только в проприетариях;
|
||||
- `(local override)` → действие проприетариев, затеняющее действие commons;
|
||||
- `[DISABLED: reason]` → действие объявляет константу `DISABLED`.
|
||||
|
||||
## Оверлей {#overlay}
|
||||
|
||||
Имена действий разрешаются по двум деревьям, сначала проприетарии:
|
||||
Имена действий разрешаются по двум деревьям, начиная с проприетариев:
|
||||
|
||||
{{< filetree/container >}}
|
||||
{{< filetree/folder name="node repository" state="open" >}}
|
||||
@@ -81,26 +81,26 @@ def run(ctx, args):
|
||||
{{< /filetree/folder >}}
|
||||
{{< /filetree/container >}}
|
||||
|
||||
Движок определяет эту схему по своему собственному расположению: когда он запускается из каталога `commons/`, родитель которого содержит `node.env`, родитель является корнем репозитория [узла](/apex/glossary#node), а `proprietaries/actions/` — деревом оверлея. Автономный checkout commons является собственным корнем и не имеет слоя оверлея. Внутри репозитория узла [идентичность узла](/apex/glossary#node-identity) разрешается из FQDN хоста перед диспетчеризацией.
|
||||
Движок определяет структуру размещения по своему расположению на диске: если запуск происходит из каталога `commons/`, а в родительском каталоге присутствует `node.env`, родительский каталог считается корнем репозитория [узла](/apex/glossary#node), а `proprietaries/actions/` — деревом оверлея. Автономный checkout commons сам выступает в качестве корня и не имеет слоя оверлея. Внутри репозитория узла [идентичность узла](/apex/glossary#node-identity) разрешается из FQDN хоста перед диспетчеризацией.
|
||||
|
||||
Разрешение для заданного имени проверяет `proprietaries/actions/<name>.py` первым и `commons/actions/<name>.py` вторым; ровно один модуль загружается по пути. Неизвестное имя — это ошибка (код завершения `1`). Оверлей поддерживает четыре приёма:
|
||||
При разрешении имени движок сначала проверяет `proprietaries/actions/<name>.py`, а затем `commons/actions/<name>.py`; по найденному пути загружается ровно один модуль. Неизвестное имя считается ошибкой (код завершения `1`). Оверлей поддерживает четыре сценария:
|
||||
|
||||
- **Добавление (Add)** → файл, существующий только в проприетариях, становится узло-локальным действием, перечисляется как `(local)`.
|
||||
- **Переопределение (Override)** → файл проприетариев по тому же относительному пути полностью затеняет реализацию commons; путь commons записывается в разрешении как `shadowed`.
|
||||
- **Обёртывание (Wrap)** → переопределяющий модуль может делегировать реализации, которую он затеняет, через `ctx.commons.run(args)`, добавляя узло-локальное поведение до или после логики commons вместо её замены.
|
||||
- **Отключение (Disable)** → модуль, присваивающий `DISABLED = "reason"`, отклоняется при диспетчеризации: движок логирует причину и завершается с кодом `1`. Перечисление показывает `[DISABLED: reason]`. Голый `DISABLED = True` также отключает, с обобщённой причиной `disabled`; пустая строка не отключает.
|
||||
- **Добавление (Add)** → файл, существующий только в проприетариях, становится локальным действием узла и отображается в списке как `(local)`.
|
||||
- **Переопределение (Override)** → файл проприетариев по тому же относительному пути полностью затеняет реализацию commons; путь commons помечается при разрешении как `shadowed`.
|
||||
- **Обёртывание (Wrap)** → переопределяющий модуль может делегировать вызов затенённой реализации через `ctx.commons.run(args)`, дополняя поведение commons локальной логикой узла до или после её вызова.
|
||||
- **Отключение (Disable)** → модуль с объявлением `DISABLED = "reason"` отклоняется при диспетчеризации: движок логирует причину и завершается с кодом `1`. При перечислении выводится `[DISABLED: reason]`. Простое значение `DISABLED = True` также отключает действие со стандартной причиной `disabled`; пустая строка действие не отключает.
|
||||
|
||||
## Коды завершения {#exit-codes}
|
||||
|
||||
Действия и движок следуют фиксированному соглашению о кодах завершения:
|
||||
Действия и движок следуют единому соглашению о кодах завершения:
|
||||
|
||||
| Код | Значение | Типичная причина |
|
||||
| --- | --- | --- |
|
||||
| `0` | успех | `run` вернул `0` или нецелочисленное значение. |
|
||||
| `0` | успех | Функция `run` вернула `0` или нецелочисленное значение. |
|
||||
| `1` | общая ошибка | Неизвестное действие, отключённое действие, необработанное исключение, нецелочисленный `SystemExit`. |
|
||||
| `64` | ошибка использования | Аргументы отклонены парсером (`EX_USAGE`). |
|
||||
| `65` | ошибка данных | Некорректные входные данные (`EX_DATAERR`), поднимается библиотеками движка. |
|
||||
| `66` | отсутствующий ввод | Требуемый ввод отсутствует (`EX_NOINPUT`), поднимается библиотеками движка. |
|
||||
| `65` | ошибка данных | Некорректные входные данные (`EX_DATAERR`), выбрасывается библиотеками движка. |
|
||||
| `66` | отсутствующий ввод | Требуемый ввод отсутствует (`EX_NOINPUT`), выбрасывается библиотеками движка. |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
---
|
||||
title: "Схема адресации флота"
|
||||
description: "Подсети /24 для каждого узла и статические якорные IP внутри пространства анклава (по умолчанию 198.18.0.0/16)."
|
||||
description: "Подсети /24 для каждого узла и статические якорные IP внутри пространства анклава (по умолчанию 198.18.0.0/16)."
|
||||
weight: 17
|
||||
---
|
||||
|
||||
Apex назначает каждому сервису фиксированный, предсказуемый IPv4-адрес внутри подсети узла. Каждый узел объявляет собственный `/24`, якоря сервисов — это статические адреса внутри него, а вся схема механически принудительно проверяется линтером якорных IP.
|
||||
Фреймворк apex назначает каждому сервису фиксированный, предсказуемый IPv4-адрес внутри подсети узла. Каждый узел объявляет собственный `/24`, якоря сервисов — это статические адреса внутри него, а вся схема автоматически валидируется линтером якорных IP.
|
||||
|
||||
## Адресное пространство {#address-space}
|
||||
|
||||
Пространство [анклава](/apex/glossary#enclave) по умолчанию равно `198.18.0.0/16` — диапазону для бенчмаркинга, выделенному [RFC 2544: Benchmarking Methodology for Network Interconnect Devices](https://datatracker.ietf.org/doc/html/rfc2544). Этот диапазон никогда не маршрутизируется в публичном Интернете и не конфликтует с распространёнными частными диапазонами (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), которые уже используют облачные провайдеры и домашние сети. Оператор может переопределить его через `APEX_ENCLAVE_SUBNET`.
|
||||
Пространство [анклава](/apex/glossary#enclave) по умолчанию равно `198.18.0.0/16` — диапазону для бенчмаркинга, выделенному [RFC 2544: Benchmarking Methodology for Network Interconnect Devices](https://datatracker.ietf.org/doc/html/rfc2544). Этот диапазон никогда не маршрутизируется в публичном интернете и не конфликтует с распространёнными частными диапазонами (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), которые уже используют облачные провайдеры и домашние сети. Оператор может переопределить его через переменную `APEX_ENCLAVE_SUBNET`.
|
||||
|
||||
## Подсети узлов {#node-subnets}
|
||||
|
||||
Каждый [узел](/apex/glossary#node) объявляет ровно один `/24` как `APEX_SUBNET` в своём `node.env`, и эта подсеть является диапазоном IPAM сети `direct`. Оператор назначает её; по соглашению она располагается внутри пространства анклава (`198.18.0.0/16` по умолчанию или того, что задано в `APEX_ENCLAVE_SUBNET`), но фреймворк не накладывает на этот выбор никакой арифметики.
|
||||
Каждый [узел](/apex/glossary#node) объявляет ровно один диапазон `/24` как `APEX_SUBNET` в своём файле `node.env`. Эта подсеть служит пулом IPAM сети `direct`. Оператор назначает её вручную: по соглашению она располагается внутри пространства анклава (`198.18.0.0/16` по умолчанию или в подсети из `APEX_ENCLAVE_SUBNET`), однако фреймворк не накладывает строгих ограничений на выбор адресов.
|
||||
|
||||
> [!NOTE]
|
||||
> В качестве опционального соглашения оператора вы можете организовать `/24` узлов в непрерывные блоки (например, группируя связанные узлы), чтобы сохранять читаемость распределений. Это вопрос учёта, а не правило фреймворка.
|
||||
> В качестве соглашения оператор может группировать подсети `/24` связанных узлов в непрерывные блоки, чтобы сохранять наглядность адресного плана. Это вопрос внутренней организации, а не жёсткое требование фреймворка.
|
||||
|
||||
## Распределение внутри `/24` узла {#node-allocation}
|
||||
|
||||
Внутри `/24` узла адреса хостов разделяются по ролям:
|
||||
Внутри подсети `/24` узла адреса хостов разделяются по ролям:
|
||||
|
||||
| Диапазон | Назначение |
|
||||
|---|---|
|
||||
| `.10`–`.19` | Зарезервировано для якорей [базовой композиции](/apex/glossary#core-composition); `traefik` всегда `.10` (через `APEX_TRAEFIK_DIRECT_IP`) |
|
||||
| `.20` и выше | Якоря сервисов, распределяемые по `<project>` |
|
||||
| `.10`–`.19` | Зарезервировано для якорей [базовой композиции](/apex/glossary#core-composition); сервис `traefik` всегда занимает адрес `.10` (через `APEX_TRAEFIK_DIRECT_IP`) |
|
||||
| `.20` и выше | Якоря сервисов, распределяемые по проектам (`<project>`) |
|
||||
|
||||
Каждый якорь сервиса — это статический `ipv4_address` в сети direct — фиксированный [якорный IP](/apex/glossary#anchor-ip), а не динамически назначаемая аренда. Распределения записываются в `compositions/ADDRESSING.md`, который служит человекочитаемым реестром подсети узла.
|
||||
Каждый якорь сервиса представляет собой статический `ipv4_address` в сети direct — фиксированный [якорный IP](/apex/glossary#anchor-ip), а не динамическую аренду (lease). Назначенные адреса фиксируются в `compositions/ADDRESSING.md`, который служит человекочитаемым реестром подсети узла.
|
||||
|
||||
## Принудительная проверка {#enforcement}
|
||||
## Автоматическая проверка {#enforcement}
|
||||
|
||||
Линтер compose проверяет якорные IP по всем [композициям](/apex/glossary#compositions) на узле. Для каждого `ipv4_address`, найденного в `compositions/*/docker-compose.yml`, он проверяет, что адрес:
|
||||
Линтер compose проверяет якорные IP-адреса во всех [композициях](/apex/glossary#compositions) узла. Для каждого значения `ipv4_address`, найденного в файлах `compositions/*/docker-compose.yml`, проверяются следующие условия:
|
||||
|
||||
- **Корректно сформирован** — некорректный литерал сообщается как ошибка линтера, а не крах.
|
||||
- **Уникален** — один и тот же якорный IP не может встречаться в двух проектах; дубликаты сообщаются с указанием обоих конфликтующих проектов.
|
||||
- **Внутри подсети узла** — каждый якорь должен попадать в объявленный `/24` узла.
|
||||
- **Корректный формат** — о некорректном литерале линтер сообщает как об ошибке, не падая аварийно.
|
||||
- **Уникальность** — один и тот же якорный IP не может использоваться в двух разных проектах; при обнаружении дубликатов выводятся имена обоих конфликтующих проектов.
|
||||
- **Принадлежность подсети узла** — каждый якорь должен входить в объявленную подсеть `/24` узла.
|
||||
|
||||
Любое нарушение приводит к провалу линтера с кодом выхода `1` (общая ошибка). Успешное прохождение проверки логирует `Anchor IP validation passed.` Реализация находится в [действии](/apex/glossary#action) `utils/lint-docker-compose`; см. [GitHub: apex.ermnvldmr.com](https://github.com/deytenit/apex.ermnvldmr.com).
|
||||
При любом нарушении линтер завершается с ошибкой и кодом `1`. При успешной проверке в лог выводится сообщение `Anchor IP validation passed.` Логика проверки реализована в [действии](/apex/glossary#action) `utils/lint-docker-compose`; см. [GitHub: apex.ermnvldmr.com](https://github.com/deytenit/apex.ermnvldmr.com).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
title: Архитектура фреймворка
|
||||
description: Четыре опоры репозиториев узла, субмодуль «commons» и поток диспетчеризации.
|
||||
description: Четыре опоры репозиториев узла, субмодуль commons и поток диспетчеризации.
|
||||
weight: 11
|
||||
---
|
||||
|
||||
apex разделяет автоматизацию флота на один общий репозиторий — [«commons» (общий репозиторий)](/apex/glossary#commons) — и по одному репозиторию на каждый [узел](/apex/glossary#node). «commons» несёт всё то, что общее для каждого узла: [движок](/apex/glossary#engine), общие [действия](/apex/glossary#action) и [ядровую композицию](/apex/glossary#core-composition). Репозиторий узла несёт всё то, что делает этот узел самим собой: его идентичность, хостовые [конфигурации](/apex/glossary#configs), сервисные [композиции](/apex/glossary#compositions) и узло-локальные [проприетарии](/apex/glossary#proprietaries).
|
||||
Фреймворк apex делит автоматизацию флота между одним общим репозиторием ([«commons»](/apex/glossary#commons)) и отдельными репозиториями для каждого [узла](/apex/glossary#node). В commons сосредоточено всё общее для флота: [движок](/apex/glossary#engine), стандартные [действия](/apex/glossary#action) и [ядровая композиция](/apex/glossary#core-composition). Репозиторий узла содержит специфичные для хоста данные: его идентичность, хостовые [конфигурации](/apex/glossary#configs), сервисные [композиции](/apex/glossary#compositions) и локальные [проприетарии](/apex/glossary#proprietaries).
|
||||
|
||||
## Четыре опоры {#pillars}
|
||||
|
||||
@@ -23,16 +23,16 @@ apex разделяет автоматизацию флота на один об
|
||||
|
||||
| Опора | Содержимое | Владение |
|
||||
|---|---|---|
|
||||
| `commons/` | Git-субмодуль → `apex.ermnvldmr.com`, закреплённый на релизном теге | Общее, версионируемое |
|
||||
| `proprietaries/` | Узло-локальные действия, библиотеки и исходники инструментов ([накладываются оверлеем](/apex/glossary#overlay) поверх commons) | Узел |
|
||||
| `commons/` | Git-субмодуль → `apex.ermnvldmr.com`, закреплённый на релизном теге | Общее, версионируемое |
|
||||
| `proprietaries/` | Узло-локальные действия, библиотеки и исходники инструментов ([накладываются оверлеем](/apex/glossary#overlay) поверх commons) | Узел |
|
||||
| `configs/` | Исходники конфигурации хоста: `ufw/`, `cron/crontab`, `systemd/`, `crowdsec/` | Узел |
|
||||
| `compositions/` | По одному каталогу на проект плюс ядровая обёртка `apex/` и реестр адресации | Узел |
|
||||
| `compositions/` | По одному каталогу на проект, ядровая обёртка `apex/` и реестр адресации | Узел |
|
||||
|
||||
Закрепление субмодуля — это версионный контракт фреймворка: узел выполняет ровно тот движок и ту ядровую композицию, которые записаны в его gitlink. Обновление узла — это осознанное действие: переключитесь на более новый тег внутри `commons/` и зафиксируйте перемещённый указатель.
|
||||
Фиксация субмодуля — версионный контракт фреймворка: узел выполняет именно ту версию движка и ядровой композиции, которая зафиксирована в его gitlink. Обновление узла выполняется явно: достаточно переключиться на новый тег внутри `commons/` и закоммитить изменённый указатель.
|
||||
|
||||
## Поток диспетчеризации {#dispatch}
|
||||
|
||||
Лаунчер `commons/apex` действует на локальный checkout; аргумента узла нет нигде.
|
||||
Лаунчер `commons/apex` работает с локальным чекаутом; передавать узел в качестве аргумента не нужно нигде.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
@@ -47,27 +47,27 @@ flowchart TD
|
||||
H --> I["run(ctx, args)"]
|
||||
```
|
||||
|
||||
Движок определяет схему размещения по своему собственному расположению: запуск из каталога с именем `commons/`, родитель которого содержит `node.env`, означает «checkout узла» — родитель становится корнем репозитория, а `proprietaries/actions/` становится деревом оверлея. Любое другое расположение — это автономный checkout commons, выступающий в роли собственного корня.
|
||||
Движок определяет режим работы по своему расположению на диске. Если он запущен из каталога `commons/`, а в родительском каталоге присутствует `node.env`, то родительский каталог принимается за корень репозитория узла, а `proprietaries/actions/` становится деревом оверлея. При любом другом расположении commons считается автономным чекаутом и выступает собственным корнем.
|
||||
|
||||
Перечисление (`apex --help`) никогда не импортирует модули действий; краткие описания и маркеры отключённых действий извлекаются статическим сканированием через `ast`. Диспетчеризация загружает ровно один модуль по пути. Подробности в [Концепция: Действия и оверлей](/apex/concepts/actions-and-overlay).
|
||||
Команда перечисления (`apex --help`) никогда не импортирует модули действий: краткие описания и маркеры отключения извлекаются статически с помощью модуля `ast`. При диспетчеризации загружается ровно один модуль по вычисленному пути. Подробнее — в разделе [Концепция: Действия и оверлей](/apex/concepts/actions-and-overlay).
|
||||
|
||||
## Модель «capture-up» {#capture-up}
|
||||
|
||||
Репозитории узлов — это реестры, а не просто источники: хост сам записывает то, что он выполняет.
|
||||
Репозитории узлов служат реестрами состояния, а не просто источниками конфигурации: хост сам фиксирует то, что выполняет на практике.
|
||||
|
||||
1. Запланированный запуск `sync/repository` на хосте фиксирует рабочее дерево в ветку `sync/<node>` (включая указатель субмодуля commons), выполняет rebase на `origin/main` и отправляет с `--force-with-lease`.
|
||||
2. Запланированное CI-задание в репозитории узла вливает `sync/<node>` обратно в `main` под идентичностью бота.
|
||||
1. Регулярная задача `sync/repository` на хосте коммитит рабочее дерево в ветку `sync/<node>` (включая указатель субмодуля commons), делает rebase поверх `origin/main` и выполняет push с флагом `--force-with-lease`.
|
||||
2. Запланированное CI-задание в репозитории узла вливает ветку `sync/<node>` обратно в `main` от имени бота.
|
||||
|
||||
Этот цикл «capture-up» ([capture-up](/apex/glossary#capture-up)) означает, что дрейф на хосте превращается в коммит — видимый, поддающийся сравнению через diff и откату — вместо тихого расхождения. Синхронизация прерывается вместо фиксации, если субмодуль commons «грязный» или дерево `compositions/` отсутствует, поэтому сломанный checkout никогда не становится записанным состоянием.
|
||||
Благодаря циклу [«capture-up»](/apex/glossary#capture-up) любой дрейф конфигурации на хосте превращается в коммит: изменение наглядно отображается в diff и легко откатывается, исключая незаметные расхождения. Если субмодуль commons имеет незакоммиченные изменения («грязный») или каталог `compositions/` отсутствует, синхронизация прерывается без коммита, поэтому повреждённое состояние чекаута никогда не попадёт в историю.
|
||||
|
||||
> [!NOTE]
|
||||
> CI-задание слияния выполняет checkout репозитория *без* субмодулей: субмодуль commons использует SSH-URL, который стандартный CI-токен не может получить, и слияние в любом случае лишь перемещает указатель gitlink.
|
||||
> CI-задание слияния клонирует репозиторий *без* субмодулей: субмодуль commons подключён по SSH-URL, недоступному стандартному токену CI, а самому слиянию требуется лишь обновить указатель gitlink.
|
||||
|
||||
## Проектные ограничения {#constraints}
|
||||
|
||||
- **Только stdlib.** Движок работает на штатном Python 3 из Debian — без `pip`, без сторонних импортов. Опциональный каталог `vendor/` существует для закреплённого чистого Python-кода, но поставляется пустым.
|
||||
- **Composes, написанные вручную.** Нет слоя манифестов или генерации кода; compose-файлы являются источником истины.
|
||||
- **Свидетельство важнее соглашения.** Всё, что может быть обеспечено принудительно, обеспечивается действием (линт якорного IP, откат crontab, самовосстановление блоков-маркеров), а не документацией.
|
||||
- **Только стандартная библиотека (stdlib).** Движок работает на штатном Python 3 из Debian — без `pip` и сторонних зависимостей. Каталог `vendor/` зарезервирован для зафиксированного чистого Python-кода, но по умолчанию поставляется пустым.
|
||||
- **Ручные compose-файлы.** Никаких промежуточных манифестов или кодогенерации: файлы compose служат единственным источником истины.
|
||||
- **Автоматическая проверка вместо соглашений.** Всё, что можно проверить или применить автоматически, выполняется действием (проверка якорных IP, откат crontab, самовосстановление блоков-маркеров), а не остаётся на совести документации.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
title: Композиции и общее ядро
|
||||
description: Ядровая композиция на основе include, профили, сети и наслоение окружения.
|
||||
title: Композиции и общее ядро
|
||||
description: Ядровая композиция на основе include, профили, сети и наслоение окружения.
|
||||
weight: 15
|
||||
---
|
||||
|
||||
Среда выполнения узла объявляется как [композиции](/apex/glossary#compositions): по одному docker-compose-проекту на сервис в каталоге `compositions/<project>/`, плюс [ядровая композиция](/apex/glossary#core-composition) — граничные и инфраструктурные сервисы, необходимые каждому узлу. Ядро располагается в [«commons»](/apex/glossary#commons) и потребляется каждым узлом через тонкую обёртку, поэтому его определение написано один раз и версионируется вместе с фреймворком.
|
||||
Среда выполнения узла описывается в виде [композиций](/apex/glossary#compositions): по одному docker-compose-проекту на сервис в каталоге `compositions/<project>/`, плюс [ядровая композиция](/apex/glossary#core-composition) — граничные и инфраструктурные сервисы, необходимые каждому узлу. Ядро находится в [«commons»](/apex/glossary#commons) и подключается каждым узлом через тонкую обёртку. Благодаря этому его манифест пишется один раз и версионируется вместе с фреймворком.
|
||||
|
||||
## Обёртка include {#include}
|
||||
|
||||
Файл `compositions/apex/docker-compose.yml` каждого узла состоит из трёх строк:
|
||||
Файл `compositions/apex/docker-compose.yml` каждого узла состоит всего из трёх строк:
|
||||
|
||||
```yaml {filename="compositions/apex/docker-compose.yml"}
|
||||
name: apex
|
||||
@@ -16,9 +16,9 @@ include:
|
||||
- ../../commons/compositions/apex/docker-compose.yml
|
||||
```
|
||||
|
||||
Общий файл полностью параметризован через `${APEX_*}` и не содержит относительных хостовых путей — каждое монтирование разрешается через переменные, которые [движок](/apex/glossary#engine) экспортирует при вызове, поэтому один и тот же файл корректен на каждом узле.
|
||||
Общий файл полностью параметризован переменными `${APEX_*}` и не содержит относительных путей хоста: все точки монтирования разрешаются через переменные, экспортируемые [движком](/apex/glossary#engine) при вызове, поэтому один и тот же файл корректно работает на каждом узле.
|
||||
|
||||
Структурное исключение — узел, которому действительно нужно изменить ядровой сервис, например опубликовать дополнительный хостовый порт, — наслаивает переопределение через форму include со списком путей вместо редактирования общего файла:
|
||||
Если узлу требуется изменить ядровой сервис (например, опубликовать дополнительный порт хоста), переопределение накладывается через список путей в директиве `include:` без редактирования общего файла:
|
||||
|
||||
```yaml {filename="compositions/apex/docker-compose.yml"}
|
||||
name: apex
|
||||
@@ -29,54 +29,54 @@ include:
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Директива `include:` требует Docker Compose v2.20 или новее.
|
||||
> Директива `include:` требует Docker Compose версии v2.20 или новее.
|
||||
|
||||
## Ядровые сервисы и профили {#core-services}
|
||||
|
||||
Ядровая композиция определяет общий набор сервисов флота. Всегда включённые сервисы:
|
||||
|
||||
- **traefik** — граничный обратный прокси и якорь сети `direct` (всегда `.10` в подсети узла). Он завершает TLS через DNS-челлендж, обнаруживает бэкенды через docker-метки и выставляет свою панель управления на FQDN узла за списком разрешённых IP [анклава](/apex/glossary#enclave).
|
||||
- **socket-proxy** — отфильтрованный прокси для docker-сокета; traefik читает состояние контейнеров через него и никогда не обращается к сокету напрямую.
|
||||
- **traefik** — граничный обратный прокси и якорь сети `direct` (всегда занимает `.10` в подсети узла). Он терминирует TLS через DNS-челлендж, обнаруживает бэкенды по docker-меткам и открывает панель управления на FQDN узла, ограничивая доступ списком разрешённых IP-адресов [анклава](/apex/glossary#enclave).
|
||||
- **socket-proxy** — отфильтрованный прокси для docker-сокета: traefik получает состояние контейнеров через него и никогда не обращается к сокету напрямую.
|
||||
|
||||
Опциональные сервисы, управляемые через compose-профили, которые узел включает в `COMPOSE_PROFILES` (задаётся в `apex.env`):
|
||||
Опциональные сервисы управляются compose-профилями, которые узел активирует в переменной `COMPOSE_PROFILES` (задаётся в `apex.env`):
|
||||
|
||||
| Профиль | Сервис | Роль |
|
||||
|---|---|---|
|
||||
| `xray` | xray | Прокси-край с host-network для анклава. |
|
||||
| `adguard` | adguard | DNS-фильтрация с использованием сетевого пространства имён traefik; маршрутизируется по `/adguard` на FQDN узла. |
|
||||
| `alloy` | alloy | Понодовый агент наблюдаемости (Grafana Alloy). Собирает метрики хоста и контейнеров, а также логи journal и контейнеров и **проталкивает** их исходящим потоком в центральные Prometheus и Loki (см. [Концепция: Модель безопасности](/apex/concepts/security-model#observability)). Его `hostname` контейнера установлен в `APEX_NODE_HOST`, и это значение проставляет метки `node` и `instance` на каждой метрике и потоке логов. |
|
||||
| `manual` | resticontainer | Одноразовый исполнитель резервного копирования, управляемый метками (`ghcr.io/deytenit/resticontainer`) — никогда не запускается через `up`; вызывается действием `backup/run`, которое обнаруживает намерение резервного копирования для каждого сервиса из compose-меток `restic.*`. |
|
||||
| `xray` | xray | Прокси-шлюз с режимом host-network для анклава. |
|
||||
| `adguard` | adguard | DNS-фильтрация в сетевом пространстве имён traefik; маршрутизируется по пути `/adguard` на FQDN узла. |
|
||||
| `alloy` | alloy | Агент наблюдаемости на каждом узле (Grafana Alloy). Собирает метрики хоста и контейнеров, а также логи systemd journal и контейнеров, после чего **проталкивает** их исходящим потоком в центральные Prometheus и Loki (см. [Концепция: Модель безопасности](/apex/concepts/security-model#observability)). Имя хоста (`hostname`) контейнера совпадает с `APEX_NODE_HOST`, благодаря чему метки `node` и `instance` автоматически проставляются на всех метриках и потоках логов. |
|
||||
| `manual` | resticontainer | Одноразовый контейнер резервного копирования на основе меток (`ghcr.io/deytenit/resticontainer`). Никогда не запускается командой `up`; вызывается действием `backup/run`, которое определяет необходимость бэкапа каждого сервиса по compose-меткам `restic.*`. |
|
||||
|
||||
## Сети {#networks}
|
||||
|
||||
Ядровая композиция владеет тремя сетями узла; сервисные композиции ссылаются на них по ключу с `external: true`.
|
||||
Ядровая композиция создаёт три сети узла, а сервисные композиции подключаются к ним через директиву `external: true`:
|
||||
|
||||
| Сеть | Тип | Назначение |
|
||||
|---|---|---|
|
||||
| `direct` | bridge, подсеть = `APEX_SUBNET` | Сеть со статическими [якорными IP](/apex/glossary#anchor-ip); якорь каждого проекта удерживает здесь фиксированный адрес (см. [Концепция: Схема адресации флота](/apex/concepts/addressing)). |
|
||||
| `enclave` | bridge, `internal: true` | Бэкенд-сеть обратного прокси; traefik достигает помеченных сервисов здесь. |
|
||||
| `socket` | bridge, `internal: true` | Несёт только трафик traefik ↔ socket-proxy. |
|
||||
| `direct` | bridge, подсеть = `APEX_SUBNET` | Сеть со статическими [якорными IP](/apex/glossary#anchor-ip); якорь каждого проекта удерживает здесь фиксированный адрес (см. [Концепция: Схема адресации флота](/apex/concepts/addressing)). |
|
||||
| `enclave` | bridge, `internal: true` | Бэкенд-сеть обратного прокси: через неё traefik направляет трафик к целевым сервисам. |
|
||||
| `socket` | bridge, `internal: true` | Изолирует трафик между traefik и socket-proxy. |
|
||||
|
||||
Внутри проекта соседние контейнеры обычно присоединяются к сетевому пространству имён якоря (`network_mode: service:<anchor>`) вместо самостоятельного подключения к сетям.
|
||||
Внутри проекта зависимые контейнеры обычно используют сетевое пространство имён якоря (`network_mode: service:<anchor>`) вместо прямого подключения к сетям.
|
||||
|
||||
## Наслоение окружения {#env-layering}
|
||||
|
||||
Значения достигают композиции из трёх слоёв в порядке возрастания приоритета:
|
||||
Переменные попадают в композицию из трёх слоёв (в порядке возрастания приоритета):
|
||||
|
||||
1. **`.env`** — незакоммиченные секреты, символьная ссылка в хранилище уровня 1 (учётные данные `APEX_RESTIC_*`, токены `APEX_TRAEFIK_*`, блок UID/GID).
|
||||
2. **`apex.env`** — закоммиченные несекретные скалярные значения (`APEX_TRAEFIK_DIRECT_IP`, `APEX_RESTIC_REPOSITORY`, `COMPOSE_PROFILES`, опциональный `APEX_RESTIC_COMPRESSION`).
|
||||
3. **Экспорты движка** — идентичность `APEX_*` и пути [уровней](/apex/glossary#tier) из `ctx.vars()`, внедряемые в окружение процесса действиями оркестрации и потому выигрывающие у обоих файлов.
|
||||
1. **`.env`** — незакоммиченные секреты, символьная ссылка на хранилище уровня 1 (учётные данные `APEX_RESTIC_*`, токены `APEX_TRAEFIK_*`, блок UID/GID).
|
||||
2. **`apex.env`** — закоммиченные несекретные параметры (`APEX_TRAEFIK_DIRECT_IP`, `APEX_RESTIC_REPOSITORY`, `COMPOSE_PROFILES`, опциональный `APEX_RESTIC_COMPRESSION`).
|
||||
3. **Экспорт движка** — переменные идентичности `APEX_*` и пути [уровней](/apex/glossary#tier) из `ctx.vars()`. Они внедряются в окружение процесса действиями оркестрации и потому имеют наивысший приоритет над обоими файлами.
|
||||
|
||||
Критические переменные несут маркеры `?error` в общем файле, поэтому отсутствующий секрет громко проваливает загрузку конфигурации композиции вместо запуска неправильно настроенного контейнера.
|
||||
Критические переменные снабжены суффиксом `?error` в общем файле: если секрет не задан, загрузка конфигурации композиции завершается явной ошибкой, предотвращая запуск некорректно настроенного контейнера.
|
||||
|
||||
## Оркестрация {#orchestration}
|
||||
|
||||
`apex compose <action>` управляет каждой композицией на узле:
|
||||
Команда `apex compose <action>` управляет всеми композициями на узле:
|
||||
|
||||
- **Порядок** — сначала ядровой проект (он владеет сетями), затем каждая сервисная композиция в алфавитном порядке; `down` разворачивает последовательность.
|
||||
- **Env-файлы** — для каждого проекта `.env` и `apex.env` передаются как `--env-file`, если они присутствуют.
|
||||
- **`up`** подразумевает `-d`; любые дополнительные аргументы передаются в `docker compose`.
|
||||
- **`--dry-run`** печатает каждую команду с её рабочим каталогом вместо выполнения — принимается в любом месте командной строки.
|
||||
- **Порядок:** сначала ядровой проект (он владеет сетями), затем остальные сервисные композиции по алфавиту; команда `down` выполняет действия в обратном порядке.
|
||||
- **Env-файлы:** для каждого проекта существующие файлы `.env` и `apex.env` передаются через флаг `--env-file`.
|
||||
- **Команда `up`:** по умолчанию подразумевает флаг `-d`; любые дополнительные аргументы передаются напрямую в `docker compose`.
|
||||
- **Флаг `--dry-run`:** выводит каждую команду с её рабочим каталогом без выполнения; поддерживается в любом месте командной строки.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,66 +1,66 @@
|
||||
---
|
||||
title: Контекст выполнения
|
||||
description: ctx — единый шов между действием и хостом.
|
||||
description: ctx — единый интерфейс взаимодействия между действием и хостом.
|
||||
weight: 14
|
||||
---
|
||||
|
||||
Каждое [действие](/apex/glossary#action) получает объект `ctx` в качестве первого аргумента. Это единственный санкционированный способ взаимодействовать с хостом: логирование, выполнение процессов, рендеринг шаблонов, изменение файлов, управление фаерволом и сервисами, а также уведомления — всё проходит через него. Поэтому действия остаются декларативными, а поведение, которое должно быть согласованным по всему флоту (формат логов, дисциплина резервного копирования, полезная нагрузка уведомлений), находится в одном месте — [движке](/apex/glossary#engine).
|
||||
Каждое [действие](/apex/glossary#action) получает объект `ctx` первым аргументом. Это единственный штатный способ взаимодействия с хостом: логирование, запуск процессов, рендеринг шаблонов, правка файлов, настройка межсетевого экрана и сервисов, а также отправка уведомлений — всё выполняется через него. Благодаря этому действия остаются компактными и декларативными, а общие для всего флота правила (формат логов, регламент резервного копирования, структура уведомлений) сосредоточены в одном месте — [движке](/apex/glossary#engine).
|
||||
|
||||
## Члены {#members}
|
||||
|
||||
| Член | Назначение |
|
||||
|---|---|
|
||||
| `ctx.log` | Логирование с префиксом: `[<node>] [<action>] [LEVEL] message`; ошибки идут в stderr, всё остальное — в stdout. |
|
||||
| `ctx.sys` | Выполнение процессов (`run`, `sudo`, `ok`), помощники для сервисов systemd и идемпотентная установка пакетов. |
|
||||
| `ctx.tpl` | Рендеринг шаблонов над опорой `configs/` с помощью `string.Template.safe_substitute`. |
|
||||
| `ctx.host` | Идемпотентное изменение хоста: резервные копии с меткой времени, атомарная запись файлов, инъекция блоков-маркеров, установка crontab с откатом. |
|
||||
| `ctx.ufw` | Применение фаервола хоста и контейнеров (см. [Концепция: Модель безопасности](/apex/concepts/security-model)). |
|
||||
| `ctx.crowdsec` | Установка CrowdSec, развёртывание конфигурации и управление коллекциями. |
|
||||
| `ctx.systemd` | Рендеринг и включение systemd-юнитов из `configs/systemd/`. |
|
||||
| `ctx.notify` | Уведомления Telegram; каждый метод возвращает булево значение, чтобы действия могли ставить код выхода в зависимость от доставки. |
|
||||
| `ctx.paths` | Разрешённые пути репозитория: `repo_root`, `commons`, `proprietaries`, `configs`, `compositions`, `core` и свойства уровней. |
|
||||
| `ctx.node` | Разрешённая запись [идентичности узла](/apex/glossary#node-identity) (`name`, `subnet`, `fqdn`, `host`); `None` в автономном чекауте commons. |
|
||||
| `ctx.commons` | Доступен только в переопределяющем проприетарном действии: `ctx.commons.run(args)` делегирует затенённой реализации commons. |
|
||||
| `ctx.log` | Логирование с префиксом: `[<node>] [<action>] [LEVEL] message`; ошибки пишутся в stderr, остальные сообщения — в stdout. |
|
||||
| `ctx.sys` | Выполнение процессов (`run`, `sudo`, `ok`), вспомогательные функции для сервисов systemd и идемпотентная установка пакетов. |
|
||||
| `ctx.tpl` | Рендеринг шаблонов из каталога `configs/` с помощью `string.Template.safe_substitute`. |
|
||||
| `ctx.host` | Идемпотентные изменения на хосте: резервные копии с временными метками, атомарная запись файлов, внедрение блоков-маркеров, установка crontab с откатом. |
|
||||
| `ctx.ufw` | Управление правилами межсетевого экрана хоста и контейнеров (см. [Концепция: Модель безопасности](/apex/concepts/security-model)). |
|
||||
| `ctx.crowdsec` | Установка CrowdSec, развёртывание конфигураций и управление коллекциями. |
|
||||
| `ctx.systemd` | Рендеринг и активация systemd-юнитов из `configs/systemd/`. |
|
||||
| `ctx.notify` | Отправка уведомлений в Telegram; методы возвращают булево значение, позволяя действиям учитывать факт доставки в коде завершения. |
|
||||
| `ctx.paths` | Разрешённые пути репозитория: `repo_root`, `commons`, `proprietaries`, `configs`, `compositions`, `core` и свойства уровней хранилища. |
|
||||
| `ctx.node` | Разрешённая запись [идентичности узла](/apex/glossary#node-identity) (`name`, `subnet`, `fqdn`, `host`); принимает значение `None` в автономном чекауте commons. |
|
||||
| `ctx.commons` | Доступен только в переопределяющих проприетарных действиях: вызов `ctx.commons.run(args)` делегирует управление затенённой реализации commons. |
|
||||
|
||||
Сигнатуры уровня методов перечислены в [Справочник: API движка](/apex/reference/engine-api).
|
||||
Сигнатуры методов подробно описаны в разделе [Справочник: API движка](/apex/reference/engine-api).
|
||||
|
||||
## Семантика шаблонов {#templates}
|
||||
|
||||
`ctx.tpl` рендерит файлы из `configs/` с помощью питоновского `string.Template.safe_substitute`:
|
||||
Модуль `ctx.tpl` рендерит файлы из `configs/` с помощью `string.Template.safe_substitute`:
|
||||
|
||||
- Известные ссылки `$VAR` и `${VAR}` заменяются.
|
||||
- **Неизвестные ссылки остаются буквальными.** Опечатка в шаблоне сохраняется в отрендеренном выводе, где она видна, вместо того чтобы молча свернуться в пустую строку.
|
||||
- Подстановка команд shell (`$(...)`) проходит нетронутой, что сохраняет целостность записей crontab со встроенным `$(cat ...)`.
|
||||
- Известные переменные `$VAR` и `${VAR}` подставляются.
|
||||
- **Неизвестные переменные сохраняются буквально:** опечатка в имени переменной останется в готовом файле на виду, а не превратится незаметно в пустую строку.
|
||||
- Подстановки команд shell (`$(cmd)`) не затрагиваются, что сохраняет целостность строк crontab со встроенными командами вида `$(cat ...)`.
|
||||
|
||||
Набор подстановки — это `ctx.vars()`: полное окружение процесса плюс предоставляемые движком значения `APEX_*`, приведённые ниже. Циклы и условия никогда не живут в шаблонах; их место — в действии.
|
||||
Подстановка выполняется по словарю `ctx.vars()`: это полное окружение процесса плюс переменные `APEX_*`, предоставляемые движком (перечислены ниже). В шаблонах нет циклов и условий — вся логика ветвления находится в действиях.
|
||||
|
||||
## Экспорт переменных {#vars}
|
||||
|
||||
`ctx.vars()` возвращает окружение, используемое для каждого рендеринга шаблона, а его подмножество `APEX_*` экспортируется в `docker compose` действиями оркестрации. На узле оно содержит:
|
||||
Функция `ctx.vars()` возвращает словарь окружения, используемый при каждом рендеринге шаблонов. Подмножество переменных `APEX_*` также передаётся в `docker compose` действиями оркестрации. На узле оно включает:
|
||||
|
||||
| Переменная | Значение |
|
||||
|---|---|
|
||||
| `APEX_COMMONS` | Абсолютный путь директории commons (лаунчер находится в `$APEX_COMMONS/apex`). |
|
||||
| `APEX_REPO_ROOT` | Абсолютный путь корня репозитория узла. |
|
||||
| `APEX_NODE_HOST`, `APEX_NODE_FQDN`, `APEX_SUBNET` | Разрешённые поля идентичности. |
|
||||
| `APEX_TIER1`…`APEX_TIER3` | Директории данных базового проекта по каждому [уровню](/apex/glossary#tier). |
|
||||
| `APEX_COMMONS` | Абсолютный путь к каталогу commons (лаунчер расположен по пути `$APEX_COMMONS/apex`). |
|
||||
| `APEX_REPO_ROOT` | Абсолютный путь к корню репозитория узла. |
|
||||
| `APEX_NODE_HOST`, `APEX_NODE_FQDN`, `APEX_SUBNET` | Разрешённые поля идентичности узла. |
|
||||
| `APEX_TIER1`…`APEX_TIER3` | Каталоги данных базового проекта для каждого [уровня](/apex/glossary#tier). |
|
||||
| `APEX_TIER1_SHARED`…`APEX_TIER3_SHARED` | Общие области внутри каждого уровня. |
|
||||
| `APEX_TIER_ROOT1`…`APEX_TIER_ROOT3` | Корни хранилищ уровней (ссылки уровня узла). |
|
||||
| `APEX_TIER_ROOT1`…`APEX_TIER_ROOT3` | Корневые каталоги хранилищ уровней (ссылки уровня узла). |
|
||||
|
||||
Полная матрица источников/потребителей приведена в [Справочник: Переменные окружения](/apex/reference/environment).
|
||||
Полная матрица источников и потребителей переменных приведена в разделе [Справочник: Переменные окружения](/apex/reference/environment).
|
||||
|
||||
## Дисциплина изменения хоста {#host}
|
||||
|
||||
`ctx.host` кодирует правила записи фреймворка:
|
||||
Объект `ctx.host` реализует строгие правила записи:
|
||||
|
||||
- **Атомарная запись.** `write_file` рендерит во временный файл (режим `0644`, соответствующий тому, что создал бы редирект root-шелла) и устанавливает его через `mv` или `sudo cp`; перезапись существующего файла сохраняет режим этого файла.
|
||||
- **Резервные копии с меткой времени.** По запросу предыдущий файл сохраняется рядом с целевым как `<name>.<epoch>.bak` перед записью.
|
||||
- **Инъекция блоков-маркеров.** Управляемые блоки внутри системных файлов (например, `/etc/ufw/before.rules`) ограничиваются начальными/конечными комментариями-маркерами. Инъекция удаляет каждый предыдущий управляемый блок — включая блоки, оставленные предшествующим инструментарием, — перед вставкой свежего после его якорной строки, поэтому повторные запуски никогда не накапливаются.
|
||||
- **Откат crontab.** `install_crontab` делает снимок текущего crontab, отказывается от пустого рендера (код выхода `65`), показывает предпросмотр результата и восстанавливает снимок, если установка не удалась.
|
||||
- **Атомарная запись:** метод `write_file` сначала формирует временный файл (права `0644`, аналогичные перенаправлению вывода из-под root) и перемещает его на место через `mv` или `sudo cp`. При перезаписи существующего файла его исходные права сохраняются.
|
||||
- **Резервные копии с временной меткой:** по запросу перед записью создаётся копия старого файла рядом с целевым: `<name>.<epoch>.bak`.
|
||||
- **Внедрение блоков-маркеров:** изменения внутри системных файлов (например, `/etc/ufw/before.rules`) обрамляются комментариями-маркерами начала и конца. Перед вставкой нового блока после якорной строки движок удаляет все предыдущие управляемые блоки (включая оставшиеся от старого инструментария), предотвращая накопление дубликатов при повторных запусках.
|
||||
- **Откат crontab:** метод `install_crontab` делает снимок текущего crontab, отклоняет пустой результат рендеринга (код выхода `65`), показывает diff изменений и автоматически восстанавливает снимок при ошибке установки.
|
||||
|
||||
## Уведомления {#notifications}
|
||||
|
||||
`ctx.notify` отправляет сообщения Telegram через стандартную библиотеку (`urllib`). Полезная нагрузка несёт имя инстанса — публичный хост узла `APEX_NODE_HOST` — статус (`firing`/`resolved`), уровень эскалации и сообщение, усечённое до 200 символов. Каждый метод возвращает `True` только тогда, когда отправка удалась; автоматические действия трактуют неудачную отправку своего финального сообщения о статусе как сбой (код выхода `1`), поэтому сломанный канал уведомлений замечается, а не молча терпится.
|
||||
Модуль `ctx.notify` отправляет сообщения в Telegram средствами стандартной библиотеки (`urllib`). Полезная нагрузка содержит имя хоста (`APEX_NODE_HOST`), статус (`firing`/`resolved`), уровень важности и текст сообщения, обрезанный до 200 символов. Методы возвращают `True` только при успешной доставке; автоматические действия трактуют сбой отправки итогового статуса как критическую ошибку (код завершения `1`), поэтому проблемы со связью сразу становятся заметны.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
---
|
||||
title: "Разрешение идентичности узла"
|
||||
description: "Как узел узнаёт, кто он: ключи идентичности node.env и запасной вариант FQDN."
|
||||
description: "Как узел определяет собственную идентичность: параметры node.env и резервный FQDN."
|
||||
weight: 12
|
||||
---
|
||||
|
||||
Каждая операция apex выполняется в контексте ровно одного [узла](/apex/glossary#node). [Движок](/apex/glossary#engine) выводит эту [идентичность узла](/apex/glossary#node-identity) — имя узла, подсеть, FQDN и публичный хост — из файла `node.env` в корне репозитория, используя имя хоста ОС только как запасной вариант для FQDN. Фреймворк не предполагает ни домена, ни кластеризации: узел объявляет, кто он, явно. Этот шаг разрешения приходит на смену механизму `root_require_node` bash-фреймворка commons «root», который заменяет apex.
|
||||
Каждая операция apex выполняется в контексте строго одного [узла](/apex/glossary#node). [Движок](/apex/glossary#engine) извлекает [идентичность узла](/apex/glossary#node-identity) — имя, подсеть, FQDN и публичный адрес — из файла `node.env` в корне репозитория. Системное имя хоста используется исключительно как резервный вариант для FQDN. Фреймворк не делает предположений о домене или наличии кластера: узел заявляет о себе явно. Этот механизм пришёл на смену функции `root_require_node` bash-фреймворка «root» commons, развитием которого стал apex.
|
||||
|
||||
## Источники идентичности {#sources}
|
||||
|
||||
### node.env
|
||||
|
||||
`node.env` — это обычный файл вида `KEY=value` (пригодный для `source` в shell), расположенный в корне репозитория. При разборе комментарии и пустые строки игнорируются, а окружающие значения одинарные или двойные кавычки удаляются.
|
||||
Файл `node.env` — это простой конфигурационный файл формата `KEY=value` (совместимый с командой `source` в shell), расположенный в корне репозитория. При разборе комментарии и пустые строки игнорируются, а одинарные или двойные кавычки вокруг значений отбрасываются.
|
||||
|
||||
Он объявляет идентичность явно через следующие ключи:
|
||||
Идентичность объявляется явно с помощью следующих ключей:
|
||||
|
||||
- `APEX_NODE_FQDN` — реальный FQDN узла, используемый для маршрутизации traefik `Host()`. Если не задан, движок откатывается к имени хоста ОС (и выводит предупреждение).
|
||||
- `APEX_NODE_HOST` — публичное имя хоста узла, используемое для `--host` резервного копирования и `instance` для наблюдаемости/уведомлений. По умолчанию равно `APEX_NODE_FQDN`.
|
||||
- `APEX_SUBNET` — `/24` узла для сети `direct`. **Обязателен**; другого источника для него нет (см. [Концепция: Схема адресации флота](/apex/concepts/addressing)).
|
||||
- `APEX_NODE_FQDN` — канонический FQDN узла для правила `Host()` в traefik. Если ключ не задан, движок берёт имя хоста из ОС и выводит предупреждение.
|
||||
- `APEX_NODE_HOST` — публичное имя хоста узла, используемое для флага `--host` при резервном копировании и метки `instance` в системе мониторинга и уведомлений. По умолчанию совпадает с `APEX_NODE_FQDN`.
|
||||
- `APEX_SUBNET` — подсеть `/24` узла для сети `direct`. Параметр **обязателен**: альтернативных источников для него нет (см. [Концепция: Схема адресации флота](/apex/concepts/addressing)).
|
||||
|
||||
```ini {filename="node.env"}
|
||||
APEX_NODE_FQDN=node1.example.com
|
||||
@@ -26,49 +26,49 @@ APEX_SUBNET=198.18.16.0/24
|
||||
|
||||
### Имя хоста ОС
|
||||
|
||||
Имя хоста ОС используется только как запасной вариант для `APEX_NODE_FQDN`. Когда в `node.env` не задан `APEX_NODE_FQDN`, движок использует имя хоста и выводит предупреждение. Оно никогда не разбирается для получения имени узла или любой другой координаты.
|
||||
Имя хоста операционной системы используется только как резервный вариант для `APEX_NODE_FQDN`. Если в `node.env` значение `APEX_NODE_FQDN` не указано, движок берёт имя хоста ОС и выводит предупреждение. Это значение никогда не парсится для вычисления короткого имени узла или других параметров.
|
||||
|
||||
## Порядок разрешения {#resolution}
|
||||
|
||||
{{% steps %}}
|
||||
|
||||
### Разрешите FQDN
|
||||
### Определение FQDN
|
||||
|
||||
`fqdn` — это `APEX_NODE_FQDN` из `node.env` или имя хоста ОС, когда этот ключ не задан. Если ни то ни другое не даёт значения, разрешение завершается неудачей (см. [Режимы сбоев](#failures)).
|
||||
Параметр `fqdn` принимает значение `APEX_NODE_FQDN` из `node.env` либо системное имя хоста ОС, если ключ не задан. Если оба источника пусты, разрешение завершается фатальной ошибкой (см. [Режимы сбоев](#failures)).
|
||||
|
||||
### Разрешите публичный хост
|
||||
### Определение публичного хоста
|
||||
|
||||
`host` — это `APEX_NODE_HOST` из `node.env` или `fqdn`, когда этот ключ не задан.
|
||||
Параметр `host` берётся из `APEX_NODE_HOST` в `node.env` либо приравнивается к вычисленному `fqdn`.
|
||||
|
||||
### Выведите короткое имя
|
||||
### Вычисление короткого имени
|
||||
|
||||
`name` — это **первая метка** `host` (`host.split(".")[0]`) — например, `node1` для `node1.example.com`. Оно используется для логов, ветки `sync/<node>` и сообщений.
|
||||
Значение `name` — это **первая часть** имени `host` (`host.split(".")[0]`), например `node1` для `node1.example.com`. Оно используется в логах, имени ветки `sync/<node>` и уведомлениях.
|
||||
|
||||
### Прочитайте подсеть
|
||||
### Получение подсети
|
||||
|
||||
`subnet` всегда читается из `APEX_SUBNET` в `node.env`. Она обязательна.
|
||||
Значение `subnet` всегда читается из переменной `APEX_SUBNET` файла `node.env`. Этот параметр строго обязателен.
|
||||
|
||||
{{% /steps %}}
|
||||
|
||||
Результат — запись `Node` с четырьмя полями: `name`, `subnet`, `fqdn` и `host`.
|
||||
В результате формируется структура `Node` с четырьмя полями: `name`, `subnet`, `fqdn` и `host`.
|
||||
|
||||
## Предупреждения {#warnings}
|
||||
|
||||
Разрешение идентичности выводит ровно одно предупреждение; оно сигнализирует об исправимой несогласованности, которую следует устранить.
|
||||
При разрешении идентичности может выводиться одно штатное предупреждение: оно указывает на неполную конфигурацию, которую стоит исправить.
|
||||
|
||||
| Условие | Сообщение предупреждения |
|
||||
|---|---|
|
||||
| `APEX_NODE_FQDN` не задан в `node.env` (вместо него используется имя хоста) | `APEX_NODE_FQDN not set in node.env; using hostname '<fqdn>'.` |
|
||||
| `APEX_NODE_FQDN` не задан в `node.env` (используется имя хоста ОС) | `APEX_NODE_FQDN not set in node.env; using hostname '<fqdn>'.` |
|
||||
|
||||
## Режимы сбоев {#failures}
|
||||
|
||||
Разрешение отказывается угадывать. Два условия фатальны, и оба завершаются с кодом `66` — соглашение движка о коде выхода для отсутствующего ввода:
|
||||
Движок никогда не пытается угадывать параметры наугад. Две ситуации считаются критическими и приводят к завершению с кодом `66` (соглашение движка о коде выхода при отсутствии обязательного ввода):
|
||||
|
||||
- **Нет FQDN вовсе.** `APEX_NODE_FQDN` не задан в `node.env` *и* имя хоста ОС пусто. Движок пишет `identity: APEX_NODE_FQDN missing from node.env and no hostname` в stderr и завершается.
|
||||
- **Нет подсети.** `APEX_SUBNET` отсутствует в `node.env` (в том числе когда сам файл отсутствует). Движок пишет `identity: APEX_SUBNET missing from node.env` в stderr и завершается.
|
||||
- **FQDN не определён:** ключ `APEX_NODE_FQDN` отсутствует в `node.env`, *а также* не задано имя хоста в ОС. Движок выводит в stderr сообщение `identity: APEX_NODE_FQDN missing from node.env and no hostname` и завершает работу.
|
||||
- **Подсеть не задана:** ключ `APEX_SUBNET` отсутствует в `node.env` (либо сам файл отсутствует). Движок выводит в stderr сообщение `identity: APEX_SUBNET missing from node.env` и завершает работу.
|
||||
|
||||
> [!WARNING]
|
||||
> Действительный `node.env`, несущий `APEX_SUBNET`, обязателен на каждом узле. `APEX_NODE_FQDN` можно опустить только тогда, когда имя хоста ОС предоставляет пригодный FQDN.
|
||||
> Корректный файл `node.env` с заданным параметром `APEX_SUBNET` обязателен на каждом узле. Параметр `APEX_NODE_FQDN` допустимо опускать только в том случае, если системное имя хоста ОС возвращает валидный FQDN.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
---
|
||||
title: Модель безопасности
|
||||
description: Изоляция привилегий, управление межсетевым экраном, обнаружение вторжений и обработка секретов.
|
||||
description: Изоляция привилегий, управление межсетевым экраном, обнаружение вторжений и защита секретов.
|
||||
weight: 18
|
||||
---
|
||||
|
||||
apex укрепляет [узел](/apex/glossary#node) в независимых слоях: изоляция привилегий на уровне файловой системы, управляемый межсетевой экран хоста, обнаружение вторжений, ограничение на уровне контейнеров и строгая граница секретов. Каждый слой применяется [действием](/apex/glossary#action) и потому воспроизводим и повторно запускаем.
|
||||
Фреймворк apex защищает [узел](/apex/glossary#node) на нескольких независимых уровнях: изоляция привилегий в файловой системе, управляемый межсетевой экран хоста, обнаружение вторжений, разграничение прав в контейнерах и строгая изоляция секретов. Каждый уровень настраивается отдельным [действием](/apex/glossary#action), что гарантирует воспроизводимость и идемпотентность конфигурации.
|
||||
|
||||
## Изоляция привилегий {#privilege}
|
||||
|
||||
Каждый проект выполняется под собственным заблокированным системным пользователем `noroot-<project>`; общее хранилище доступно на запись группе с установленным sticky-битом, поэтому проекты могут обмениваться файлами, не имея возможности удалять файлы друг друга. Полный механизм — пользователи, группы и матрица владения — описан в [Концепция: Многоуровневое хранилище](/apex/concepts/tiered-storage).
|
||||
Каждый проект работает под изолированным системным пользователем `noroot-<project>` без права входа. Общее хранилище открыто на запись группе с установленным sticky-битом: проекты могут обмениваться файлами, но не могут удалять чужие данные. Подробное устройство механизма (пользователи, группы и матрица прав) описано в разделе [Концепция: Многоуровневое хранилище](/apex/concepts/tiered-storage).
|
||||
|
||||
## Межсетевой экран хоста {#firewall}
|
||||
|
||||
`apex configure/ufw` применяет межсетевой экран узла из опоры `configs/ufw/` в трёх частях:
|
||||
Команда `apex configure/ufw` настраивает межсетевой экран узла на основе каталога `configs/ufw/` по трём направлениям:
|
||||
|
||||
- **Правила хоста** (`configs/ufw/host/host.rules`) — простые команды `ufw`, по одной на строку, применяемые по порядку.
|
||||
- **Блоки правил уровня ядра** (`configs/ufw/host/before.rules`, `after.rules`) — внедряются в `/etc/ufw/before.rules` и `/etc/ufw/after.rules` как управляемые блоки-маркеры. Внедрение является **самовосстанавливающимся**: каждый предыдущий управляемый блок — включая блоки, оставленные инструментарием-предшественником, — удаляется перед вставкой свежего блока после якоря ufw `# End required lines`, а рядом сохраняется `.bak` целевого файла с меткой времени.
|
||||
- **Правила контейнеров** (`configs/ufw/docker/*.rules`) — применяются через `ufw-docker`, который транслирует их в правила, переживающие собственные манипуляции Docker с iptables. `configure/base` устанавливает вспомогательный инструмент в `~/.local/bin/ufw-docker`.
|
||||
- **Правила хоста** (`configs/ufw/host/host.rules`) — список команд `ufw` (по одной на строку), применяемых последовательно.
|
||||
- **Правила уровня ядра** (`configs/ufw/host/before.rules`, `after.rules`) — внедряются в `/etc/ufw/before.rules` и `/etc/ufw/after.rules` в виде управляемых блоков-маркеров. Процесс внедрения **самовосстанавливающийся**: любые ранее добавленные блоки (включая оставшиеся от предыдущих утилит) удаляются перед вставкой актуального блока после якорной строки `# End required lines`. Рядом с изменяемым файлом сохраняется резервная копия `.bak` с временной меткой.
|
||||
- **Правила для контейнеров** (`configs/ufw/docker/*.rules`) — применяются через утилиту `ufw-docker`, которая формирует правила iptables, устойчивые к перезапускам и манипуляциям самого Docker. Команда `configure/base` устанавливает этот инструмент в `~/.local/bin/ufw-docker`.
|
||||
|
||||
Отсутствующая конфигурация — это ошибка, а не тихий пропуск: отсутствующий каталог `docker/` или `host/` завершается с кодом `66`, если существует его аналог, а отсутствующий целевой файл `/etc/ufw` прерывает запуск. Действие завершается напоминанием, что `ufw` должен быть перезапущен, чтобы изменения вступили в силу, — и что SSH-порт должен быть разрешён в первую очередь.
|
||||
Отсутствующая конфигурация считается ошибкой, а не поводом для тихого пропуска: отсутствие каталогов `docker/` или `host/` (при наличии парного каталога) завершает работу с кодом `66`, а отсутствие целевого системного файла в `/etc/ufw` немедленно прерывает выполнение. В конце действие напоминает о необходимости перезапустить `ufw` для применения изменений и проверить доступность порта SSH.
|
||||
|
||||
## Обнаружение вторжений {#crowdsec}
|
||||
|
||||
`apex configure/crowdsec` устанавливает CrowdSec через его официальный установщик, когда `cscli` отсутствует, рендерит всё из `configs/crowdsec/` в `/etc/crowdsec/` (источники сбора, парсеры, конфигурация bouncer) и устанавливает коллекции `crowdsecurity/linux` и `crowdsecurity/traefik`. Когда конфигурации узла включают конфигурацию firewall-bouncer, устанавливается и соответствующий пакет bouncer, замыкая цикл от обнаружения к блокировке.
|
||||
Команда `apex configure/crowdsec` автоматически устанавливает CrowdSec через официальный инсталлятор (если утилита `cscli` отсутствует), генерирует конфигурацию из `configs/crowdsec/` в `/etc/crowdsec/` (источники логов, парсеры, параметры bouncer) и устанавливает коллекции `crowdsecurity/linux` и `crowdsecurity/traefik`. Если конфигурация узла предусматривает firewall-bouncer, соответствующий пакет bouncer устанавливается автоматически, связывая обнаружение атак с немедленной блокировкой на уровне сети.
|
||||
|
||||
## Ограничение на уровне контейнеров {#containers}
|
||||
|
||||
[Ядровая композиция](/apex/glossary#core-composition) кодирует укрепление на уровне контейнеров:
|
||||
[Ядровая композиция](/apex/glossary#core-composition) задаёт строгие ограничения для контейнеров:
|
||||
|
||||
- Граничный прокси никогда не обращается к Docker-сокету: он обнаруживает бэкенды через **отфильтрованный socket-proxy**, который выставляет состояние контейнеров/сервисов в режиме только для чтения в изолированной сети.
|
||||
- `enclave` и `socket` — сети с `internal: true` — без исходящего трафика, без выставления хостовых портов.
|
||||
- Ядровые сервисы устанавливают `no-new-privileges` и работают под noroot-идентичностью проекта везде, где это позволяет нагрузка; capabilities выдаются для каждого сервиса, а не глобально.
|
||||
- Граничный прокси изолирован от сокета Docker: он получает данные о сервисах через **отфильтрованный socket-proxy**, который отдаёт состояние контейнеров только для чтения в замкнутой сети.
|
||||
- Сети `enclave` и `socket` объявлены с параметром `internal: true`: у них нет прямого доступа в интернет и опубликованных портов хоста.
|
||||
- Ядровые сервисы запускаются с опцией `no-new-privileges` под noroot-пользователем везде, где позволяет рабочая нагрузка; Linux capabilities выдаются точечно каждому сервису, а не глобально.
|
||||
|
||||
## Наблюдаемость {#observability}
|
||||
|
||||
Наблюдаемость — это **исходящий push**, а не выставленный маршрут. Опциональный ядровой сервис `alloy` (Grafana Alloy, один агент на узел) собирает метрики хоста (эквивалент `node_exporter`), метрики контейнеров (cadvisor), systemd journal и логи docker-контейнеров и проталкивает их в **центральные** Prometheus и Loki, которые располагаются на выделенном узле — метрики через `prometheus.remote_write` в `APEX_PROM_PUSH_URL`, логи через `loki.write` в `APEX_LOKI_PUSH_URL`. Обе цели аутентифицируются через HTTP basic auth с использованием `APEX_OBS_BASIC_AUTH_USER` / `APEX_OBS_BASIC_AUTH_PASS`. Поскольку push является исходящим, Alloy подключается к сети `direct` (у которой есть исходящий трафик), а не к `internal`-сети `enclave`; узло-локального хранилища логов нет, как нет и маршрута `/loki`, который нужно было бы защищать.
|
||||
Система мониторинга построена по модели **исходящей отправки (push)**, а не входящих опросов. Опциональный сервис `alloy` (Grafana Alloy, один агент на узел) собирает метрики хоста (аналог `node_exporter`), метрики контейнеров (cadvisor), системный журнал systemd journal и логи docker-контейнеров, после чего передаёт их в **центральные** хранилища Prometheus и Loki на выделенном узле. Метрики отправляются через `prometheus.remote_write` на адрес `APEX_PROM_PUSH_URL`, логи — через `loki.write` на `APEX_LOKI_PUSH_URL`. Обе конечные точки защищены базовой аутентификацией HTTP basic auth (`APEX_OBS_BASIC_AUTH_USER` / `APEX_OBS_BASIC_AUTH_PASS`). Поскольку трафик отправляется наружу, Alloy подключён к сети `direct` с доступом в интернет, а не к закрытой сети `enclave`. На самих узлах логи не накапливаются, и открывать внешний доступ к эндпоинтам сервисов не требуется.
|
||||
|
||||
## Граница секретов {#secrets}
|
||||
## Изоляция секретов {#secrets}
|
||||
|
||||
- Секреты живут только в файлах `.env` для каждого проекта, которые являются **символьными ссылками в хранилище уровня 1** — за пределами рабочего дерева репозитория и покрытыми `.gitignore` (`**/.env`).
|
||||
- Каждый репозиторий узла документирует ожидаемые ключи в `SECRETS.md`; файл документирует имена, но никогда — значения.
|
||||
- Закоммиченная конфигурация (`apex.env`) ограничена несекретными скалярными значениями.
|
||||
- Общая композиция помечает критические секреты через `?error`, поэтому отсутствующее значение быстро проваливается на этапе загрузки конфигурации compose.
|
||||
- Коммиты «capture-up» безопасны по построению: игнорируемые ссылки `.env` и данные уровней никогда не попадают в git-индекс.
|
||||
- Секреты хранятся исключительно в файлах `.env` каждого проекта. Эти файлы представляют собой **символьные ссылки на хранилище уровня 1**, расположены вне рабочего дерева git и внесены в `.gitignore` (`**/.env`).
|
||||
- Для каждого узла перечень обязательных переменных задокументирован в файле `SECRETS.md`, причём указываются только имена ключей, но ни в коем случае не их значения.
|
||||
- Версионируемый файл `apex.env` содержит только несекретные параметры.
|
||||
- В ядровых compose-файлах критически важные секреты снабжены маркером `?error`: при отсутствии переменной запуск немедленно останавливается с ошибкой ещё на этапе разбора конфигурации.
|
||||
- Механизм [«capture-up»](/apex/glossary#capture-up) безопасен конструктивно: игнорируемые файлы `.env` и данные уровней хранилища никогда не попадают в git-индекс.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
---
|
||||
title: Многоуровневое хранилище и изоляция привилегий
|
||||
description: Структура ссылок @tier, общее хранилище и понодовые noroot-пользователи.
|
||||
description: Структура ссылок @tier, разделяемое хранилище и локальные noroot-пользователи для проектов.
|
||||
weight: 16
|
||||
---
|
||||
|
||||
Данные сервисов никогда не живут внутри репозитория [узла](/apex/glossary#node). Они живут в трёх [уровнях](/apex/glossary#tier) хранилища — раздельных расположениях, выбираемых для каждого хоста, — и достигают [композиций](/apex/glossary#compositions) через стандартизированную структуру символьных ссылок. Владение этими данными делегируется системным пользователям для каждого проекта, поэтому ни один сервисный контейнер не пишет от имени root.
|
||||
Данные сервисов никогда не хранятся внутри репозитория [узла](/apex/glossary#node). Они распределяются по трём [уровням](/apex/glossary#tier) хранилища — отдельным каталогам, настраиваемым индивидуально для каждого хоста, — и подключаются к [композициям](/apex/glossary#compositions) через стандартизированную структуру символьных ссылок. Права на эти каталоги делегируются системным пользователям проектов, благодаря чему ни один контейнер не ведёт запись от имени root.
|
||||
|
||||
## Структура ссылок уровней {#links}
|
||||
|
||||
`apex tiers/link <tier1> <tier2> <tier3>` строит два слоя ссылок:
|
||||
Команда `apex tiers/link <tier1> <tier2> <tier3>` формирует два уровня символьных ссылок:
|
||||
|
||||
- **На уровне узла:** `compositions/@tierN` → корень хранилища уровня. Именно их движок выставляет как `APEX_TIER_ROOT1..3`.
|
||||
- **Для каждого проекта:** для каждого `compositions/<project>/` действие создаёт `<tier-root>/<project>/` и связывает `compositions/<project>/@tierN` → `<tier-root>/<project>`. Внутри каждого уровня оно также поддерживает `shared/` с подкаталогом для каждого проекта и относительной ссылкой `shared` внутри каталога уровня каждого проекта, указывающей обратно на него.
|
||||
- **На уровне узла:** `compositions/@tierN` → корень хранилища соответствующего уровня. Именно эти пути движок предоставляет в переменных `APEX_TIER_ROOT1..3`.
|
||||
- **Для каждого проекта:** в каталоге `compositions/<project>/` создаётся путь `<tier-root>/<project>/`, на который указывает ссылка `compositions/<project>/@tierN`. Внутри каждого уровня поддерживается общий каталог `shared/` с поддиректорией для каждого проекта, а внутри каталога уровня проекта создаётся относительная ссылка `shared`, ведущая обратно в общую зону.
|
||||
|
||||
Каждый проект дополнительно получает `compositions/<project>/.env` → `<tier1-root>/<project>/.env` (создаётся пустым, если отсутствует) — файл незакоммиченных секретов, описанный в [Концепция: Композиции](/apex/concepts/compositions). Ссылки `@tierN` и файлы `.env` игнорируются git; репозиторий остаётся чистым источником.
|
||||
Каждый проект дополнительно получает символьную ссылку `compositions/<project>/.env` → `<tier1-root>/<project>/.env` (создаётся пустым, если файл отсутствует). В нём хранятся незакоммиченные секреты (см. [Концепция: Композиции и общее ядро](/apex/concepts/compositions)). Ссылки `@tierN` и файлы `.env` исключены из git через `.gitignore`, благодаря чему репозиторий остаётся чистым источником кода.
|
||||
|
||||
Если путь, который должен быть символьной ссылкой, существует как настоящий файл или каталог, действие предупреждает и запрашивает подтверждение перед его удалением; отказ (включая неинтерактивный EOF) пропускает эту ссылку и продолжает.
|
||||
Если по пути предполагаемой символьной ссылки уже существует обычный файл или каталог, действие предупреждает об этом и запрашивает подтверждение перед удалением. При отказе (включая неинтерактивный EOF) создание ссылки пропускается, а выполнение продолжается дальше.
|
||||
|
||||
## Пользователи и группы {#users}
|
||||
|
||||
`apex tiers/useradd` провизионирует по одному заблокированному системному пользователю на проект — `noroot-<project>` (имя санитизируется, собственная первичная группа) — плюс общефлотовую группу `noroot-shared`, к которой присоединяется каждый пользователь проекта. Затем оно внедряет разрешённые ID в `.env` проекта между комментариями-маркерами, заменяя любой предыдущий блок при повторных запусках:
|
||||
Команда `apex tiers/useradd` создаёт по одному заблокированному системному пользователю на проект — `noroot-<project>` (с санитизацией имени и собственной первичной группой), а также общефлотовую группу `noroot-shared`, в которую входят все пользователи проектов. Затем сгенерированные идентификаторы прописываются в файл `.env` соответствующего проекта между комментариями-маркерами, перезаписывая старый блок при повторных запусках:
|
||||
|
||||
```ini {filename="compositions/<project>/.env"}
|
||||
# START AUTO GENERATED APEX-NOROOT-USERS
|
||||
@@ -29,42 +29,42 @@ APEX_SHARED_GID=997
|
||||
# END AUTO GENERATED APEX-NOROOT-USERS
|
||||
```
|
||||
|
||||
Композиции потребляют их как `user: "${APEX_UID?error}:${APEX_GID?error}"` и `group_add: ["${APEX_SHARED_GID?error}"]`.
|
||||
Композиции подключают эти идентификаторы через переменные `user: "${APEX_UID?error}:${APEX_GID?error}"` и `group_add: ["${APEX_SHARED_GID?error}"]`.
|
||||
|
||||
## Матрица владения {#ownership}
|
||||
|
||||
`apex tiers/chown` обеспечивает владение по всем трём уровням. Без аргументов оно работает с корнями уровней (`APEX_TIER_ROOT1..3`); явные пути уровней могут переопределить это.
|
||||
Действие `apex tiers/chown` выставляет корректные права по всем трём уровням хранилища. Без аргументов оно обрабатывает корни уровней (`APEX_TIER_ROOT1..3`); при необходимости можно указать конкретные пути вручную.
|
||||
|
||||
| Путь | Владение | Примечания |
|
||||
|---|---|---|
|
||||
| `<tier-root>` | `root:root` | Только inode каталога. |
|
||||
| `<tier-root>/shared` | `root:root` | Только inode каталога — никогда рекурсивно, поэтому существующие данные проектов внутри не затрагиваются. |
|
||||
| `<tier-root>/shared` | `root:root` | Только inode каталога (без рекурсии, чтобы не затронуть пользовательские данные проектов). |
|
||||
| `<tier-root>/node-infra` | `root:root` | Рекурсивно. |
|
||||
| `<tier-root>/<project>` | `uid:gid` | Рекурсивно; ID читаются из `.env` проекта. |
|
||||
| `<tier-root>/shared/<project>` | `uid:shared_gid` | Рекурсивно, плюс sticky-бит — участники могут добавлять файлы, но удалять только свои. |
|
||||
| `<tier-root>/<project>` | `uid:gid` | Рекурсивно; идентификаторы читаются из `.env` проекта. |
|
||||
| `<tier-root>/shared/<project>` | `uid:shared_gid` | Рекурсивно, с установкой sticky-бита: участники могут добавлять файлы, но удалять — только собственные. |
|
||||
|
||||
Проекты с неполным набором `APEX_UID`/`APEX_GID`/`APEX_SHARED_GID` пропускаются с предупреждением, указывающим на `tiers/useradd`.
|
||||
Проекты с неполным набором переменных `APEX_UID`, `APEX_GID` или `APEX_SHARED_GID` пропускаются с предупреждением и рекомендацией выполнить `tiers/useradd`.
|
||||
|
||||
## Переменные уровней движка {#variables}
|
||||
|
||||
| Переменная | Разрешается в |
|
||||
|---|---|
|
||||
| `APEX_TIER_ROOT1..3` | Корни хранилищ уровней (`compositions/@tierN`, полностью разрешённые). |
|
||||
| `APEX_TIER1..3` | Каталоги данных ядрового проекта внутри каждого уровня (`compositions/apex/@tierN`). |
|
||||
| `APEX_TIER1..3_SHARED` | Общая область внутри уровней ядрового проекта. |
|
||||
| `APEX_TIER_ROOT1..3` | Корни хранилищ уровней (`compositions/@tierN`, абсолютные пути). |
|
||||
| `APEX_TIER1..3` | Каталоги данных ядрового проекта на каждом уровне (`compositions/apex/@tierN`). |
|
||||
| `APEX_TIER1..3_SHARED` | Общие области внутри уровней ядрового проекта. |
|
||||
|
||||
Ядровая композиция монтирует исключительно через них; исполнитель резервного копирования вместо этого монтирует корень данных флота `/srv` в режиме только для чтения и разрешает контейнерные монтирования каждого помеченного сервиса обратно в их хостовые пути.
|
||||
Ядровая композиция монтирует каталоги исключительно через эти переменные. Контейнер резервного копирования, напротив, монтирует общий корень данных флота `/srv` в режиме «только для чтения» и сопоставляет контейнерные пути каждого сервиса с их реальными путями на хосте.
|
||||
|
||||
## Резервные копии {#backups}
|
||||
## Резервное копирование {#backups}
|
||||
|
||||
`apex backup/run <telegram-bot-url>` управляет ядровым сервисом `resticontainer` (профиль `manual`, одноразовый) от начала до конца. Намерение резервного копирования **управляется метками**: resticontainer обнаруживает каждый сервис, несущий `restic.enable=true`, читает его `restic.backup.paths` (и опциональные `restic.backup.stop` / `restic.hooks.*`), разрешает эти контейнерные пути в хостовые пути и делает снимок их объединения в одном запуске restic.
|
||||
Команда `apex backup/run <telegram-bot-url>` полностью берёт на себя управление сервисом `resticontainer` (профиль `manual`, одноразовый запуск). Резервное копирование **управляется метками**: resticontainer находит все контейнеры с меткой `restic.enable=true`, считывает список путей `restic.backup.paths` (и необязательные параметры `restic.backup.stop` / `restic.hooks.*`), транслирует пути контейнеров в пути хоста и создаёт единый снимок restic.
|
||||
|
||||
1. Проверяет доступность репозитория через `snapshots`; инициализирует репозиторий при необходимости.
|
||||
2. Выполняет `backup` с `--host <APEX_NODE_HOST>` и `--tag biweekly` — resticontainer выполняет pre-hook каждого помеченного сервиса, останавливает любой контейнер, помеченный `restic.backup.stop`, делает снимок разрешённых хостовых путей, затем перезапускает и выполняет post-hooks. Запуск отказывается сообщать об успехе, если не появляется новый снимок (защита от обнаружения нулевого количества меток).
|
||||
3. Применяет удержание через `forget --prune` (7 ежедневных, 4 еженедельных, 12 ежемесячных).
|
||||
4. Отправляет статус в Telegram в любом случае; успех, уведомление о котором не может быть доставлено, всё равно завершается с кодом `1`, чтобы планировщики это заметили.
|
||||
1. Проверяет доступность репозитория с помощью команды `snapshots`; при необходимости автоматически инициализирует его.
|
||||
2. Запускает команду `backup` с параметрами `--host <APEX_NODE_HOST>` и `--tag biweekly`. При этом resticontainer выполняет pre-hook сервиса, временно останавливает контейнеры с меткой `restic.backup.stop`, делает снимок хостовых путей, перезапускает остановленные контейнеры и запускает post-hooks. Запуск не засчитывается как успешный, если в репозитории не появился новый снимок (защита от ложного успеха при отсутствии помеченных сервисов).
|
||||
3. Применяет политику хранения с помощью `forget --prune` (хранятся 7 ежедневных, 4 еженедельных и 12 ежемесячных снимков).
|
||||
4. Отправляет статус выполнения в Telegram. Если уведомление не удалось доставить, операция завершается с кодом ошибки `1` даже при успешном бэкапе, чтобы сбой в канале оповещения не остался незамеченным.
|
||||
|
||||
Расположение репозитория и учётные данные полностью берутся из конфигурации `APEX_RESTIC_*` (см. [Справочник: Переменные окружения](/apex/reference/environment)). Какие данные захватываются, решается для каждого сервиса его метками `restic.*` — например, ядровые сервисы резервируют только свою конфигурацию (динамические правила traefik, конфигурацию xray/adguard), оставляя воспроизводимые данные уровня 3, такие как кэши и логи, вне набора.
|
||||
Параметры подключения и учётные данные репозитория считываются из конфигурации `APEX_RESTIC_*` (см. [Справочник: Переменные окружения](/apex/reference/environment)). Состав сохраняемых данных определяется метками каждого отдельного сервиса. Например, для ядровых сервисов копируются только файлы конфигурации (динамические правила traefik, настройки xray и adguard), тогда как легко восстановимые данные уровня 3 (кэши и журналы) в снимок не включаются.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in new issue
Block a user