Это руководство описывает только декларативный контракт stand.yml: какие
серверы нужны, какие приложения и инстансы существуют и где они размещаются.
Создание нового app.yml рассматривается в
руководстве по приложениям, а provider credentials,
cloud-init и команды запуска — в
руководстве по эксплуатации.
stand
├── node_profiles ──> nodes
├── registries ─────> apps ──> instances
├── agents ─────────> instances на каждой node
└── nodes.apps ─────> размещённые instances
Манифест описывает желаемое состояние целиком. Имена образуют связи:
- приложение ссылается на registry;
- инстанс ссылается на роль своего приложения;
- node ссылается на profile и глобальные имена инстансов;
- agent ссылается на инстанс, который нужно размножить на все nodes.
version: 1
stand:
project: demo
env: test
users:
sudo: admin
app: userapp
ssh:
key_name_admin: admin-key
node_profiles:
default:
location: hel1
type_serv: cpx32
image: rocky-10
network: demo-network
cloud-init: ./cloud-init.yaml.mako
registries:
docker:
url: docker.io
apps:
hello:
from_dep_manifest: ./app-registry/hello/app.yml
instances:
hello-main:
role: web
cpu: 250
ram: 128
nodes:
app-server:
profile: default
apps:
- hello-mainОбязательные корневые mappings: stand, node_profiles, registries, apps,
nodes. Все они должны быть непустыми. agents необязателен.
version: 1 используется во всех примерах, но текущий parser его не валидирует.
stand:
project: payments
env: test
users:
sudo: stand-admin
app: userapp
ssh:
key_name_admin: company-admin-key| Поле | Назначение |
|---|---|
project |
Имя Pulumi project и часть имени локальных результатов |
env |
Имя Pulumi stack/окружения |
users.sudo |
Административный пользователь создаваемых серверов |
users.app |
Пользователь, от которого запускаются приложения |
ssh.key_name_admin |
Имя уже существующего SSH key в Hetzner |
Все значения обязательны и должны быть непустыми строками.
stand.project и stand.env вместе с внешним STAND__USER идентифицируют
состояние и локальные артефакты. Не меняйте их у существующего стенда без
понимания, что движок выберет другой Pulumi project/stack.
Создание пользователей и SSH key описано в operations guide.
Profile содержит повторяющуюся конфигурацию сервера:
node_profiles:
default:
location: hel1
type_serv: cpx32
image: rocky-10
network: demo-network
cloud-init: ./cloud-init.yaml.mako
app_runtime: podman
large:
location: fsn1
type_serv: cpx52
image: rocky-10
network: demo-network
cloud-init: ./cloud-init.yaml.mako
app_runtime: podman| Поле | Требование |
|---|---|
location |
Обязательная локация Hetzner |
type_serv |
Обязательный тип Hetzner Server |
image |
Обязательное имя поддерживаемого Hetzner image |
network |
Имя уже существующей Hetzner network |
cloud-init |
Путь к обязательному Mako-шаблону |
app_runtime |
Необязательно; по умолчанию podman |
Если node не содержит profile, используется profile с именем default. Поэтому
либо объявите default, либо задавайте profile каждой node.
Все nodes стенда должны после применения overrides иметь один и тот же
app_runtime; несколько runtimes в одном стенде пока не поддерживаются.
Путь cloud-init разрешается относительно файла, где он объявлен. Контекст и
требования к шаблону описаны в operations.
registries:
docker:
url: docker.io
local:
url: registry.example.test
username: robot
password: !secret registry-password
insecure: falseRegistry key должен быть непустым и используется в apps.*.image.registry.
| Поле | Требование |
|---|---|
url |
Обязательная непустая строка без схемы, если её не принимает Podman |
username |
Необязательно, но только вместе с password |
password |
Необязательно, но только вместе с username |
insecure |
Необязательный boolean, по умолчанию false |
Registries удобно вынести:
from_dep_manifest: ./app-registry/registries.ymlПравила безопасности и выполнение login/pull/logout описаны в operations.
Стенд подключает переиспользуемое приложение и добавляет конкретные параметры:
apps:
redis:
from_dep_manifest: ./app-registry/redis/app.yml
connection_instance: redis-main
preferences:
admin_user: stand-admin
admin_pass: !secret redis-admin-password
instances:
redis-main:
role: master-redis
cpu: 1000
ram: 2048
oom_priority: -200
preferences:
shard: primary
hooks: ./app-registry/redis/bootstrapПосле раскрытия dependency приложение обязано иметь name, image, roles,
templates и непустой instances.
preferences — произвольный mapping общих параметров приложения. Его схема
определяется шаблонами конкретного приложения. Секретные значения задавайте через
!secret.
Механизм dependency не поддерживает overrides конфликтующих ключей. Поэтому
stand-specific preferences не следует одновременно объявлять в app.yml.
| Поле | Требование |
|---|---|
role |
Обязательное имя роли этого приложения |
cpu |
Обязательный положительный integer в millicpu |
ram |
Обязательный положительный integer в десятичных MB |
oom_priority |
Необязательный integer -1000..1000 |
preferences |
Необязательный mapping параметров инстанса |
hooks |
Необязательный путь либо конфигурация hook и внешних assets |
Имена инстансов глобальны для всего стенда, включая разные приложения. Повтор запрещён.
Относительный hooks разрешается от манифеста, в котором поле объявлено. Если
поле находится в stand.yml, путь указывайте относительно stand.yml.
Для переиспользуемого hook и миграций из прикладного проекта используется расширенная форма:
instances:
mongo-main:
role: member
cpu: 1000
ram: 2048
hooks:
path: hook
assets:
- source: resource://project-assets/mongo/migrations
dest: migrationpath задаёт базовый hook с обязательным hook.sh.mako. Каждый source должен
указывать на каталог, а dest — на безопасный относительный POSIX-каталог внутри
hook. Значение . добавляет файлы в корень hook. Содержимое assets копируется как
есть и не обрабатывается Mako.
Именованный корень передаётся одинаково launcher-у и локальному CLI:
./stands-engine --resource project-assets=/path/to/project/assets create stand.yml
uv run stands-engine --resource project-assets=/path/to/project/assets create stand.ymlПри destroy подключать resources не требуется.
Если подключённый app.yml объявляет connection, стенд обязан выбрать
connection_instance из инстансов того же приложения.
Без connection поле запрещено. Agent-инстанс выбрать нельзя. Формат
connection template описан в
application guide.
nodes:
api-server:
profile: default
apps:
- api-main
- redis-main
worker-server:
profile: large
location: nbg1
apps:
- worker-1Каждый node key — имя создаваемого Hetzner Server. Node требует список apps,
который может быть пустым.
profile необязателен и по умолчанию равен default. Поддерживаемые overrides:
nodes:
special-server:
profile: default
location: fsn1
type_serv: cpx42
image: rocky-10
network: other-network
cloud-init: ./special-cloud-init.yaml.mako
app_runtime: podman
apps: []Overrides заменяют соответствующие значения выбранного profile. Текущий валидатор подробно проверяет profile, но не типы всех override-полей; используйте те же непустые строковые значения, что и в profile.
- каждый обычный инстанс обязан находиться ровно на одной node;
- неизвестное имя инстанса запрещено;
- повтор на той же или другой node запрещён;
- agent-инстанс нельзя размещать вручную;
- node должна ссылаться на существующий profile.
Порядок развёртывания является частью контракта манифеста. Движок обрабатывает
приложения сверху вниз в порядке ключей верхнеуровневого mapping apps, а внутри
каждого приложения — сверху вниз в порядке ключей instances.
Для каждого инстанса движок последовательно генерирует и запускает systemd unit,
дожидается состояния active и прослушивания всех портов роли, затем выполняет
hook, если он задан. Только после успешного завершения этих действий начинается
развёртывание следующего инстанса. Поэтому изменение порядка apps или
instances в YAML изменяет порядок развёртывания.
Список nodes.<node>.apps задаёт только размещение инстансов. Порядок элементов в
нём и порядок самих nodes на порядок развёртывания не влияют. Движок не строит
dependency graph и не проверяет прикладную готовность: если следующему приложению
нужна более сильная гарантия, чем active service и открытые порты, её следует
реализовать retry/timeout-логикой приложения или hook.
Agent — объявленный инстанс, который автоматически запускается на каждой node:
apps:
dozzle:
from_dep_manifest: ./app-registry/dozzle/app.yml
instances:
dozzle:
role: logs-viewer
cpu: 500
ram: 512
agents:
apps:
- dozzleДля nodes master и worker движок создаст:
dozzle--master
dozzle--worker
Эти имена становятся фактическими instance.name, ключами Mako apps,
service/container names и частями configset.
Agent-инстансы развёртываются на позиции соответствующего приложения в
верхнеуровневом apps. Внутри него сгенерированные инстансы идут после обычных
инстансов этого приложения и следуют порядку nodes.
Ограничения:
- список содержит существующие уникальные имена инстансов;
- agent не указывается в
nodes.*.apps; - agent не может быть
connection_instance; - генерируемое имя не должно конфликтовать с явным инстансом.
agents можно не указывать. agents.apps: [] допустим.
Ключ можно использовать внутри любого YAML mapping:
apps:
redis:
from_dep_manifest: ./app-registry/redis/app.yml
instances:
redis-main:
role: master-redis
cpu: 500
ram: 512Правила:
- Путь вычисляется относительно текущего манифеста.
- Dependencies раскрываются рекурсивно.
- Загруженный mapping объединяется с локальными ключами на текущем уровне.
- Одинаковый ключ с разными значениями вызывает ошибку.
- Локальное значение не переопределяет dependency.
- Пути templates, connection, hooks и cloud-init нормализуются относительно соответствующих объявлений.
Файл должен иметь расширение .yml или .yaml и содержать YAML mapping.
preferences:
password: !secret service-passwordИмя преобразуется в env variable:
service-password -> SECRET_SERVICE_PASSWORD
Допустимый шаблон имени: [A-Za-z][A-Za-z0-9_-]*.
!secret применяется только к scalar value. Его нельзя использовать как mapping
key или для list/mapping. Подставленное значение всегда является строкой.
Перед create все ссылки должны иметь непустые значения. При destroy можно не
передавать секреты из preferences и registry credentials; структурные значения
остаются обязательными.
Подробнее о загрузке окружения и защите результатов: operations.
Полный актуальный пример находится в demo/stand/stand.yml. Он
показывает:
- общий node profile;
- внешний registry manifest;
- несколько приложений и ролей;
- cluster preferences и secrets;
- ресурсы и hooks;
- connection instances;
- agent на каждой node;
- распределение обычных инстансов по трём nodes.
Используйте demo как структуру, но замените project, env, users, provider resource names, credentials и размеры серверов.
Для полной локальной проверки выполните:
set -a
source devBack.env
set +a
uv run stands-engine \
--resource project-assets=demo/resources \
validate demo/stand/stand.ymlКоманда раскрывает dependencies, разрешает secrets, нормализует пути, проверяет связи и локально рендерит cloud-init, app, hook и connection Mako templates. Она не создаёт облачные ресурсы, ключи или configsets и не требует Hetzner/S3 credentials.
Валидатор проверяет:
- обязательные mappings и строки;
- registry credentials и ссылки image;
- совпадение app key/name;
- roles, resources и OOM range;
- connection ownership;
- глобальную уникальность инстансов;
- profiles и размещение;
- agents и конфликты генерируемых имён;
- существование templates, Mako render и структуру результирующих YAML/JSON;
- upload paths/modes и конфликты hostPort на одном узле;
- обязательные secrets.
После проверки YAML отдельно убедитесь, что provider resources реально существуют. Это относится к эксплуатационной проверке, а не к parser.
- Missing section — отсутствует обязательный корневой mapping.
app.name must match app key— ключ app отличается от подключённого name.- Unknown registry/role/profile/instance — нарушена именованная ссылка.
from_dep_manifest conflicts— локальный ключ пытается переопределить dependency.- Instance is not assigned — обычный инстанс отсутствует во всех nodes.
- Instance already assigned — инстанс указан более одного раза.
- Agent cannot be assigned — agent вручную добавлен на node.
- Connection outside app — выбран инстанс другого приложения.
- Only one app runtime — profiles/overrides дают разные runtimes.
- Missing manifest secret — соответствующая
SECRET_*отсутствует или пуста.
-
project,env, users и SSH key name заданы осознанно. - Каждый profile содержит все обязательные provider-поля.
- Hetzner network, images, server types и locations существуют.
- Все nodes используют один app runtime.
- Registries объявлены, credentials заданы парами.
- Каждый app key совпадает с
app.ymlname. - Все обязательные app preferences заполнены.
- Имена инстансов глобально уникальны.
- CPU/RAM положительны, OOM priority находится в диапазоне.
- Каждый обычный инстанс размещён ровно на одной node.
- Agents не размещены вручную.
- Connection instance принадлежит нужному приложению.
- Относительные пути вычисляются от правильного файла.
- Все
SECRET_*переданы процессу. - Статическая проверка проходит без облачных операций.
- Перед
createвыполнен operational checklist из operations.