Welcome to my personal place for love, peace and happiness 🤖

HashiCorp Nomad: развертывание интерактивного Python-приложения на macOS

В мире оркестрации контейнеров и приложений существует множество инструментов, но одним из самых гибких и простых в освоении является HashiCorp Nomad. Это оркестратор рабочих нагрузок, который позволяет развертывать как контейнеризированные, так и обычные приложения с использованием единого декларативного подхода.

В этой статье мы разберем, что такое Nomad, установим его на macOS, создадим простое интерактивное приложение на Marimo с полем ввода имени и генерацией случайных данных на столбчатой диаграмме, а затем развернем его с помощью Nomad, используя драйвер `raw_exec`. В конце обсудим ограничения такого подхода и путь к настоящему продакшену.


Что такое HashiCorp Nomad?

Nomad — это гибкий оркестратор рабочих нагрузок, который позволяет развертывать и управлять любыми контейнеризированными или устаревшими приложениями с помощью единого рабочего процесса.

Ключевые особенности

  • Универсальность: Nomad может запускать Docker-контейнеры, обычные приложения (без контейнеризации), микросервисы и пакетные задачи на одной инфраструктуре.
  • Простота: Nomad распространяется как единый бинарный файл, не требует внешних зависимостей для хранения или координации и автоматически обрабатывает сбои приложений, узлов и драйверов.
  • Масштабируемость: Nomad способен масштабироваться до 10 000+ узлов и имеет встроенную поддержку GPU-нагрузок.
  • Гибкость: Поддерживает различные типы задач: сервисы, пакетные задания и системные задачи. Работает на Linux, macOS и Windows.

Установка Nomad на macOS

Для установки Nomad на macOS удобнее всего использовать Homebrew. Выполните следующие команды в терминале:

# Добавляем репозиторий HashiCorp
brew tap hashicorp/tap

# Устанавливаем Nomad
brew install hashicorp/tap/nomad

После установки проверьте версию:

nomad version

Пример вывода:

Nomad v2.0.5
BuildDate 2026-08-12T18:22:41Z
Revision 5c8612bba6eb8e44cc3fc434125cb1b13463c309

Важно: На Intel Mac (x86_64) могут возникнуть проблемы с загрузкой бинарников из-за прекращения поддержки Apple. В этом случае рекомендуется использовать VPN для доступа к ресурсам HashiCorp. Если Nomad не устанавливается через Homebrew, можно собрать его из исходников или использовать MacPorts.


Запуск Nomad в режиме разработки

Для тестирования и разработки Nomad поддерживает режим `-dev`, который запускает одноузловой кластер на вашем компьютере:

nomad agent -dev -bind=0.0.0.0

Эта команда запустит Nomad, и веб-интерфейс будет доступен по адресу `http://127.0.0.1:4646`. Оставьте этот терминал открытым — это ваш “сервер” Nomad.


Создание Marimo-приложения

Установка `uv`

`uv` — это современный и быстрый менеджер пакетов для Python. Установите его:

curl -LsSf https://astral.sh/uv/install.sh | sh

Создание проекта

mkdir ~/marimo-app
cd ~/marimo-app
uv init
uv add marimo altair pandas numpy

Написание приложения

Создайте файл `app.py` со следующим содержимым. Приложение будет иметь поле для ввода имени, кнопку для генерации случайных значений и столбчатую диаграмму:

import marimo

__generated_with = "0.11.0"
app = marimo.App()


@app.cell
def _():
    import marimo as mo
    import altair as alt
    import pandas as pd
    import numpy as np
    return alt, mo, np, pd


@app.cell
def _(mo):
    # Поле для ввода имени
    name_input = mo.ui.text(
        placeholder="Введите ваше имя...",
        label="Имя пользователя"
    )
    name_input
    return (name_input,)


@app.cell
def _(mo, name_input):
    # Отображение приветствия
    if name_input.value:
        mo.md(f"## Привет, **{name_input.value}**! 👋")
    else:
        mo.md("## Введите имя выше")
    return


@app.cell
def _(mo):
    # Кнопка для генерации данных
    generate_button = mo.ui.button(
        label="🎲 Сгенерировать данные",
        value=0,
        on_click=lambda value: value + 1
    )
    generate_button
    return (generate_button,)


@app.cell
def _(alt, generate_button, mo, np, pd):
    # Генерация случайных данных при нажатии кнопки
    np.random.seed(generate_button.value)
    categories = ["Категория A", "Категория B", "Категория C", "Категория D", "Категория E"]
    values = np.random.randint(10, 100, size=5)
    
    df = pd.DataFrame({
        "Категория": categories,
        "Значение": values
    })
    
    chart = alt.Chart(df).mark_bar().encode(
        x=alt.X("Категория", sort=None),
        y="Значение",
        color=alt.value("#4C78A8")
    ).properties(
        title="Случайные значения по категориям",
        width=500,
        height=300
    )
    
    mo.ui.altair_chart(chart)
    return (chart,)


if __name__ == "__main__":
    app.run()

Проверка локально

Запустите приложение в режиме редактирования:

uv run marimo run app.py

Убедитесь, что приложение работает: введите имя, нажмите кнопку — диаграмма должна обновиться. Закройте сервер (`Ctrl+C`).


Создание задания для Nomad

Создайте файл `marimo-job.nomad` в папке проекта:

job "marimo-app" {
  datacenters = ["dc1"]
  type = "service"

  group "app" {
    count = 2

    network {
      port "http" {}
    }

    task "marimo" {
      driver = "raw_exec"

      config {
        command = "/Users/yuriygavrilov/marimo-app/.venv/bin/marimo"
        args = [
          "run", "app.py",
          "--headless",
          "--port", "${NOMAD_PORT_http}",
          "--host", "0.0.0.0"
        ]
        work_dir = "/Users/yuriygavrilov/marimo-app"
      }

      resources {
        cpu    = 500
        memory = 256
      }
    }
  }
}

Пояснения:

  • `driver = “raw_exec”` — запускает процесс напрямую на хосте без изоляции.
  • `count = 2` — запускаем две реплики приложения. Nomad автоматически распределит их по узлам (в нашем случае — по одному узлу, но на разных портах).
  • `port “http” {}` — динамический порт. Nomad резервирует случайный свободный порт для каждой аллокации. Такой подход избавляет от конфликтов портов при запуске нескольких реплик.
  • `command` — полный путь к исполняемому файлу `marimo` в виртуальном окружении.
  • `${NOMAD_PORT_http}` — Nomad автоматически подставляет динамически выделенный порт через переменную окружения `NOMAD_PORT_
  • `work_dir` — рабочая директория, где находится `app.py`.

Важно: замените `/Users/yuriygavrilov/` на реальный путь к вашей домашней директории.


Запуск задания

Убедитесь, что Nomad работает (в первом терминале). Во втором терминале выполните:

nomad job run marimo-job.nomad

Пример успешного вывода:

==> 2026-09-26T21:26:27+03:00: Monitoring evaluation "cba695bd"
    2026-09-26T21:26:27+03:00: Allocation "2327286c" created: node "7f836ccd", group "app"
==> 2026-09-26T21:26:27+03:00: Monitoring deployment "9ea4ff53"
  ✓ Deployment "9ea4ff53" successful
    
    Deployed
    Task Group  Desired  Placed  Healthy  Unhealthy  Progress Deadline
    app         1        1       1        0

Как узнать порт приложения

После запуска задания выполните:

nomad job status marimo-app

Вы увидите таблицу аллокаций с их ID. Для каждой аллокации выполните:

nomad alloc status <ID_аллокации>

В выводе найдите секцию `Allocation Addresses`:

Allocation Addresses:
Label  Dynamic  Address
*http  yes      127.0.0.1:20938

Откройте в браузере `http://127.0.0.1:20938` — ваше приложение работает.

Повторите команду для остальных аллокаций, чтобы получить порты всех реплик.


Ограничения текущего подхода

Мы успешно запустили приложение, но важно понимать, что это ещё не полноценное масштабированное решение. Вот почему:

1. Нет балансировщика нагрузки

У нас работают две реплики на разных портах (`20938`, `21001` и т.д.). Чтобы обратиться к приложению, пользователь должен знать конкретный порт каждой реплики. Это неудобно:

  • Нельзя дать пользователям один адрес, например `http://marimo.mycompany.com`.
  • Нет автоматического распределения трафика между репликами.
  • Нет отказоустойчивости: если одна реплика упадёт, клиент, знающий только её порт, потеряет доступ.

2. Нет сервис-дискавери

Nomad не регистрирует сервисы автоматически (для этого нужен Consul). Без сервис-дискавери балансировщик не сможет узнать, какие реплики сейчас работают.

3. Минимальная изоляция

Драйвер `raw_exec` запускает процессы прямо на хосте. В продакшене это небезопасно: приложение имеет доступ ко всей файловой системе и может влиять на систему.


Путь к продакшену: что нужно добавить

1. Балансировщик нагрузки (Traefik, NGINX, HAProxy)

Traefik — самый простой вариант для интеграции с Nomad. Он умеет:

  • Автоматически обнаруживать сервисы через Consul.
  • Распределять трафик между всеми здоровыми репликами.
  • Работать с TLS/HTTPS “из коробки”.
  • Иметь удобную панель управления.

Как это выглядит в задании: вы добавляете специальные теги в блок `service` вашего приложения, и Traefik сам настраивает маршрутизацию:

service {
  name = "marimo-app"
  port = "http"
  tags = [
    "traefik.enable=true",
    "traefik.http.routers.marimo.rule=Host(`marimo.localhost`)",
  ]
}

Затем запускаете Traefik как отдельную задачу в Nomad, и он становится единой точкой входа для всех пользователей.

2. Сервис-дискавери (Consul)

Consul — это распределённый каталог сервисов. Он:

  • Хранит информацию о том, какие реплики сейчас работают и на каких портах.
  • Предоставляет DNS-интерфейс: ваше приложение доступно как `marimo-app.service.consul`.
  • Позволяет балансировщику динамически обновлять список бэкендов.

3. Контейнеризация (Docker/Podman)

В продакшене используйте контейнерные драйверы вместо `raw_exec`:

  • Изоляция: приложение работает в своём окружении, не влияя на хост.
  • Воспроизводимость: одинаковое поведение на любой машине.
  • Безопасность: ограничения по ресурсам и правам доступа.

4. Полноценный кластер Nomad

В продакшене Nomad работает не как один процесс, а как кластер:

  • 3 серверных узла (минимум) — хранят состояние и принимают решения о планировании.
  • N клиентских узлов — выполняют задачи.
  • Интеграция с Vault для управления секретами.

Итоговая архитектура для продакшена

Пользователь
     │
     ▼
  Traefik  ──►  Marimo 1 / 2 / 3
(балансировщик)      │
                     ▼
                  Consul
                     ▲
                     │
              Nomad Cluster

Заключение

Мы рассмотрели базовый процесс развертывания Python-приложения с помощью HashiCorp Nomad:

  1. Установили Nomad через Homebrew.
  2. Запустили Nomad в режиме разработки.
  3. Создали Marimo-приложение с полем ввода имени и интерактивной диаграммой.
  4. Написали задание Nomad с драйвером `raw_exec` и динамическим портом.
  5. Запустили две реплики приложения и научились находить их порты.

Этот подход отлично работает для разработки и тестирования. Но для настоящего продакшена необходимо:

  • Добавить балансировщик нагрузки (Traefik, NGINX, HAProxy), чтобы пользователи обращались к одному адресу.
  • Внедрить сервис-дискавери (Consul), чтобы балансировщик знал о работающих репликах.
  • Использовать контейнерные драйверы (Docker, Podman) для изоляции.
  • Построить полноценный кластер Nomad с тремя серверными узлами.

Nomad — это мощный и гибкий инструмент, который подходит как для простых задач, так и для сложных распределённых систем. Его простота и универсальность делают его отличным выбором для разработчиков, которые хотят быстро развертывать приложения без излишней сложности Kubernetes.

Следующий логичный шаг — добавить Traefik и Consul, чтобы превратить наш прототип в отказоустойчивое и масштабируемое решение. Об этом — в следующих статьях.

Follow this blog
Send
Share
Tweet
Pin