Skip to content

Latest commit

 

History

History
491 lines (389 loc) · 17.3 KB

File metadata and controls

491 lines (389 loc) · 17.3 KB

Описание приложений

Это руководство предназначено для автора переиспользуемого приложения Stands Engine. Оно описывает каталог приложения, контракт app.yml, Mako-шаблоны, connection template и post-start hooks.

Размещение инстансов по серверам и остальные поля stand.yml описаны отдельно в руководстве по манифесту стенда.

1. Модель и структура каталога

Описание приложения хранит неизменную между стендами часть:

  • container image;
  • доступные роли;
  • открываемые ролью порты;
  • шаблоны файлов;
  • опциональный шаблон данных подключения.

Параметры окружения, credentials, инстансы и их ресурсы задаются в stand.yml.

app-registry/
└── hello/
    ├── app.yml
    ├── hello.yml.mako
    ├── connection.json.mako       # необязательно
    └── bootstrap/                 # необязательно
        ├── hook.sh.mako
        └── data.json.mako

Обязательны app.yml и хотя бы один шаблон. Для запуска должен существовать template с ключом pod.

2. Минимальное приложение

app-registry/hello/app.yml:

version: 1
name: hello

image:
  registry: docker
  path: library/nginx
  version: "1.27-alpine"

roles:
  web:
    ports:
      - number: 8080
        protocol: tcp
        zone: internal

templates:
  pod:
    path: hello.yml.mako
    dest: /home/userapp/hello.yml
    owner: userapp
    mode: "644"

app-registry/hello/hello.yml.mako:

apiVersion: v1
kind: Pod
metadata:
  name: ${instance.name}
  labels:
    stands-engine.io/managed: "true"
% if instance.oom_priority is not None:
  annotations:
    io.podman.annotations.oom_score_adj: "${instance.oom_priority}"
% endif
spec:
  restartPolicy: Always
  containers:
    - name: ${instance.name}
      image: ${cluster.image.full_name}
      resources:
        requests:
          cpu: "${instance.cpu}m"
          memory: "${instance.ram}M"
        limits:
          cpu: "${instance.cpu}m"
          memory: "${instance.ram}M"
      ports:
        - containerPort: 80
          hostPort: 8080
          hostIP: ${node.private_ip}

Минимальное подключение в стенде:

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

Ключ apps.hello должен совпадать с name: hello.

3. Контракт app.yml

Полная переиспользуемая часть:

version: 1
name: example

image:
  registry: docker
  path: organization/example
  version: "2.4.1"

roles:
  api:
    preferences:
      container_port: 8080
    ports:
      - number: 8080
        protocol: tcp
        zone: internal
  worker: {}

templates:
  pod:
    path: example.yml.mako
    dest: /home/userapp/example.yml
    owner: userapp
    mode: "644"
  app-config:
    path: application.conf.mako
    dest: /home/userapp/application.conf
    owner: userapp
    mode: "600"

connection: connection.json.mako
Поле Требование
version Рекомендуется 1; текущий parser поле не валидирует
name Обязательная непустая строка, совпадающая с ключом приложения в стенде
image Обязательный mapping с registry, path, version
roles Обязательный непустой mapping
templates Обязательный непустой mapping; runtime ожидает ключ pod
connection Необязательный путь к Mako-шаблону connection JSON

Image

image:
  registry: local
  path: team/service
  version: "2026.07"
  • registry — ключ из registries стенда, а не URL;
  • path — путь образа без registry и tag;
  • version — непустая строка; числовой tag нужно заключать в кавычки.

Полное имя формируется как <registry.url>/<image.path>:<image.version>.

Roles и ports

roles:
  leader:
    preferences:
      quorum_port: 7000
    ports:
      - number: 7000
        protocol: tcp
        zone: internal
  follower: {}

Роль задаёт логическую функцию инстанса, постоянные role preferences и порты. Для порта обязательны:

  • number — integer; используйте допустимый диапазон 1..65535;
  • protocol — только tcp или udp;
  • zone — непустое имя firewalld zone.

Role port открывает firewall и добавляет ожидание listen socket. Он не создаёт containerPort/hostPort в pod и не выполняет прикладной health check. Порты в роли и шаблоне необходимо согласовать вручную.

Templates

templates:
  pod:
    path: service.yml.mako
    dest: /home/userapp/service.yml
    owner: userapp
    mode: "644"

Каждый template требует непустые path, dest, owner и mode. Относительный path вычисляется от файла, в котором объявлен.

Движок рендерит и загружает все templates для каждого инстанса. Дополнительный template сам по себе не используется приложением: pod или hook должен обратиться к его dest.

Ключ pod специальный. Из его dest движок создаёт unit:

podlet --overwrite --unit-directory --name <instance> \
  podman kube play --network app-net --no-pod-prefix <dest>

Содержимое должно поддерживаться podman kube play и Podlet. Валидатор пока не проверяет наличие ключа pod.

owner и домашний каталог в dest должны соответствовать stand.users.app.

4. Mako-контекст

Все app templates, connection template и файлы hook получают:

Переменная Основные поля
node private_ip, public_ip, app_runtime
instance name, cpu, ram, oom_priority, preferences, role
role name, ports, preferences
cluster name, image.full_name, preferences, instances_app, instance_count
apps Все инстансы стенда по глобальному имени

Текущий инстанс:

${instance.name}
${instance.cpu}
${instance.ram}
${role.name}
${node.private_ip}
${cluster.image.full_name}
${cluster.instance_count}
${cluster.preferences.log_level}
${instance.preferences['shard']}
${role.preferences['container_port']}

cluster.preferences поддерживает точечный доступ. Для role/instance preferences предпочтителен доступ как к обычному mapping.

Другой инстанс представлен wrapper с node, app и cluster:

${apps['database-main'].node.private_ip}
${apps['database-main'].app.preferences['shard']}
${apps['database-main'].cluster.preferences['password']}

Ключ — глобальное имя инстанса, а не имя приложения. Такая ссылка создаёт контракт на имя, который нужно обновлять при переименовании.

Условия и экранирование

% if role.name == "leader":
        - name: MODE
          value: leader
% endif

Для строк в JSON и JSON-совместимом YAML используйте encoder:

<%!
import json
%>
value: ${json.dumps(cluster.preferences.message)}

Литеральные shell ${VARIABLE} Mako воспринимает как выражение. Защитите блок:

<%text>
echo "${SHELL_VARIABLE}"
</%text>

Ресурсы

cpu, ram и oom_priority только передаются в контекст. Чтобы они применялись, шаблон должен использовать:

resources:
  requests:
    cpu: "${instance.cpu}m"
    memory: "${instance.ram}M"
  limits:
    cpu: "${instance.cpu}m"
    memory: "${instance.ram}M"

Для OOM priority используется annotation io.podman.annotations.oom_score_adj. Значение задаётся инстансом в диапазоне -1000..1000.

5. Preferences и зависимости

Существуют три независимых уровня:

Уровень Доступ
Приложение apps.<app>.preferences cluster.preferences
Роль roles.<role>.preferences role.preferences
Инстанс instances.<name>.preferences instance.preferences

Mappings автоматически не сливаются. Схема пользовательских preferences не валидируется, поэтому приложение должно документировать обязательные ключи.

Для связи приложений используйте apps:

bootstrapServers: ${apps['redpanda-master'].node.private_ip}:19092

Движок ожидает active service и role ports, но не строит dependency graph. Приложение или hook должны самостоятельно дожидаться прикладной готовности зависимости с ограниченным timeout.

6. Connection template

В app.yml:

connection: connection.json.mako

Стенд дополнительно выбирает обычный инстанс этого приложения через connection_instance.

<%!
import json
from urllib.parse import quote
%><%
user = cluster.preferences.admin_user
password = cluster.preferences.admin_pass
endpoint = node.private_ip
url = f"redis://{quote(user, safe='')}:{quote(password, safe='')}@{endpoint}:6379"
%>{
  "endpoint": ${json.dumps(endpoint)},
  "port": 6379,
  "credentials": {
    "user": ${json.dumps(user)},
    "password": ${json.dumps(password)}
  },
  "url": ${json.dumps(url)}
}

Результат — один JSON object:

Поле Контракт
endpoint Обязательная непустая строка
port Обязательный integer 1..65535
credentials Обязательный object
credentials.user/password Обязательные непустые строки
url Необязательная непустая строка

В credentials разрешены дополнительные поля. Другие поля верхнего уровня запрещены. Значения обязательно сериализуйте через json.dumps; URL-компоненты также кодируйте через urllib.parse.quote.

Настройки публикации и защиты connection output описаны в руководстве по эксплуатации.

7. Post-start hooks

Каталог hook:

bootstrap/
├── hook.sh.mako
└── helpers.sh

Путь подключается к конкретному инстансу через instances.<name>.hooks. Каталог обязан содержать hook.sh.mako.

Движок:

  1. Рекурсивно обрабатывает файлы: файлы с суффиксом .mako рендерит как Mako-текст, остальные копирует без изменений.
  2. Удаляет суффикс .mako только у отрендеренных файлов.
  3. Загружает дерево в /home/<app-user>/hook/<instance>/ с mode 644.
  4. Выполняет hook.sh из корня дерева.
  5. После успеха удаляет remote-каталог.

Обычные, в том числе бинарные, ресурсы можно помещать в hook-каталог без суффикса .mako: движок сохраняет их имена и содержимое без изменений.

Stand-specific миграции необязательно хранить рядом с переиспользуемым hook. Их можно подключить из прикладного проекта:

hooks:
  path: hook
  assets:
    - source: resource://project-assets/mongo/migrations
      dest: migration

Resource задаётся при запуске через --resource project-assets=/path/to/application/assets. Один корень может обслуживать assets нескольких hooks. Движок рекурсивно добавляет содержимое source в dest. Внешние assets всегда копируются буквально, включая файлы с суффиксом .mako, и не могут перезаписывать файлы базового hook или другого asset.

Надёжный hook должен:

  • завершаться при ошибке и возвращать ненулевой exit code;
  • быть идемпотентным;
  • иметь retry с общим timeout для зависимостей;
  • не печатать секреты;
  • использовать относительные пути от корня hook.

Примеры: mongo/hook и redpanda/migration.

8. Проверка приложения

До реального стенда:

  1. Подключите приложение к минимальному тестовому stand.yml и выполните stands-engine validate из stand guide.
  2. Проверьте в отрендерованном Pod image, имена, resources, volumes и ports, специфичные для приложения.
  3. Отдельно протестируйте фактическое выполнение hook и поведение приложения — локальный preflight проверяет Mako render, но не исполняет shell-команды.

Подход к unit-тесту рендеринга показан в tests/test_app_resources.py.

9. Типичные ошибки

  • app.name must match app keyname не совпадает с ключом в apps.
  • Unknown registryimage.registry содержит URL вместо ключа registry.
  • Image version is not a string — заключите числовой tag в кавычки.
  • Ошибка templates["pod"] — templates непустой, но ключ pod отсутствует.
  • Порт не слушается — role port не согласован с hostPort или listen address.
  • Ресурсы не применились — template не использует instance.cpu/ram.
  • Другой app не найден — в apps[...] указано имя приложения вместо инстанса.
  • Hook падает на ${VAR} — shell expression не защищён от Mako.
  • Connection не создаётся — отсутствует connection или connection_instance.

Checklist автора приложения

  • name стабилен и совпадёт с ключом приложения.
  • Registry указан ключом, image tag является строкой.
  • Роли и порты соответствуют реальному поведению контейнера.
  • В templates есть pod.
  • Пути, owner и mode подходят app user.
  • Pod использует instance.name и cluster.image.full_name.
  • CPU, RAM и OOM priority отражены в шаблоне.
  • Контракт preferences понятен автору стенда.
  • Все строки безопасно экранируются.
  • Зависимости имеют retry/timeout.
  • Connection выдаёт допустимый JSON.
  • Hook текстовый, идемпотентный и не раскрывает секреты.
  • Все templates успешно рендерятся и разбираются.

Готовые эталоны находятся в demo/stand/app-registry: Redis — простой stateful service, Redpanda — несколько ролей, Kafka UI — зависимость, MongoDB — hook и connection, Dozzle — node agent.