Skip to content

Latest commit

 

History

History
538 lines (400 loc) · 22.1 KB

File metadata and controls

538 lines (400 loc) · 22.1 KB

Настройка и эксплуатация Stands Engine

Это руководство предназначено для оператора движка: подготовка provider credentials, cloud-init и SSH, запуск create/destroy, работа с результатами и диагностика.

Формат stand.yml описан в руководстве по манифесту, а формат пакета приложения — в руководстве по приложениям.

create выполняет pulumi up и создаёт реальные ресурсы Hetzner. Проверьте выбранные server types, количество nodes и стоимость до запуска.

1. Требования

  • 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

2. Конфигурация окружения

Настройки читаются из 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=

Provider и state

Переменная Назначение
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 обязателен и должен быть каталогом либо ещё не существовать.

Загрузка env-файла

Для локального процесса:

set -a
source devBack.env
set +a

Не добавляйте env-файл с credentials в Git. В CI используйте protected/secret variables и ограничивайте вывод окружения.

3. SSH key и пользователи

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 административному пользователю.

4. 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

Относительный путь вычисляется от манифеста, где поле объявлено.

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/stand/cloud-init.yaml.mako.

Изменения cloud-init

Pulumi resource настроен с ignore_changes=["user_data"]. Изменение шаблона у уже созданного server не применяет новый user data автоматически. Для существующей ноды выполните настройку отдельно либо осознанно пересоздайте ресурс.

Перед использованием кастомного шаблона:

  1. Отрендерите его тестовыми значениями.
  2. Проверьте cloud-init schema.
  3. Убедитесь, что выбранный OS image содержит нужные systemd/firewalld механизмы.
  4. Проверьте установку Podman и Podlet без интерактивных действий.

5. Registries и secrets

Registry credentials находятся в итоговом манифесте, но значения рекомендуется передавать через !secret:

registries:
  local:
    url: registry.example.test
    username: robot
    password: !secret registry-password
export SECRET_REGISTRY_PASSWORD='change-me'

При create движок:

  1. Проверяет наличие всех manifest secrets до cloud operations.
  2. Выполняет podman login от app user.
  3. Скачивает отсутствующие images.
  4. Выполняет logout.

insecure: true добавляет --tls-verify=false к login/pull. Используйте только для доверенной внутренней сети.

При destroy разрешено не передавать application preferences и registry credentials. Структурные secrets остаются обязательными.

!secret не шифрует configsets и connection files. Не публикуйте их в Git, логи или незащищённые CI artifacts.

6. Локальная проверка до provision

Перед каждым create движок автоматически выполняет локальный preflight. Ту же проверку можно запустить отдельно для разработки и CI:

set -a
source devBack.env
set +a

uv run stands-engine \
  --resource project-assets=demo/resources \
  validate demo/stand/stand.yml

Команда требует те же manifest secrets, что и create, но не требует Hetzner и S3 credentials. Она не создаёт ресурсы, SSH keys, configsets или connection files. При успехе stdout содержит одну NDJSON-запись:

{"operation":"validate","status":"success"}

Preflight проверяет manifest dependencies и связи, наличие файлов, Mako-render cloud-init/application/hook/connection templates, структуру cloud-init и Pod YAML, connection JSON contract, upload destinations/modes и конфликты hostPort. Для рендера используются зарезервированные тестовые IP; реальные manifest preferences и secrets сохраняются, поэтому обнаруживаются ошибки экранирования. Независимые ошибки собираются в один отчёт на stderr без вывода secret values.

Дополнительно вручную проверьте:

  • Hetzner token и права;
  • существование network и административного SSH key;
  • доступность server type/image в выбранной location;
  • S3 bucket, endpoint и credentials;
  • доступность registries;
  • возможность записи key/configset/output paths.

Проверка является локальной: доступность Hetzner network/image/server type/SSH key, S3 backend, registry и container images не проверяется. Pulumi preview не запускается.

7. Запуск

Локально

uv run stands-engine --resource project-assets=demo/resources create demo/stand/stand.yml
uv run stands-engine destroy demo/stand/stand.yml

Совместимый вариант:

python main.py --resource project-assets=demo/resources create demo/stand/stand.yml

CLI принимает только:

stands-engine [--resource NAME=PATH] <validate|create|destroy> <manifest>

Через container launcher

./stands-engine \
  --env-file common.env \
  --env-file stands/devBack.env \
  --resource project-assets=demo/resources \
  create demo/stand/stand.yml
./stands-engine --env-file common.env --env-file stands/devBack.env destroy demo/stand/stand.yml

--env-file можно повторять: файлы загружаются слева направо, и значения из последующих файлов переопределяют значения из предыдущих.

Явный runtime/image:

./stands-engine \
  --runtime docker \
  --image registry.example.test/stands-engine:0.1.0 \
  --env-file devBack.env \
  --resource project-assets=demo/resources \
  create demo/stand/stand.yml

Launcher:

  • выбирает 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 common.env,stands\dev.env
.\stands-engine.ps1 destroy .\demo\stand.yml -EnvFile common.env,stands\dev.env

8. Lifecycle create

Фактическая последовательность:

  1. Загрузка внешней конфигурации.
  2. Parsing manifest, dependencies и secrets.
  3. Validation, сборка модели, разворачивание agents и полный локальный preflight.
  4. Только после успешного preflight — выбор/создание Pulumi stack в S3 backend.
  5. Создание Hetzner servers, attachment к network, cloud-init и labels.
  6. Получение public/private IP и подготовка SSH inventory.
  7. Локальный рендеринг templates и hook assets.
  8. Ожидание cloud-init на nodes.
  9. Настройка Podman, firewalld, app user systemd, socket и app-net.
  10. Registry login, параллельный pull images и logout.
  11. Загрузка templates.
  12. Последовательное развёртывание инстансов в порядке верхнеуровневого apps, затем instances внутри приложения. Для каждого инстанса движок генерирует Podlet unit и запускает user service, ожидает active service и каждый role port (до 30 попыток с интервалом 2 секунды), выполняет post-start hook и только затем переходит к следующему инстансу. Порядок nodes.<node>.apps задаёт размещение и на эту последовательность не влияет.
  13. Рендеринг и публикация connection output.

Движок проверяет service/listen socket, но не HTTP readiness и не dependency graph. Hooks сложных кластеров должны иметь собственный retry/timeout.

Pulumi state и события

Состояние хранится в S3 prefix STAND__USER; credentials передаются Pulumi через AWS-compatible variables. Hetzner token записывается в stack config как secret.

Во время работы движок печатает resource operations, warnings/errors и итоговую сводку Pulumi, но подавляет обычный progress и outputs.

9. Lifecycle destroy

uv run stands-engine destroy demo/stand/stand.yml

destroy:

  1. Загружает тот же manifest и backend identity.
  2. Допускает неразрешённые app/registry secrets.
  3. Выбирает существующий Pulumi stack.
  4. Выполняет pulumi destroy.
  5. После успешного удаления печатает NDJSON-запись с id_stand, operation: "destroy" и status: "success".

Команда не удаляет локальные SSH keys, configsets, connection files, S3 stack metadata или bucket.

Для гарантированного выбора нужного stack не изменяйте STAND__USER, stand.project, stand.env, S3 settings и passphrase между create и destroy.

10. Configsets

Результаты 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 по внутренней политике;
  • учитывать, что текущий код перезаписывает файлы при следующем рендеринге.

11. Connection output

Connection templates определяются приложениями, но публикация настраивается оператором:

Переменная Default Поведение
OUTPUT__CONSOLE true Печатает одну NDJSON-запись после успешного create
OUTPUT__CONSOLE_SECRETS false Показывает настоящие password и URL
OUTPUT__FILE false Сохраняет полный форматированный JSON
OUTPUT__FILE_PATH Каталог, обязательный при file output

Консоль по умолчанию заменяет credentials.password и url на ***. Дополнительные secret-подобные поля внутри credentials автоматически не маскируются.

Во время create/destroy stdout содержит только компактные NDJSON-результаты — по одному JSON-объекту на строку. Для create первым полем идёт id_stand, равный имени configset-каталога, а connection-приложения остаются верхнеуровневыми полями:

{"id_stand":"owner_demo_test","redis":{"endpoint":"10.0.0.2","port":6379,"credentials":{"user":"admin","password":"***"},"url":"***"}}

Имя connection-приложения id_stand зарезервировано; такой create завершается до запуска Pulumi.

Файл:

<OUTPUT__FILE_PATH>/<STAND__USER>_<project>_<env>.json

содержит ту же структуру с реальными значениями, записанную как обычный многострочный JSON с отступами, и создаётся с mode 0600. Рассматривайте файл как секрет. File output выполняется только после успешных приложений и hooks.

Формат самого connection template описан в application guide.

12. Диагностика

Диагностика Pulumi и PyInfra, а также сообщения об ошибках движка отправляются в stderr и не смешиваются с NDJSON в stdout. При ошибке результирующая запись не печатается.

Exit code Значение
0 Успешное выполнение, --help или --version
1 Ошибка конфигурации или выполнения, включая Pulumi, PyInfra и SSH
2 Неверные аргументы командной строки
130 Выполнение прервано через Ctrl+C

Ошибка до Pulumi

Проверьте:

  • Pydantic message об отсутствующей env variable;
  • путь/расширение manifest;
  • from_dep_manifest и локальные ресурсы;
  • список отсутствующих SECRET_*;
  • отчёт stands-engine validate.

Pulumi/S3

  • убедитесь, что Pulumi CLI доступен процессу;
  • проверьте endpoint, bucket, region и credentials;
  • подтвердите прежние project/stack/passphrase;
  • изучите [pulumi:error], [pulumi:warning] и resource events.

Hetzner

  • token имеет нужные права;
  • network и SSH key существуют;
  • location поддерживает server type;
  • account quota и billing позволяют создать nodes.

SSH/cloud-init

  • подключитесь 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 в публичную диагностику.

Checklist перед create

  • stands-engine validate завершился успешно с актуальными secrets.
  • Проверено количество и стоимость 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.