Это руководство предназначено для оператора движка: подготовка provider
credentials, cloud-init и SSH, запуск create/destroy, работа с результатами и
диагностика.
Формат stand.yml описан в руководстве по манифесту, а
формат пакета приложения — в
руководстве по приложениям.
createвыполняетpulumi upи создаёт реальные ресурсы Hetzner. Проверьте выбранные server types, количество nodes и стоимость до запуска.
- Python
>=3.14иuvдля локального запуска; - Pulumi CLI в
PATH; - аккаунт Hetzner Cloud и API token;
- существующие Hetzner network и административный SSH key;
- S3-совместимое хранилище для Pulumi state;
- доступ к registries приложений;
- на целевом image — рабочий cloud-init;
- Docker или Podman, если движок запускается через container launcher.
Python-зависимости:
uv syncПроверка CLI:
uv run stands-engine --version
uv run stands-engine --help
pulumi versionНастройки читаются из environment или .env. Вложенность задаётся через __,
имена регистронезависимы, лишние переменные игнорируются.
HCLOUD__TOKEN=
S3__ACCESS_KEY=
S3__SECRET_KEY=
S3__REGION=
S3__ENDPOINT=
S3__BUCKET=
STAND__USER=
STAND__PASSPHRASE=
STAND__PATH_TO_KEY=
STAND__PATH_TO_CONFIGSET=
OUTPUT__CONSOLE=true
OUTPUT__CONSOLE_SECRETS=false
OUTPUT__FILE=false
OUTPUT__FILE_PATH=| Переменная | Назначение |
|---|---|
HCLOUD__TOKEN |
Token Hetzner Cloud API |
S3__ACCESS_KEY |
S3 access key для Pulumi backend |
S3__SECRET_KEY |
S3 secret key |
S3__REGION |
Регион S3 |
S3__ENDPOINT |
Endpoint с https:// или без схемы |
S3__BUCKET |
Bucket состояния |
Backend формируется как:
s3://<bucket>/<STAND__USER>?region=<region>&endpoint=<endpoint>&s3ForcePathStyle=true
Pulumi project берётся из stand.project, stack — из stand.env.
STAND__PASSPHRASE используется provider passphrase для Pulumi secrets.
Для повторного create и последующего destroy используйте те же:
STAND__USER;STAND__PASSPHRASE;stand.project;stand.env;- S3 backend.
Иначе будет выбран другой backend prefix, project или stack либо станет невозможно расшифровать state.
| Переменная | Назначение |
|---|---|
STAND__PATH_TO_KEY |
Файл приватного SSH key стенда |
STAND__PATH_TO_CONFIGSET |
Корень локальных отрендерированных файлов |
OUTPUT__FILE_PATH |
Каталог connection output |
Если OUTPUT__FILE=true, OUTPUT__FILE_PATH обязателен и должен быть каталогом
либо ещё не существовать.
Для локального процесса:
set -a
source dev.env
set +aНе добавляйте env-файл с credentials в Git. В CI используйте protected/secret variables и ограничивайте вывод окружения.
stand.users.sudo и stand.users.app передаются cloud-init:
- sudo user получает публичный ключ и административный доступ;
- app user используется для rootless Podman и user systemd.
stand.ssh.key_name_admin — имя существующего SSH key в Hetzner, которое provider
передаёт создаваемому серверу. Оно не является путём к локальному ключу.
Локальная пара управляется через STAND__PATH_TO_KEY:
- если файл существует, движок читает private key и вычисляет public key;
- если файла нет при
create, движок генерирует ключ, использует public part в cloud-init и записывает private key по указанному пути; - при
destroyсуществующий ключ используется только для построения модели, подключение к серверу не требуется.
Каталог для нового ключа должен существовать и быть доступен на запись. Защитите private key правами файловой системы и резервной копией, если стенд нужно диагностировать по SSH.
Hetzner SSH key из key_name_admin и сгенерированный ключ выполняют разные роли:
первый передаётся provider как ресурс Hetzner, второй добавляется cloud-init
административному пользователю.
Каждый node profile должен ссылаться на Mako-файл:
node_profiles:
default:
location: hel1
type_serv: cpx32
image: rocky-10
network: demo-network
cloud-init: ./cloud-init.yaml.makoОтносительный путь вычисляется от манифеста, где поле объявлено.
| Переменная | Значение |
|---|---|
user_admin |
stand.users.sudo |
user_app |
stand.users.app |
ssh_public_key |
Public part локального ключа стенда |
network_ip_range |
CIDR найденной Hetzner network |
Минимальные фрагменты:
#cloud-config
users:
- name: ${user_admin}
shell: /bin/bash
ssh_authorized_keys:
- ${ssh_public_key}
- name: ${user_app}
shell: /bin/bashТекущий runtime ожидает, что cloud-init подготовит как минимум:
- административного и app users;
- SSH-доступ admin user;
- Podman;
- firewalld и зоны, используемые app roles;
- Podlet в
PATH.
После provision движок ожидает cloud-init status --wait, затем самостоятельно
настраивает user systemd, linger, Podman socket и сеть app-net.
Готовый поддерживаемый шаблон:
demo/cloud-init.yaml.mako.
Pulumi resource настроен с ignore_changes=["user_data"]. Изменение шаблона у
уже созданного server не применяет новый user data автоматически. Для существующей
ноды выполните настройку отдельно либо осознанно пересоздайте ресурс.
Перед использованием кастомного шаблона:
- Отрендерите его тестовыми значениями.
- Проверьте
cloud-init schema. - Убедитесь, что выбранный OS image содержит нужные systemd/firewalld механизмы.
- Проверьте установку Podman и Podlet без интерактивных действий.
Registry credentials находятся в итоговом манифесте, но значения рекомендуется
передавать через !secret:
registries:
local:
url: registry.example.test
username: robot
password: !secret registry-passwordexport SECRET_REGISTRY_PASSWORD='change-me'При create движок:
- Проверяет наличие всех manifest secrets до cloud operations.
- Выполняет
podman loginот app user. - Скачивает отсутствующие images.
- Выполняет logout.
insecure: true добавляет --tls-verify=false к login/pull. Используйте только
для доверенной внутренней сети.
При destroy разрешено не передавать application preferences и registry
credentials. Структурные secrets остаются обязательными.
!secret не шифрует configsets и connection files. Не публикуйте их в Git,
логи или незащищённые CI artifacts.
До provision выполните полный parser:
set -a
source dev.env
set +a
uv run python -c \
'from pathlib import Path; from ManifestParser import parse_manifest; parse_manifest(Path("demo/stand.yml")); print("manifest: OK")'Команда не создаёт ресурсы. Она проверяет YAML, dependencies, secrets, связи и нормализует пути.
Дополнительно вручную проверьте:
- Hetzner token и права;
- существование network и административного SSH key;
- доступность server type/image в выбранной location;
- S3 bucket, endpoint и credentials;
- доступность registries;
- возможность записи key/configset/output paths.
Проект пока не предоставляет preview или validate через CLI, хотя внутренний
provision layer содержит Pulumi preview.
uv run stands-engine create demo/stand.yml
uv run stands-engine destroy demo/stand.ymlСовместимый вариант:
python main.py create demo/stand.ymlCLI принимает только:
stands-engine <create|destroy> <manifest>
./stands-engine --env-file dev.env create demo/stand.yml
./stands-engine --env-file dev.env destroy demo/stand.ymlЯвный runtime/image:
./stands-engine \
--runtime docker \
--image registry.example.test/stands-engine:0.1.0 \
--env-file dev.env \
create demo/stand.ymlLauncher:
- выбирает Podman или Docker;
- монтирует workspace read-only в
/workspace; - создаёт
.stands-engine/keys,configsets,output; - переопределяет пути на
/data/...; - удаляет временный container после команды.
На Linux launcher использует SELinux volume labels для Podman. Для воспроизводимости используйте immutable version tag или digest движка.
PowerShell:
.\stands-engine.ps1 create .\demo\stand.yml -EnvFile dev.env
.\stands-engine.ps1 destroy .\demo\stand.yml -EnvFile dev.envФактическая последовательность:
- Загрузка внешней конфигурации.
- Parsing manifest, dependencies и secrets.
- Validation и сборка модели; разворачивание agents.
- Выбор/создание Pulumi stack в S3 backend.
- Создание Hetzner servers, attachment к network, cloud-init и labels.
- Получение public/private IP и подготовка SSH inventory.
- Локальный рендеринг templates и hook assets.
- Ожидание cloud-init на nodes.
- Настройка Podman, firewalld, app user systemd, socket и
app-net. - Registry login, параллельный pull images и logout.
- Загрузка templates.
- Генерация Podlet units и запуск user services.
- Ожидание active service и каждого role port: до 30 попыток с интервалом 2 секунды.
- Выполнение post-start hooks.
- Рендеринг и публикация connection output.
Движок проверяет service/listen socket, но не HTTP readiness и не dependency graph. Hooks сложных кластеров должны иметь собственный retry/timeout.
Состояние хранится в S3 prefix STAND__USER; credentials передаются Pulumi через
AWS-compatible variables. Hetzner token записывается в stack config как secret.
Во время работы движок печатает resource operations, warnings/errors и итоговую сводку Pulumi, но подавляет обычный progress и outputs.
uv run stands-engine destroy demo/stand.ymldestroy:
- Загружает тот же manifest и backend identity.
- Допускает неразрешённые app/registry secrets.
- Выбирает существующий Pulumi stack.
- Выполняет
pulumi destroy.
Команда не удаляет локальные SSH keys, configsets, connection files, S3 stack metadata или bucket.
Для гарантированного выбора нужного stack не изменяйте STAND__USER,
stand.project, stand.env, S3 settings и passphrase между create и
destroy.
Результаты Mako сохраняются в:
<STAND__PATH_TO_CONFIGSET>/<owner>_<project>_<env>/
└── <app>--<instance>/
├── <template-without-.mako>
└── hook/
Configset полезен для диагностики фактически переданного server config. Он может содержать passwords, tokens, keys и application data в открытом виде.
Рекомендации:
- исключить каталог из Git;
- ограничить filesystem access;
- не отправлять целиком в issue/логи;
- очищать устаревшие configsets по внутренней политике;
- учитывать, что текущий код перезаписывает файлы при следующем рендеринге.
Connection templates определяются приложениями, но публикация настраивается оператором:
| Переменная | Default | Поведение |
|---|---|---|
OUTPUT__CONSOLE |
true |
Печатает общий JSON после успешного create |
OUTPUT__CONSOLE_SECRETS |
false |
Показывает настоящие password и URL |
OUTPUT__FILE |
false |
Сохраняет полный JSON |
OUTPUT__FILE_PATH |
— | Каталог, обязательный при file output |
Консоль по умолчанию заменяет credentials.password и url на ***.
Дополнительные secret-подобные поля внутри credentials автоматически не
маскируются.
Файл:
<OUTPUT__FILE_PATH>/<STAND__USER>_<project>_<env>.json
содержит реальные значения и создаётся с mode 0600. Рассматривайте его как
секрет. File output выполняется только после успешных приложений и hooks.
Формат самого connection template описан в application guide.
Проверьте:
- Pydantic message об отсутствующей env variable;
- путь/расширение manifest;
from_dep_manifestи локальные ресурсы;- список отсутствующих
SECRET_*; - статический parser.
- убедитесь, что Pulumi CLI доступен процессу;
- проверьте endpoint, bucket, region и credentials;
- подтвердите прежние project/stack/passphrase;
- изучите
[pulumi:error],[pulumi:warning]и resource events.
- token имеет нужные права;
- network и SSH key существуют;
- location поддерживает server type;
- account quota и billing позволяют создать nodes.
- подключитесь private key из
STAND__PATH_TO_KEY; - проверьте
/var/log/cloud-init.logиcloud-init status --long; - убедитесь, что admin/app users созданы;
- проверьте firewalld, Podman, Podlet и user systemd.
На node:
systemctl --user --machine=userapp@.host status <instance>.service --no-pager
ss -ltnПроверьте отрендерированный configset, image pull, host ports, Podman logs и ошибки hook. Не вставляйте секретный configset в публичную диагностику.
- Manifest прошёл статический parser.
- Проверено количество и стоимость Hetzner servers.
- Token, network, SSH key, locations, images и server types существуют.
- S3 backend доступен и сохранены identity/passphrase.
- Локальный key path защищён и доступен на запись.
- Cloud-init отрендерирован и проверен.
- Registries доступны, все
SECRET_*переданы. - Configset/output directories защищены и исключены из Git.
- App templates и hooks протестированы.
- Connection output настроен согласно политике секретов.
- Для первого запуска используется отдельный test stand.
- Известна команда
destroyс теми же backend parameters.