<?xml version="1.0" encoding="utf-8"?> 
<rss version="2.0"
  xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
  xmlns:atom="http://www.w3.org/2005/Atom">

<channel>

<title>Yuriy Gavrilov: posts tagged DevOps</title>
<link>https://gavrilov.info/tags/devops/</link>
<description>Welcome to my personal place for love, peace and happiness 🤖 Yuiry Gavrilov</description>
<author></author>
<language>en</language>
<generator>Aegea 11.4 (v4171e)</generator>

<itunes:owner>
<itunes:name></itunes:name>
<itunes:email>yvgavrilov@gmail.com</itunes:email>
</itunes:owner>
<itunes:subtitle>Welcome to my personal place for love, peace and happiness 🤖 Yuiry Gavrilov</itunes:subtitle>
<itunes:image href="https://gavrilov.info/pictures/userpic/userpic-square@2x.jpg?1643451008" />
<itunes:explicit>no</itunes:explicit>

<item>
<title>Части 3: Настройка HTTPS с самоподписными сертификатами для Traefik и Marimo</title>
<guid isPermaLink="false">358</guid>
<link>https://gavrilov.info/all/chasti-3-nastroyka-https-s-samopodpisnymi-sertifikatami-dlya-tra/</link>
<pubDate>Tue, 29 Sep 2026 00:02:00 +0300</pubDate>
<author></author>
<comments>https://gavrilov.info/all/chasti-3-nastroyka-https-s-samopodpisnymi-sertifikatami-dlya-tra/</comments>
<description>
&lt;p&gt;В предыдущей части мы настроили балансировку нагрузки через Traefik, но весь трафик ходил по HTTP — незашифрованным. В этой части мы добавим HTTPS с помощью самоподписных сертификатов. Это хороший способ подготовить инфраструктуру к продакшену, где вы сможете заменить их на сертификаты от Let’s Encrypt буквально одной строкой.&lt;/p&gt;
&lt;p&gt;Часть 1: &lt;a href="https://gavrilov.info/all/hashicorp-nomad-razvertyvanie-interaktivnogo-python-prilozheniya/"&gt;HashiCorp Nomad: развертывание интерактивного Python-приложения на macOS&lt;/a&gt;&lt;br /&gt;
Часть 2: &lt;a href="https://gavrilov.info/all/chast-2-balansirovka-nagruzki-i-servis-diskaveri-s-consul-i-trae/"&gt;Балансировка нагрузки и сервис-дискавери с Consul и Traefik для Marimo&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Зачем нужен HTTPS&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Шифрование&lt;/b&gt;: данные между браузером и сервером передаются в зашифрованном виде.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Подготовка к продакшену&lt;/b&gt;: архитектура с двумя точками входа (HTTP и HTTPS) и автоматическим редиректом — это стандартная схема.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Доверие к сертификатам&lt;/b&gt;: локально вы можете использовать самоподписные сертификаты, а в продакшене — Let’s Encrypt, не меняя структуру конфигурации.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Шаг 1: Генерация самоподписного сертификата&lt;/h3&gt;
&lt;p&gt;Для создания сертификата используем `openssl`, который уже есть в macOS. Важный момент: обязательно указываем &lt;b&gt;Subject Alternative Name (SAN)&lt;/b&gt; с именем домена, иначе современные браузеры откажутся считать сертификат валидным.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;mkdir -p ~/marimo-app/certs &amp;amp;&amp;amp; cd ~/marimo-app/certs

openssl req -x509 -nodes -days 365 \
  -newkey rsa:2048 \
  -keyout marimo.localhost.key \
  -out marimo.localhost.crt \
  -subj &amp;quot;/CN=marimo.localhost&amp;quot; \
  -addext &amp;quot;subjectAltName=DNS:marimo.localhost&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;В папке `certs` появятся два файла:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;`marimo.localhost.key` — приватный ключ.&lt;/li&gt;
&lt;li&gt;`marimo.localhost.crt` — сертификат.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;b&gt;Что произойдёт при открытии сайта&lt;/b&gt;: браузер покажет предупреждение, что сертификат не является доверенным. Это ожидаемое поведение — вы можете принять исключение для домена `marimo.localhost`. В продакшене предупреждения не будет, так как сертификаты Let’s Encrypt автоматически доверенные.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Шаг 2: Обновление конфигурации Traefik&lt;/h3&gt;
&lt;p&gt;Traefik поддерживает разделение конфигурации на &lt;b&gt;статическую&lt;/b&gt; (точки входа, провайдеры) и &lt;b&gt;динамическую&lt;/b&gt; (маршруты, сертификаты). Мы создадим обе через блоки `template` в задании Nomad.&lt;/p&gt;
&lt;h4&gt;Статическая конфигурация&lt;/h4&gt;
&lt;p&gt;Добавим точку входа `websecure` на порту 8443 и настроим автоматический редирект с HTTP на HTTPS:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;[entryPoints]
  [entryPoints.web]
    address = &amp;quot;:8080&amp;quot;
    [entryPoints.web.http.redirections.entryPoint]
      to = &amp;quot;websecure&amp;quot;
      scheme = &amp;quot;https&amp;quot;
      permanent = true

  [entryPoints.websecure]
    address = &amp;quot;:8443&amp;quot;

  [entryPoints.traefik]
    address = &amp;quot;:8081&amp;quot;

[api]
  dashboard = true
  insecure  = true

[providers.consulCatalog]
  prefix           = &amp;quot;traefik&amp;quot;
  exposedByDefault = false

  [providers.consulCatalog.endpoint]
    address = &amp;quot;127.0.0.1:8500&amp;quot;
    scheme  = &amp;quot;http&amp;quot;

[providers.file]
  filename = &amp;quot;${NOMAD_TASK_DIR}/dynamic.toml&amp;quot;
  watch    = true&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Ключевые моменты:&lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;`websecure` на порту `8443` — точка входа для HTTPS-трафика.&lt;/li&gt;
&lt;li&gt;`[entryPoints.web.http.redirections.entryPoint]` — HTTP-запросы автоматически перенаправляются на HTTPS.&lt;/li&gt;
&lt;li&gt;`[providers.file]` — подключает динамический провайдер, который читает наш файл с сертификатами.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Динамическая конфигурация&lt;/h4&gt;
&lt;p&gt;Здесь мы описываем сам сертификат:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;[tls]
  [tls.stores]
    [tls.stores.default]
      [tls.stores.default.defaultCertificate]
        certFile = &amp;quot;/Users/yuriygavrilov/marimo-app/certs/marimo.localhost.crt&amp;quot;
        keyFile  = &amp;quot;/Users/yuriygavrilov/marimo-app/certs/marimo.localhost.key&amp;quot;

  [[tls.certificates]]
    certFile = &amp;quot;/Users/yuriygavrilov/marimo-app/certs/marimo.localhost.crt&amp;quot;
    keyFile  = &amp;quot;/Users/yuriygavrilov/marimo-app/certs/marimo.localhost.key&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Обратите внимание, что здесь используются &lt;b&gt;абсолютные пути&lt;/b&gt;. Это работает, потому что мы запускаем Traefik через `raw_exec` — процесс работает прямо на хосте и имеет доступ ко всем файлам.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Шаг 3: Полное задание Traefik&lt;/h3&gt;
&lt;p&gt;Вот итоговый файл `traefik.nomad` с обеими конфигурациями:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;job &amp;quot;traefik&amp;quot; {
  region      = &amp;quot;global&amp;quot;
  datacenters = [&amp;quot;dc1&amp;quot;]
  type        = &amp;quot;service&amp;quot;

  group &amp;quot;traefik&amp;quot; {
    count = 1

    network {
      port &amp;quot;http&amp;quot; {
        static = 8080
      }
      port &amp;quot;api&amp;quot; {
        static = 8081
      }
    }

    service {
      name = &amp;quot;traefik&amp;quot;
      check {
        name     = &amp;quot;alive&amp;quot;
        type     = &amp;quot;tcp&amp;quot;
        port     = &amp;quot;http&amp;quot;
        interval = &amp;quot;10s&amp;quot;
        timeout  = &amp;quot;2s&amp;quot;
      }
    }

    task &amp;quot;traefik&amp;quot; {
      driver = &amp;quot;raw_exec&amp;quot;

      # Статическая конфигурация Traefik
      template {
        data = &amp;lt;&amp;lt;EOF
[entryPoints]
  [entryPoints.web]
    address = &amp;quot;:8080&amp;quot;
    [entryPoints.web.http.redirections.entryPoint]
      to = &amp;quot;websecure&amp;quot;
      scheme = &amp;quot;https&amp;quot;
      permanent = true

  [entryPoints.websecure]
    address = &amp;quot;:8443&amp;quot;

  [entryPoints.traefik]
    address = &amp;quot;:8081&amp;quot;

[api]
  dashboard = true
  insecure  = true

[providers.consulCatalog]
  prefix           = &amp;quot;traefik&amp;quot;
  exposedByDefault = false

  [providers.consulCatalog.endpoint]
    address = &amp;quot;127.0.0.1:8500&amp;quot;
    scheme  = &amp;quot;http&amp;quot;

[providers.file]
  filename = &amp;quot;${NOMAD_TASK_DIR}/dynamic.toml&amp;quot;
  watch    = true
EOF
        destination = &amp;quot;local/traefik.toml&amp;quot;
      }

      # Динамическая конфигурация: сертификаты по абсолютному пути
      template {
        data = &amp;lt;&amp;lt;EOF
[tls]
  [tls.stores]
    [tls.stores.default]
      [tls.stores.default.defaultCertificate]
        certFile = &amp;quot;/Users/yuriygavrilov/marimo-app/certs/marimo.localhost.crt&amp;quot;
        keyFile  = &amp;quot;/Users/yuriygavrilov/marimo-app/certs/marimo.localhost.key&amp;quot;

  [[tls.certificates]]
    certFile = &amp;quot;/Users/yuriygavrilov/marimo-app/certs/marimo.localhost.crt&amp;quot;
    keyFile  = &amp;quot;/Users/yuriygavrilov/marimo-app/certs/marimo.localhost.key&amp;quot;
EOF
        destination = &amp;quot;local/dynamic.toml&amp;quot;
      }

      config {
        command = &amp;quot;/usr/local/bin/traefik&amp;quot;
        args = [
          &amp;quot;--configfile&amp;quot;,
          &amp;quot;${NOMAD_TASK_DIR}/traefik.toml&amp;quot;
        ]
      }

      resources {
        cpu    = 100
        memory = 128
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;hr /&gt;
&lt;h3&gt;Шаг 4: Обновление тегов Marimo&lt;/h3&gt;
&lt;p&gt;В задании `marimo-job.nomad` нужно переключить сервис на точку входа `websecure` и включить TLS:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;service {
  name = &amp;quot;marimo-app&amp;quot;
  port = &amp;quot;http&amp;quot;
  tags = [
    &amp;quot;traefik.enable=true&amp;quot;,
    &amp;quot;traefik.http.routers.marimo.rule=Host(`marimo.localhost`)&amp;quot;,
    &amp;quot;traefik.http.routers.marimo.entrypoints=websecure&amp;quot;,
    &amp;quot;traefik.http.routers.marimo.tls=true&amp;quot;,
    &amp;quot;traefik.http.services.marimo-app.loadbalancer.sticky.cookie=true&amp;quot;,
  ]
  check {
    type     = &amp;quot;http&amp;quot;
    path     = &amp;quot;/&amp;quot;
    interval = &amp;quot;2s&amp;quot;
    timeout  = &amp;quot;2s&amp;quot;
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Что изменилось:&lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;`entrypoints=websecure` — трафик направляется на HTTPS-точку входа.&lt;/li&gt;
&lt;li&gt;`tls=true` — включена TLS-терминация для этого маршрута.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Шаг 5: Запуск и проверка&lt;/h3&gt;
&lt;p&gt;Перезапустите оба задания:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;nomad job stop traefik
nomad job run traefik.nomad
nomad job run marimo-job.nomad&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Откройте в браузере:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;https://marimo.localhost:8443&lt;/code&gt;&lt;/pre&gt;&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2026-09-28-v-23.38.21.png" width="1176" height="1114" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Браузер покажет предупреждение о недоверенном сертификате. Нажмите «Дополнительно» → «Перейти на marimo.localhost (небезопасно)» или добавьте сертификат в системный раздел доверенных сертов (в macOS это делается через «Связка ключей»).&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2026-09-28-v-23.49.01.png" width="1154" height="736" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Если вы попробуете открыть `&lt;a href="http://marimo.localhost:8080"&gt;http://marimo.localhost:8080&lt;/a&gt;`, Traefik автоматически перенаправит вас на HTTPS.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Возможные ошибки и их решения&lt;/h3&gt;
&lt;h4&gt;Ошибка: `Unclosed configuration block`&lt;/h4&gt;
&lt;p&gt;&lt;b&gt;Причина:&lt;/b&gt; незакрытые блоки `{ ... }` в HCL-файле.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Решение:&lt;/b&gt; внимательно проверьте, что все блоки `job`, `group`, `task`, `config`, `template` закрыты. В HCL отступы не важны, но фигурные скобки — критичны.&lt;/p&gt;
&lt;h4&gt;Ошибка: `Missing required argument: “command”` при `raw_exec`&lt;/h4&gt;
&lt;p&gt;&lt;b&gt;Причина:&lt;/b&gt; конфигурация скопирована из задания для `docker` (с полями `image`, `volumes`, `network_mode`), но применён драйвер `raw_exec`.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Решение:&lt;/b&gt; для `raw_exec` используйте поля `command` и `args`. Файлы конфигурации создавайте через `template`.&lt;/p&gt;
&lt;h4&gt;Ошибка: `volume_mount` не поддерживается драйвером `raw_exec`&lt;/h4&gt;
&lt;p&gt;&lt;b&gt;Причина:&lt;/b&gt; драйвер `raw_exec` работает напрямую с процессами хоста и не поддерживает монтирование томов.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Решение:&lt;/b&gt; используйте &lt;b&gt;абсолютные пути&lt;/b&gt; к файлам (как мы сделали в динамической конфигурации). Или переходите на драйвер `docker`/`podman` — там `volume_mount` работает штатно.&lt;/p&gt;
&lt;h4&gt;Ошибка: `entryPoint “websecure” doesn’t exist`&lt;/h4&gt;
&lt;p&gt;&lt;b&gt;Причина:&lt;/b&gt; имя точки входа в тегах сервиса не совпадает с именем в статической конфигурации Traefik.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Решение:&lt;/b&gt; убедитесь, что в конфиге есть блок `[entryPoints.websecure]`, а в тегах указано `traefik.http.routers.marimo.entrypoints=websecure`.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Итог&lt;/h3&gt;
&lt;p&gt;Мы добавили HTTPS в наш стек. Теперь архитектура выглядит так:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;Пользователь (HTTPS)
     │
     ▼
  Traefik  ──►  Marimo 1 / 2 / 3
(8443)              │
     │              ▼
     └─────────► Consul
                    ▲
                    │
             Nomad Cluster&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;b&gt;HTTP-трафик&lt;/b&gt; на порту 8080 автоматически перенаправляется на HTTPS.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HTTPS-трафик&lt;/b&gt; на порту 8443 расшифровывается Traefik и направляется на реплики Marimo.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Сертификаты&lt;/b&gt; хранятся на хосте и подключаются через абсолютные пути.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;В продакшене достаточно заменить пути к сертификатам и указать Traefik получать их автоматически от Let’s Encrypt — структура конфигурации останется той же. Мы разберём это в следующей части.&lt;/p&gt;
</description>
</item>

<item>
<title>Часть 2: Балансировка нагрузки и сервис-дискавери с Consul и Traefik для Marimo</title>
<guid isPermaLink="false">357</guid>
<link>https://gavrilov.info/all/chast-2-balansirovka-nagruzki-i-servis-diskaveri-s-consul-i-trae/</link>
<pubDate>Mon, 28 Sep 2026 23:37:19 +0300</pubDate>
<author></author>
<comments>https://gavrilov.info/all/chast-2-balansirovka-nagruzki-i-servis-diskaveri-s-consul-i-trae/</comments>
<description>
&lt;p&gt;В первой части мы развернули приложение Marimo через Nomad, используя драйвер `raw_exec` и динамические порты. Приложение работало, но у него был существенный недостаток: каждая реплика была доступна по отдельному порту, а пользователю приходилось вручную искать актуальный адрес. Кроме того, отсутствовала отказоустойчивость — при падении одной реплики трафик не перераспределялся.&lt;/p&gt;
&lt;p&gt;Первая часть тут &lt;a href="https://gavrilov.info/all/hashicorp-nomad-razvertyvanie-interaktivnogo-python-prilozheniya/"&gt;HashiCorp Nomad: развертывание интерактивного Python-приложения на macOS&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;В этой статье мы решим эти проблемы с помощью &lt;b&gt;Consul&lt;/b&gt; (сервис-дискавери) и &lt;b&gt;Traefik&lt;/b&gt; (балансировщик нагрузки). Мы также настроим &lt;b&gt;липкие сессии&lt;/b&gt; (sticky sessions), которые критически важны для stateful-приложений, таких как Marimo.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2026-09-28-v-22.55.24.png" width="444" height="160" alt="" /&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h3&gt;Зачем нужны Consul и Traefik?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Consul&lt;/b&gt; — распределённый каталог сервисов. Он хранит информацию о работающих репликах, проверяет их здоровье и предоставляет DNS-интерфейс. Без него балансировщик не знал бы, куда направлять трафик.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Traefik&lt;/b&gt; — современный обратный прокси и балансировщик нагрузки. Он автоматически обнаруживает сервисы через Consul Catalog Provider и распределяет запросы между здоровыми репликами.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Nomad&lt;/b&gt; — оркестратор, который управляет всеми компонентами.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Итоговая архитектура:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;Пользователь
     │
     ▼
  Traefik  ──►  Marimo 1 / 2 / 3
(балансировщик)      │
     │               ▼
     └──────────► Consul
                     ▲
                     │
              Nomad Cluster&lt;/code&gt;&lt;/pre&gt;&lt;hr /&gt;
&lt;h3&gt;Предварительные требования&lt;/h3&gt;
&lt;p&gt;Убедитесь, что у вас есть:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;&lt;b&gt;Работающий Nomad&lt;/b&gt; в режиме разработки (`nomad agent -dev -bind=0.0.0.0`).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Проект `~/marimo-app`&lt;/b&gt; с файлами `app.py`, `pyproject.toml`, `uv.lock` и виртуальным окружением.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Установленный Consul&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Установленный Traefik&lt;/b&gt; (мы будем запускать его через Nomad, но бинарник должен быть доступен).&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Установка Consul&lt;/h4&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;brew tap hashicorp/tap
brew install hashicorp/tap/consul&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;Установка Traefik&lt;/h4&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;brew install traefik&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Проверьте пути:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;which consul   # /usr/local/bin/consul
which traefik  # /usr/local/bin/traefik&lt;/code&gt;&lt;/pre&gt;&lt;hr /&gt;
&lt;h3&gt;Шаг 1: Запуск Consul в режиме разработки&lt;/h3&gt;
&lt;p&gt;Откройте отдельный терминал и запустите Consul:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;consul agent -dev -bind=0.0.0.0 -client=0.0.0.0&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Consul будет доступен по адресу `&lt;a href="http://127.0.0.1:8500"&gt;http://127.0.0.1:8500&lt;/a&gt;`. Оставьте терминал открытым.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Важно:&lt;/b&gt; Consul должен быть запущен &lt;b&gt;до&lt;/b&gt; того, как вы запустите Nomad с интеграцией с Consul, иначе сервисы не зарегистрируются.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Шаг 2: Обновление задания Marimo&lt;/h3&gt;
&lt;p&gt;Мы добавим в задание блок `service`, который зарегистрирует приложение в Consul и снабдит его тегами для Traefik. Также включим &lt;b&gt;липкие сессии&lt;/b&gt;, чтобы запросы одного пользователя всегда попадали на одну и ту же реплику.&lt;/p&gt;
&lt;p&gt;Создайте файл `marimo-job.nomad`:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;job &amp;quot;marimo-app&amp;quot; {
  datacenters = [&amp;quot;dc1&amp;quot;]
  type = &amp;quot;service&amp;quot;

  group &amp;quot;app&amp;quot; {
    count = 3

    network {
      port &amp;quot;http&amp;quot; {}
    }

    service {
      name = &amp;quot;marimo-app&amp;quot;
      port = &amp;quot;http&amp;quot;
      tags = [
        &amp;quot;traefik.enable=true&amp;quot;,
        &amp;quot;traefik.http.routers.marimo.rule=Host(`marimo.localhost`)&amp;quot;,
        &amp;quot;traefik.http.routers.marimo.entrypoints=web&amp;quot;,
        &amp;quot;traefik.http.services.marimo-app.loadbalancer.sticky.cookie=true&amp;quot;,
      ]
      check {
        type     = &amp;quot;http&amp;quot;
        path     = &amp;quot;/&amp;quot;
        interval = &amp;quot;2s&amp;quot;
        timeout  = &amp;quot;2s&amp;quot;
      }
    }

    task &amp;quot;marimo&amp;quot; {
      driver = &amp;quot;raw_exec&amp;quot;

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

      resources {
        cpu    = 500
        memory = 256
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Что изменилось:&lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;`count = 3` — запускаем три реплики.&lt;/li&gt;
&lt;li&gt;Блок `service` — регистрирует сервис в Consul.&lt;/li&gt;
&lt;li&gt;Теги Traefik:
&lt;ul&gt;
  &lt;li&gt;`traefik.enable=true` — разрешает Traefik обслуживать сервис.&lt;/li&gt;
  &lt;li&gt;`traefik.http.routers.marimo.rule=Host(\`marimo.localhost\`)` — правило маршрутизации по домену.&lt;/li&gt;
  &lt;li&gt;`traefik.http.routers.marimo.entrypoints=web` — точка входа (должна совпадать с именем в конфиге Traefik).&lt;/li&gt;
  &lt;li&gt;`traefik.http.services.marimo-app.loadbalancer.sticky.cookie=true` — включает липкие сессии на основе cookie.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;b&gt;Примечание про домен `.localhost`&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;В примере используется домен `marimo.localhost`. &lt;b&gt;Добавлять его в `/etc/hosts` не нужно&lt;/b&gt; — имена, заканчивающиеся на `.localhost`, автоматически резолвятся в `127.0.0.1` согласно стандарту &lt;b&gt;RFC 6761&lt;/b&gt; (раздел 6.3) и &lt;b&gt;RFC 6762&lt;/b&gt;. Это поведение поддерживается:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;системным резолвером macOS (`mDNSResponder`),&lt;/li&gt;
&lt;li&gt;всеми современными браузерами (Chrome, Safari, Firefox),&lt;/li&gt;
&lt;li&gt;а также большинством инструментов командной строки (`curl`, `ping` и т.д.).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Именно поэтому `&lt;a href="http://marimo.localhost:8080"&gt;http://marimo.localhost:8080&lt;/a&gt;` будет работать «из коробки» без правки системных файлов.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Когда `/etc/hosts` всё-таки понадобится:&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;Если вы хотите использовать другое имя — например, `marimo.dev`, `marimo.internal` или реальный домен вроде `marimo.example.com`, — тогда запись нужна:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;echo &amp;quot;127.0.0.1 marimo.dev&amp;quot; | sudo tee -a /etc/hosts&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Для локальной разработки и тестирования домен `.localhost` — самый удобный вариант: он не требует никаких дополнительных настроек и работает одинаково на любой машине.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Шаг 3: Создание задания для Traefik&lt;/h3&gt;
&lt;p&gt;Traefik будет запущен через `raw_exec`. Конфигурационный файл создаётся динамически с помощью шаблона Nomad.&lt;/p&gt;
&lt;p&gt;Создайте файл `traefik.nomad`:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;job &amp;quot;traefik&amp;quot; {
  region      = &amp;quot;global&amp;quot;
  datacenters = [&amp;quot;dc1&amp;quot;]
  type        = &amp;quot;service&amp;quot;

  group &amp;quot;traefik&amp;quot; {
    count = 1

    network {
      port &amp;quot;http&amp;quot; {
        static = 8080
      }
      port &amp;quot;api&amp;quot; {
        static = 8081
      }
    }

    service {
      name = &amp;quot;traefik&amp;quot;
      check {
        name     = &amp;quot;alive&amp;quot;
        type     = &amp;quot;tcp&amp;quot;
        port     = &amp;quot;http&amp;quot;
        interval = &amp;quot;10s&amp;quot;
        timeout  = &amp;quot;2s&amp;quot;
      }
    }

    task &amp;quot;traefik&amp;quot; {
      driver = &amp;quot;raw_exec&amp;quot;

      # Создаём конфигурационный файл Traefik через шаблон
      template {
        data = &amp;lt;&amp;lt;EOF
[entryPoints]
  [entryPoints.web]
    address = &amp;quot;:8080&amp;quot;
  [entryPoints.traefik]
    address = &amp;quot;:8081&amp;quot;

[api]
  dashboard = true
  insecure  = true

# Включаем Consul Catalog Provider
[providers.consulCatalog]
  prefix           = &amp;quot;traefik&amp;quot;
  exposedByDefault = false

  [providers.consulCatalog.endpoint]
    address = &amp;quot;127.0.0.1:8500&amp;quot;
    scheme  = &amp;quot;http&amp;quot;
EOF
        destination = &amp;quot;local/traefik.toml&amp;quot;
      }

      config {
        command = &amp;quot;/usr/local/bin/traefik&amp;quot;
        args = [
          &amp;quot;--configfile&amp;quot;,
          &amp;quot;${NOMAD_TASK_DIR}/traefik.toml&amp;quot;
        ]
      }

      resources {
        cpu    = 100
        memory = 128
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Ключевые моменты:&lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;`driver = “raw_exec”` — запускаем бинарник напрямую.&lt;/li&gt;
&lt;li&gt;`template` — создаёт файл `traefik.toml` в директории задачи.&lt;/li&gt;
&lt;li&gt;`command` — полный путь к Traefik.&lt;/li&gt;
&lt;li&gt;`args` — указываем путь к созданному конфигу через переменную `${NOMAD_TASK_DIR}`.&lt;/li&gt;
&lt;li&gt;В конфиге Traefik точка входа называется `web` (порт 8080). Это имя должно совпадать с тегом `traefik.http.routers.marimo.entrypoints=web`.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Шаг 4: Запуск всех компонентов&lt;/h3&gt;
&lt;p&gt;Убедитесь, что Consul и Nomad запущены в отдельных терминалах.&lt;/p&gt;
&lt;p&gt;Затем в терминале, где вы работаете с заданиями:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;cd ~/marimo-app

# Запускаем приложение
nomad job run marimo-job.nomad

# Запускаем Traefik
nomad job run traefik.nomad&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Проверьте статус:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;nomad job status marimo-app
nomad job status traefik&lt;/code&gt;&lt;/pre&gt;&lt;hr /&gt;
&lt;h3&gt;Шаг 5: Проверка работы&lt;/h3&gt;
&lt;ol start="1"&gt;
&lt;li&gt;&lt;b&gt;Consul&lt;/b&gt;: откройте `&lt;a href="http://127.0.0.1:8500"&gt;http://127.0.0.1:8500&lt;/a&gt;` и убедитесь, что сервис `marimo-app` зарегистрирован.&lt;/li&gt;
&lt;/ol&gt;
&lt;ol start="2"&gt;
&lt;li&gt;&lt;b&gt;Traefik Dashboard&lt;/b&gt;: откройте `&lt;a href="http://127.0.0.1:8081/dashboard/"&gt;http://127.0.0.1:8081/dashboard/&lt;/a&gt;`. В разделе &lt;b&gt;HTTP Routers&lt;/b&gt; должен быть маршрут `marimo` со статусом &lt;b&gt;Enabled&lt;/b&gt;. В &lt;b&gt;HTTP Services&lt;/b&gt; — сервис `marimo-app` с тремя здоровыми репликами.&lt;/li&gt;
&lt;/ol&gt;
&lt;ol start="3"&gt;
&lt;li&gt;&lt;b&gt;Приложение&lt;/b&gt;: откройте `&lt;a href="http://marimo.localhost:8080"&gt;http://marimo.localhost:8080&lt;/a&gt;`. Вы должны увидеть интерфейс Marimo с полем ввода имени и диаграммой. Проверьте, что при нажатии кнопки данные обновляются, а ошибка `Invalid session id` не появляется.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;Возможные ошибки и их решения&lt;/h3&gt;
&lt;p&gt;В процессе настройки мы столкнулись с несколькими типичными проблемами. Вот они и способы их устранения.&lt;/p&gt;
&lt;h4&gt;Ошибка 1: `EntryPoint doesn’t exist entryPointName=web`&lt;/h4&gt;
&lt;p&gt;&lt;b&gt;Причина:&lt;/b&gt; в тегах сервиса указана точка входа `web`, а в конфигурации Traefik она называется иначе (например, `http`).&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Решение:&lt;/b&gt; приведите имена к единому виду. В нашем случае мы переименовали точку входа в конфиге Traefik в `web`:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;[entryPoints]
  [entryPoints.web]
    address = &amp;quot;:8080&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;И в тегах указали `traefik.http.routers.marimo.entrypoints=web`.&lt;/p&gt;
&lt;h4&gt;Ошибка 2: `Invalid session id: s_bpy8u7`&lt;/h4&gt;
&lt;p&gt;&lt;b&gt;Причина:&lt;/b&gt; отсутствие липких сессий. Marimo — stateful-приложение, оно хранит сессию на конкретной реплике. Если запросы распределяются случайно, браузер попадает на разные реплики, и сессия теряется.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Решение:&lt;/b&gt; добавьте тег `traefik.http.services.marimo-app.loadbalancer.sticky.cookie=true`. Traefik установит cookie и будет направлять все запросы от одного браузера на одну и ту же реплику.&lt;/p&gt;
&lt;h4&gt;Ошибка 3: `Missing required argument: “command”` при использовании `raw_exec`&lt;/h4&gt;
&lt;p&gt;&lt;b&gt;Причина:&lt;/b&gt; конфигурация написана для драйвера `docker` (с полями `image`, `volumes`, `network_mode`), но применён `raw_exec`.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Решение:&lt;/b&gt; для `raw_exec` используйте `command` и `args`. Файлы конфигурации создавайте через `template`. Пример для Traefik приведён выше.&lt;/p&gt;
&lt;h4&gt;Ошибка 4: `raw_exec` отключён&lt;/h4&gt;
&lt;p&gt;&lt;b&gt;Причина:&lt;/b&gt; в некоторых сборках Nomad драйвер `raw_exec` отключён по умолчанию.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Решение:&lt;/b&gt; добавьте в конфигурацию клиента Nomad (например, `/etc/nomad.d/nomad.hcl`):&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;plugin &amp;quot;raw_exec&amp;quot; {
  config {
    enabled = true
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;и перезапустите агент Nomad, но в моем случае это не понадобилось.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Итог&lt;/h3&gt;
&lt;p&gt;Мы добавили к нашему приложению Marimo два важных компонента:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Consul&lt;/b&gt; — для сервис-дискавери и проверки здоровья.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Traefik&lt;/b&gt; — для балансировки нагрузки и единой точки входа.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Теперь приложение доступно по адресу `&lt;a href="http://marimo.localhost:8080"&gt;http://marimo.localhost:8080&lt;/a&gt;`, а трафик автоматически распределяется между тремя репликами. Благодаря липким сессиям пользователи не теряют состояние при работе с интерактивным интерфейсом.&lt;/p&gt;
&lt;p&gt;Этот подход является основой для построения отказоустойчивых и масштабируемых систем. В следующих статьях мы рассмотрим настройку TLS/HTTPS, использование Vault для управления секретами и автоматическое масштабирование.&lt;/p&gt;
&lt;p&gt;Поздравляю! Теперь ваше приложение готово к более серьёзным нагрузкам. 🚀&lt;/p&gt;
</description>
</item>

<item>
<title>HashiCorp Nomad: развертывание интерактивного Python-приложения на macOS</title>
<guid isPermaLink="false">356</guid>
<link>https://gavrilov.info/all/hashicorp-nomad-razvertyvanie-interaktivnogo-python-prilozheniya/</link>
<pubDate>Sun, 27 Sep 2026 00:59:39 +0300</pubDate>
<author></author>
<comments>https://gavrilov.info/all/hashicorp-nomad-razvertyvanie-interaktivnogo-python-prilozheniya/</comments>
<description>
&lt;p&gt;В мире оркестрации контейнеров и приложений существует множество инструментов, но одним из самых гибких и простых в освоении является &lt;b&gt;HashiCorp Nomad&lt;/b&gt;. Это оркестратор рабочих нагрузок, который позволяет развертывать как контейнеризированные, так и обычные приложения с использованием единого декларативного подхода.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2026-09-26-v-21.45.50.png" width="286" height="94" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;В этой статье мы разберем, что такое Nomad, установим его на macOS, создадим простое интерактивное приложение на Marimo с полем ввода имени и генерацией случайных данных на столбчатой диаграмме, а затем развернем его с помощью Nomad, используя драйвер `raw_exec`. В конце обсудим ограничения такого подхода и путь к настоящему продакшену.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Что такое HashiCorp Nomad?&lt;/h3&gt;
&lt;p&gt;&lt;b&gt;Nomad&lt;/b&gt; — это гибкий оркестратор рабочих нагрузок, который позволяет развертывать и управлять любыми контейнеризированными или устаревшими приложениями с помощью единого рабочего процесса.&lt;/p&gt;
&lt;h4&gt;Ключевые особенности&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Универсальность&lt;/b&gt;: Nomad может запускать Docker-контейнеры, обычные приложения (без контейнеризации), микросервисы и пакетные задачи на одной инфраструктуре.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Простота&lt;/b&gt;: Nomad распространяется как единый бинарный файл, не требует внешних зависимостей для хранения или координации и автоматически обрабатывает сбои приложений, узлов и драйверов.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Масштабируемость&lt;/b&gt;: Nomad способен масштабироваться до 10 000+ узлов и имеет встроенную поддержку GPU-нагрузок.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Гибкость&lt;/b&gt;: Поддерживает различные типы задач: сервисы, пакетные задания и системные задачи. Работает на Linux, macOS и Windows.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Установка Nomad на macOS&lt;/h3&gt;
&lt;p&gt;Для установки Nomad на macOS удобнее всего использовать Homebrew. Выполните следующие команды в терминале:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;# Добавляем репозиторий HashiCorp
brew tap hashicorp/tap

# Устанавливаем Nomad
brew install hashicorp/tap/nomad&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;После установки проверьте версию:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;nomad version&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Пример вывода:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;Nomad v2.0.5
BuildDate 2026-08-12T18:22:41Z
Revision 5c8612bba6eb8e44cc3fc434125cb1b13463c309&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Важно&lt;/b&gt;: На Intel Mac (x86_64) могут возникнуть проблемы с загрузкой бинарников из-за прекращения поддержки Apple. В этом случае рекомендуется использовать VPN для доступа к ресурсам HashiCorp. Если Nomad не устанавливается через Homebrew, можно собрать его из исходников или использовать MacPorts.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Запуск Nomad в режиме разработки&lt;/h3&gt;
&lt;p&gt;Для тестирования и разработки Nomad поддерживает режим `-dev`, который запускает одноузловой кластер на вашем компьютере:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;nomad agent -dev -bind=0.0.0.0&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Эта команда запустит Nomad, и веб-интерфейс будет доступен по адресу `&lt;a href="http://127.0.0.1:4646"&gt;http://127.0.0.1:4646&lt;/a&gt;`. Оставьте этот терминал открытым — это ваш “сервер” Nomad.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Создание Marimo-приложения&lt;/h3&gt;
&lt;h4&gt;Установка `uv`&lt;/h4&gt;
&lt;p&gt;`uv` — это современный и быстрый менеджер пакетов для Python. Установите его:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;curl -LsSf https://astral.sh/uv/install.sh | sh&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;Создание проекта&lt;/h4&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;mkdir ~/marimo-app
cd ~/marimo-app
uv init
uv add marimo altair pandas numpy&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;Написание приложения&lt;/h4&gt;
&lt;p&gt;Создайте файл `app.py` со следующим содержимым. Приложение будет иметь поле для ввода имени, кнопку для генерации случайных значений и столбчатую диаграмму:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;import marimo

__generated_with = &amp;quot;0.11.0&amp;quot;
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=&amp;quot;Введите ваше имя...&amp;quot;,
        label=&amp;quot;Имя пользователя&amp;quot;
    )
    name_input
    return (name_input,)


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


@app.cell
def _(mo):
    # Кнопка для генерации данных
    generate_button = mo.ui.button(
        label=&amp;quot;🎲 Сгенерировать данные&amp;quot;,
        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 = [&amp;quot;Категория A&amp;quot;, &amp;quot;Категория B&amp;quot;, &amp;quot;Категория C&amp;quot;, &amp;quot;Категория D&amp;quot;, &amp;quot;Категория E&amp;quot;]
    values = np.random.randint(10, 100, size=5)
    
    df = pd.DataFrame({
        &amp;quot;Категория&amp;quot;: categories,
        &amp;quot;Значение&amp;quot;: values
    })
    
    chart = alt.Chart(df).mark_bar().encode(
        x=alt.X(&amp;quot;Категория&amp;quot;, sort=None),
        y=&amp;quot;Значение&amp;quot;,
        color=alt.value(&amp;quot;#4C78A8&amp;quot;)
    ).properties(
        title=&amp;quot;Случайные значения по категориям&amp;quot;,
        width=500,
        height=300
    )
    
    mo.ui.altair_chart(chart)
    return (chart,)


if __name__ == &amp;quot;__main__&amp;quot;:
    app.run()&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;Проверка локально&lt;/h4&gt;
&lt;p&gt;Запустите приложение в режиме редактирования:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;uv run marimo run app.py&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Убедитесь, что приложение работает: введите имя, нажмите кнопку — диаграмма должна обновиться. Закройте сервер (`Ctrl+C`).&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Создание задания для Nomad&lt;/h3&gt;
&lt;p&gt;Создайте файл `marimo-job.nomad` в папке проекта:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;job &amp;quot;marimo-app&amp;quot; {
  datacenters = [&amp;quot;dc1&amp;quot;]
  type = &amp;quot;service&amp;quot;

  group &amp;quot;app&amp;quot; {
    count = 2

    network {
      port &amp;quot;http&amp;quot; {}
    }

    task &amp;quot;marimo&amp;quot; {
      driver = &amp;quot;raw_exec&amp;quot;

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

      resources {
        cpu    = 500
        memory = 256
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Пояснения:&lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;`driver = “raw_exec”` — запускает процесс напрямую на хосте без изоляции.&lt;/li&gt;
&lt;li&gt;`count = 2` — запускаем две реплики приложения. Nomad автоматически распределит их по узлам (в нашем случае — по одному узлу, но на разных портах).&lt;/li&gt;
&lt;li&gt;`port “http” {}` — динамический порт. Nomad резервирует случайный свободный порт для каждой аллокации. Такой подход избавляет от конфликтов портов при запуске нескольких реплик.&lt;/li&gt;
&lt;li&gt;`command` — полный путь к исполняемому файлу `marimo` в виртуальном окружении.&lt;/li&gt;
&lt;li&gt;`${NOMAD_PORT_http}` — Nomad автоматически подставляет динамически выделенный порт через переменную окружения `NOMAD_PORT_&lt;label&gt;`.&lt;/li&gt;
&lt;li&gt;`work_dir` — рабочая директория, где находится `app.py`.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;b&gt;Важно&lt;/b&gt;: замените `/Users/yuriygavrilov/` на реальный путь к вашей домашней директории.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Запуск задания&lt;/h3&gt;
&lt;p&gt;Убедитесь, что Nomad работает (в первом терминале). Во втором терминале выполните:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;nomad job run marimo-job.nomad&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Пример успешного вывода:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;==&amp;gt; 2026-09-26T21:26:27+03:00: Monitoring evaluation &amp;quot;cba695bd&amp;quot;
    2026-09-26T21:26:27+03:00: Allocation &amp;quot;2327286c&amp;quot; created: node &amp;quot;7f836ccd&amp;quot;, group &amp;quot;app&amp;quot;
==&amp;gt; 2026-09-26T21:26:27+03:00: Monitoring deployment &amp;quot;9ea4ff53&amp;quot;
  ✓ Deployment &amp;quot;9ea4ff53&amp;quot; successful
    
    Deployed
    Task Group  Desired  Placed  Healthy  Unhealthy  Progress Deadline
    app         1        1       1        0&lt;/code&gt;&lt;/pre&gt;&lt;hr /&gt;
&lt;h3&gt;Как узнать порт приложения&lt;/h3&gt;
&lt;p&gt;После запуска задания выполните:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;nomad job status marimo-app&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Вы увидите таблицу аллокаций с их ID. Для каждой аллокации выполните:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;nomad alloc status &amp;lt;ID_аллокации&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;В выводе найдите секцию `Allocation Addresses`:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;Allocation Addresses:
Label  Dynamic  Address
*http  yes      127.0.0.1:20938&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Откройте в браузере `&lt;a href="http://127.0.0.1:20938"&gt;http://127.0.0.1:20938&lt;/a&gt;` — ваше приложение работает.&lt;/p&gt;
&lt;p&gt;Повторите команду для остальных аллокаций, чтобы получить порты всех реплик.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2026-09-26-v-21.48.33.png" width="1172" height="1080" alt="" /&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h3&gt;Ограничения текущего подхода&lt;/h3&gt;
&lt;p&gt;Мы успешно запустили приложение, но важно понимать, что &lt;b&gt;это ещё не полноценное масштабированное решение&lt;/b&gt;. Вот почему:&lt;/p&gt;
&lt;h4&gt;1. Нет балансировщика нагрузки&lt;/h4&gt;
&lt;p&gt;У нас работают &lt;b&gt;две реплики&lt;/b&gt; на разных портах (`20938`, `21001` и т.д.). Чтобы обратиться к приложению, пользователь должен знать конкретный порт каждой реплики. Это неудобно:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Нельзя дать пользователям один адрес, например `&lt;a href="http://marimo.mycompany.com"&gt;http://marimo.mycompany.com&lt;/a&gt;`.&lt;/li&gt;
&lt;li&gt;Нет автоматического распределения трафика между репликами.&lt;/li&gt;
&lt;li&gt;Нет отказоустойчивости: если одна реплика упадёт, клиент, знающий только её порт, потеряет доступ.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. Нет сервис-дискавери&lt;/h4&gt;
&lt;p&gt;Nomad не регистрирует сервисы автоматически (для этого нужен &lt;b&gt;Consul&lt;/b&gt;). Без сервис-дискавери балансировщик не сможет узнать, какие реплики сейчас работают.&lt;/p&gt;
&lt;h4&gt;3. Минимальная изоляция&lt;/h4&gt;
&lt;p&gt;Драйвер `raw_exec` запускает процессы прямо на хосте. В продакшене это небезопасно: приложение имеет доступ ко всей файловой системе и может влиять на систему.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Путь к продакшену: что нужно добавить&lt;/h3&gt;
&lt;h4&gt;1. Балансировщик нагрузки (Traefik, NGINX, HAProxy)&lt;/h4&gt;
&lt;p&gt;&lt;b&gt;Traefik&lt;/b&gt; — самый простой вариант для интеграции с Nomad. Он умеет:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Автоматически обнаруживать сервисы через Consul.&lt;/li&gt;
&lt;li&gt;Распределять трафик между всеми здоровыми репликами.&lt;/li&gt;
&lt;li&gt;Работать с TLS/HTTPS “из коробки”.&lt;/li&gt;
&lt;li&gt;Иметь удобную панель управления.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;b&gt;Как это выглядит в задании&lt;/b&gt;: вы добавляете специальные теги в блок `service` вашего приложения, и Traefik сам настраивает маршрутизацию:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;service {
  name = &amp;quot;marimo-app&amp;quot;
  port = &amp;quot;http&amp;quot;
  tags = [
    &amp;quot;traefik.enable=true&amp;quot;,
    &amp;quot;traefik.http.routers.marimo.rule=Host(`marimo.localhost`)&amp;quot;,
  ]
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Затем запускаете Traefik как отдельную задачу в Nomad, и он становится единой точкой входа для всех пользователей.&lt;/p&gt;
&lt;h4&gt;2. Сервис-дискавери (Consul)&lt;/h4&gt;
&lt;p&gt;&lt;b&gt;Consul&lt;/b&gt; — это распределённый каталог сервисов. Он:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Хранит информацию о том, какие реплики сейчас работают и на каких портах.&lt;/li&gt;
&lt;li&gt;Предоставляет DNS-интерфейс: ваше приложение доступно как `marimo-app.service.consul`.&lt;/li&gt;
&lt;li&gt;Позволяет балансировщику динамически обновлять список бэкендов.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. Контейнеризация (Docker/Podman)&lt;/h4&gt;
&lt;p&gt;В продакшене используйте &lt;b&gt;контейнерные драйверы&lt;/b&gt; вместо `raw_exec`:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Изоляция&lt;/b&gt;: приложение работает в своём окружении, не влияя на хост.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Воспроизводимость&lt;/b&gt;: одинаковое поведение на любой машине.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Безопасность&lt;/b&gt;: ограничения по ресурсам и правам доступа.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4. Полноценный кластер Nomad&lt;/h4&gt;
&lt;p&gt;В продакшене Nomad работает не как один процесс, а как &lt;b&gt;кластер&lt;/b&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;3 серверных узла&lt;/b&gt; (минимум) — хранят состояние и принимают решения о планировании.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;N клиентских узлов&lt;/b&gt; — выполняют задачи.&lt;/li&gt;
&lt;li&gt;Интеграция с &lt;b&gt;Vault&lt;/b&gt; для управления секретами.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Итоговая архитектура для продакшена&lt;/h3&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;Пользователь
     │
     ▼
  Traefik  ──►  Marimo 1 / 2 / 3
(балансировщик)      │
                     ▼
                  Consul
                     ▲
                     │
              Nomad Cluster&lt;/code&gt;&lt;/pre&gt;&lt;hr /&gt;
&lt;h3&gt;Заключение&lt;/h3&gt;
&lt;p&gt;Мы рассмотрели базовый процесс развертывания Python-приложения с помощью HashiCorp Nomad:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;&lt;b&gt;Установили Nomad&lt;/b&gt; через Homebrew.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Запустили Nomad&lt;/b&gt; в режиме разработки.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Создали Marimo-приложение&lt;/b&gt; с полем ввода имени и интерактивной диаграммой.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Написали задание Nomad&lt;/b&gt; с драйвером `raw_exec` и динамическим портом.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Запустили две реплики&lt;/b&gt; приложения и научились находить их порты.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Этот подход отлично работает для &lt;b&gt;разработки и тестирования&lt;/b&gt;. Но для настоящего продакшена необходимо:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Добавить &lt;b&gt;балансировщик нагрузки&lt;/b&gt; (Traefik, NGINX, HAProxy), чтобы пользователи обращались к одному адресу.&lt;/li&gt;
&lt;li&gt;Внедрить &lt;b&gt;сервис-дискавери&lt;/b&gt; (Consul), чтобы балансировщик знал о работающих репликах.&lt;/li&gt;
&lt;li&gt;Использовать &lt;b&gt;контейнерные драйверы&lt;/b&gt; (Docker, Podman) для изоляции.&lt;/li&gt;
&lt;li&gt;Построить &lt;b&gt;полноценный кластер Nomad&lt;/b&gt; с тремя серверными узлами.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nomad — это мощный и гибкий инструмент, который подходит как для простых задач, так и для сложных распределённых систем. Его простота и универсальность делают его отличным выбором для разработчиков, которые хотят быстро развертывать приложения без излишней сложности Kubernetes.&lt;/p&gt;
&lt;p&gt;Следующий логичный шаг — добавить Traefik и Consul, чтобы превратить наш прототип в отказоустойчивое и масштабируемое решение. Об этом — в следующих статьях.&lt;/p&gt;
</description>
</item>

<item>
<title>Свой Heroku: запустил Dokku дома и накатил новогоднего :)</title>
<guid isPermaLink="false">307</guid>
<link>https://gavrilov.info/all/svoy-heroku-doma-zapustil-dokku-doma-i-nakatil-novogodnego/</link>
<pubDate>Thu, 01 Jan 2026 22:59:46 +0300</pubDate>
<author></author>
<comments>https://gavrilov.info/all/svoy-heroku-doma-zapustil-dokku-doma-i-nakatil-novogodnego/</comments>
<description>
&lt;p&gt;Мы привыкли, что для развертывания веб-приложений нужно платить за VPS или разбираться в дебрях Kubernetes. Но если у вас есть домашний сервер (в моем случае — Asustor), вы можете создать свою собственную PaaS-платформу (Platform as a Service), которая работает по принципу *“git push — и готово”*.&lt;/p&gt;
&lt;p&gt;Сегодня я расскажу, как настроить &lt;b&gt;Dokku&lt;/b&gt; через Portainer, запустить веселое Python-приложение к Новому 2026 году и поделюсь лайфхаками по оптимизации сборки и масштабированию.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2026-01-01-v-22.53.49.png" width="2546" height="936" alt="" /&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h3&gt;Часть 1. Фундамент: Запуск Dokku в Portainer&lt;/h3&gt;
&lt;p&gt;Dokku — это Docker-контейнер, который управляет другими Docker-контейнерами. Чтобы он заработал на NAS, его нужно правильно запустить. Я использовал Portainer Stack.&lt;/p&gt;
&lt;h4&gt;Docker Compose конфигурация&lt;/h4&gt;
&lt;p&gt;Вот рабочий `docker-compose.yml`, который решает главную проблему — доступность приложений из локальной сети.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;# version: '3.2'
services:
  agent:
    image: dokku/dokku:${VERSION}
    pid: host.  # ⚠️ важно для работы на NAS
    network_mode: bridge # ⚠️ важно для работы на NAS
    environment:
      DOKKU_HOSTNAME: ${DOKKU_HOSTNAME}
      DOKKU_HOST_ROOT: ${DOKKU_HOST_ROOT}
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ${VOLUME_PATH:-/var/lib/dokku}:/mnt/dokku
    ports:
      - &amp;quot;3022:22&amp;quot; # ⚠️ важно для работы через ssh и что бы не конфликтовал с 22 
      - &amp;quot;80:80&amp;quot;    # Внешний порт 80 -&amp;gt; порт 80 внутри контейнера Dokku
      - &amp;quot;443:443&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Нюансы сети:&lt;/b&gt;&lt;br /&gt;
Чтобы обращаться к приложениям по красивым домегам типа `my-app.dokku.datahub.mother`, я настроил &lt;b&gt;AdGuard Home&lt;/b&gt; в качестве локального DNS-сервера (фильтры получил бонусом – где-то 20% это всякие счетчики, ужас:). Добавил правило Rewrite: `*.dokku.datahub.mother` → `192.168.0.20` (IP моего NAS). Теперь все поддомены (приложения) автоматически ведут на Dokku.&lt;/p&gt;
&lt;p&gt;Также для удобства я настроил `~/.ssh/config` на ноутбуке, чтобы не вводить порты вручную:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;Host dokku.datahub.mother
  HostName 192.168.0.20
  Port 3022
  User dokku&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;ну и ключ сам так можно добавить&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;echo &amp;quot;ВАШ_ПУБЛИЧНЫЙ_КЛЮЧ&amp;quot; | dokku ssh-keys:add dokku&lt;/code&gt;&lt;/pre&gt;&lt;hr /&gt;
&lt;h3&gt;Часть 2. Приложение “my-first-app”: Новогоднее гадание&lt;/h3&gt;
&lt;p&gt;Для теста я написал простое Flask-приложение, которое рассчитывает ваш возраст в наступающем 2026 году.&lt;/p&gt;
&lt;h4&gt;Код приложения (`app.py`)&lt;/h4&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;from flask import Flask, request

app = Flask(__name__)

@app.route('/')
def home():
    return &amp;quot;&amp;quot;&amp;quot;
    &amp;lt;h1&amp;gt;Приветствую в игре Нового 2026 года! 🎉&amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;Это веселая интерактивная игра в честь Нового года. Угадай свой возраст на 1 января 2026!&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;Введи свой год рождения:&amp;lt;/p&amp;gt;
    &amp;lt;form action=&amp;quot;/result&amp;quot; method=&amp;quot;get&amp;quot;&amp;gt;
        &amp;lt;input type=&amp;quot;number&amp;quot; name=&amp;quot;birth_year&amp;quot; min=&amp;quot;1900&amp;quot; max=&amp;quot;2025&amp;quot; required&amp;gt;
        &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Угадать возраст на Новогодний 2026!&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
    &amp;quot;&amp;quot;&amp;quot;

@app.route('/result')
def result():
    birth_year = request.args.get('birth_year')
    if not birth_year or not birth_year.isdigit():
        return &amp;quot;Ошибка: введи корректный год рождения!&amp;quot;
    
    birth_year = int(birth_year)
    age_in_2026 = 2026 - birth_year
    return f&amp;quot;&amp;quot;&amp;quot;
    &amp;lt;h2&amp;gt;Результат: 🎇&amp;lt;/h2&amp;gt;
    &amp;lt;p&amp;gt;В 2026 году тебе будет {age_in_2026} лет!&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;Счастливого Нового 2026 года! Пусть все твои желания сбудутся!&amp;lt;/p&amp;gt;
    &amp;lt;a href=&amp;quot;/&amp;quot;&amp;gt;Играть снова&amp;lt;/a&amp;gt;
    &amp;quot;&amp;quot;&amp;quot;

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;Подготовка к деплою&lt;/h4&gt;
&lt;p&gt;Чтобы Dokku понял, как запускать это чудо, нужны два файла в корне проекта:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;&lt;b&gt;`requirements.txt`&lt;/b&gt; (зависимости):&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;Flask==2.3.2
gunicorn==20.1.0
Werkzeug==2.3.3&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;&lt;b&gt;`Procfile`&lt;/b&gt; (команда запуска):&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;web: gunicorn app:app --bind 0.0.0.0:5000&lt;/code&gt;&lt;/pre&gt;&lt;ol start="3"&gt;
&lt;li&gt;&lt;b&gt;`.python-version`&lt;/b&gt; (опционально, явная версия Python):&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;3.11.14&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;Процесс деплоя&lt;/h4&gt;
&lt;p&gt;Все делается через Git, как на “взрослых” платформах:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;&lt;b&gt;Создаем приложение на сервере:&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;ssh dokku@dokku.datahub.mother apps:create my-first-app&lt;/code&gt;&lt;/pre&gt;&lt;ol start="2"&gt;
&lt;li&gt;&lt;b&gt;Отправляем код:&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;git init
    git add .
    git commit -m &amp;quot;Happy New Year 2026 version&amp;quot;
    git remote add dokku dokku@dokku.datahub.mother:my-first-app
    git push dokku master&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Если вы до этого еще что-то деплоили, то лучше проверить куда гит смотрит&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;git remote -v&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;ну и поменяем еще не верное&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;git remote remove dokku&lt;/code&gt;&lt;/pre&gt;&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;git init
    git add .
    git commit -m &amp;quot;Happy New Year 2026 version&amp;quot;
    git remote add dokku dokku@dokku.datahub.mother:my-first-app
    git push dokku master&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;После пуша Dokku сам скачает Python, установит Flask и запустит Gunicorn. Через минуту-две приложение доступно по адресу `&lt;a href="http://my-first-app.dokku.datahub.mother"&gt;http://my-first-app.dokku.datahub.mother&lt;/a&gt;`.&lt;/p&gt;
&lt;p&gt;Еще нужно домен установить&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;ssh dokku@dokku.datahub.mother domains:set my-first-app my-first-app.dokku.datahub.mother&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;или можно сразу глобально его установить:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;ssh dokku@dokku.datahub.mother domains:set-global dokku.datahub.mother
# но тогда придется удалить ручную привязку 
ssh dokku@dokku.datahub.mother domains:clear my-first-app

# не забыть перебрать nginx ( на всякий случай ) 
ssh dokku@dokku.datahub.mother proxy:build-config my-first-app&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Сертификат сгенерировать&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;ssh dokku@dokku.datahub.mother certs:generate my-first-app my-first-app.dokku.datahub.mother&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;и проверить порты&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;ssh dokku@dokku.datahub.mother ports:report my-first-app&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;если порты не корректные, то можно их установить так:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;ssh dokku@dokku.datahub.mother ports:set my-first-app http:80:5000 https:443:5000&lt;/code&gt;&lt;/pre&gt;&lt;hr /&gt;
&lt;h3&gt;Часть 3. Уровень PRO: Скорость (uv) и Marimo&lt;/h3&gt;
&lt;p&gt;Аппетит приходит во время еды. После простого Flask-приложения я решил развернуть что-то посерьезнее — Data Science ноутбук на &lt;b&gt;Marimo&lt;/b&gt;, и столкнулся с реальными сложностями и особенностями. Для примера брал их дело ноутбук &lt;a href="https://marimo.app"&gt;https://marimo.app&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;1. Ускорение сборки с `uv`&lt;/h4&gt;
&lt;p&gt;Стандартный `pip` устанавливает пакеты медленно. Если проект большой, деплой может висеть минутами.&lt;br /&gt;
Я перешел на &lt;b&gt;uv&lt;/b&gt; — новый менеджер пакетов на Rust.&lt;/p&gt;
&lt;p&gt;Вместо `requirements.txt` я использовал `pyproject.toml` и `uv.lock`. Dokku (благодаря современным buildpacks) увидел `uv.lock` и переключился на быстрый режим. Время сборки сократилось &lt;b&gt;в разы&lt;/b&gt;.&lt;/p&gt;
&lt;h4&gt;2. Ловушка масштабирования (Scaling)&lt;/h4&gt;
&lt;p&gt;Marimo — это &lt;b&gt;stateful&lt;/b&gt; приложение (хранит состояние в памяти). Flask, который мы делали выше — &lt;b&gt;stateless&lt;/b&gt;.&lt;/p&gt;
&lt;p&gt;Когда я задеплоил Marimo, Dokku по умолчанию все было хорошо, но потом я решил масштабировать его и сделал так&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;ssh dokku@dokku.datahub.mother ps:scale my-marimo-app web=3&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;далее Dokku запустил &lt;b&gt;3 копии&lt;/b&gt; контейнера (`web=3`).&lt;br /&gt;
Начался хаос:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Интерфейс открывался.&lt;/li&gt;
&lt;li&gt;При нажатии кнопок вылетала ошибка `Invalid server token`.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;b&gt;Почему?&lt;/b&gt; Браузер загружал страницу с *Контейнера 1*, а WebSocket-запрос улетал в *Контейнер 2*, который ничего не знал про мою сессию.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Решение:&lt;/b&gt;&lt;br /&gt;
Для интерактивных приложений (Streamlit, Marimo, Jupyter) всегда принудительно ставьте одну реплику:&lt;br /&gt;
Ну ли придется делать липкие сессии на nginx или еще что-то.&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;ssh dokku.datahub.mother ps:scale my-marimo-app web=1 # все вернуло в рабочее состояние.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;А если не хватает мощности — лучше дайте этому единственному контейнеру больше ресурсов, чем пытаться плодить клонов или дайте каждому запускать свой:&lt;/p&gt;
&lt;p&gt;Вот так можно установить лимиты или повысить их:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;ssh dokku.datahub.mother resource:limit my-marimo-app --memory 2G --cpu 2&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;3. SSL в локальной сети&lt;/h4&gt;
&lt;p&gt;Браузеры блокируют микрофон и иногда WebSockets на HTTP-сайтах. Для локальной сети Let’s Encrypt не сработает (нет публичного IP), ну и его чуть сложнее запускать.&lt;br /&gt;
Я решил вопрос генерацией самоподписанного сертификата одной командой Dokku:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;ssh dokku.datahub.mother certs:generate my-first-app my-first-app.dokku.datahub.mother&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Браузер ругается, но приложение работает полноценно.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Еще я прогнал стресс тесты&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;ab -n 10000 -k -c 2000 ...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Много они не показали, решением было подкрутить nginx, настроить кеш ssl, горизонтальное масштабирование не приносило больших результатов. я упирался в ограничения клиента при тестах нагрузки.&lt;/p&gt;
&lt;h3&gt;Итог&lt;/h3&gt;
&lt;p&gt;Dokku на домашнем сервере — это отличный инструмент.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Для простых API (Flask/FastAPI):&lt;/b&gt; Работает “из коробки” идеально.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Для сложных задач:&lt;/b&gt; Использование `uv` делает работу комфортной, а понимание разницы между *Stateless* и *Stateful* приложениями спасает от занудных ошибок и отладки.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Теперь my-first-app готово предсказывать возраст всем гостям на Новый год, а сервер готов к новым экспериментам! 🎄 Пожалуй оставлю его для будущих экспериментов. Прижился как-то быстро. Кстати у Dokku есть коммерческая PRO версия, а точнее не версия, а полноценный UI с кнопочками и стоит он 900$. &lt;a href="https://dokku.com/docs/enterprise/pro/"&gt;https://dokku.com/docs/enterprise/pro/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Пора чего-нибудь накатить новогоднего еще :)&lt;/p&gt;
</description>
</item>

<item>
<title>KubeVela: Современная доставка приложений</title>
<guid isPermaLink="false">300</guid>
<link>https://gavrilov.info/all/kubevela-sovremennaya-dostavka-prilozheniy/</link>
<pubDate>Mon, 08 Dec 2025 01:10:10 +0300</pubDate>
<author></author>
<comments>https://gavrilov.info/all/kubevela-sovremennaya-dostavka-prilozheniy/</comments>
<description>
&lt;p&gt;В мире Cloud-Native технологий Kubernetes стал операционной системой облака. Однако, как было отмечено в материалах про Rainbond, «ванильный» Kubernetes сложен для разработчиков. Если Rainbond решает эту проблему, предлагая готовый &lt;b&gt;PaaS «из коробки»&lt;/b&gt; с упором на UI, то &lt;b&gt;KubeVela&lt;/b&gt; подходит к задаче с другой стороны: это &lt;b&gt;двигатель для построения собственной платформы&lt;/b&gt; (Platform Engine), основанный на модели OAM (Open Application Model).&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2025-12-08-v-01.09.35.png" width="558" height="632" alt="" /&gt;
&lt;div class="e2-text-caption"&gt;&lt;a href="https://kubevela.io"&gt;https://kubevela.io&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;KubeVela позволяет создавать простые и ориентированные на приложение (Application-centric) абстракции, сохраняя при этом полную гибкость и управляемость через код (Infrastructure as Code).&lt;/p&gt;
&lt;h3&gt;Основная философия: OAM и разделение ответственности&lt;/h3&gt;
&lt;p&gt;В основе KubeVela лежит стандарт &lt;b&gt;OAM (Open Application Model)&lt;/b&gt;. Эта модель разделяет ответственность между двумя ролями:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;&lt;b&gt;Разработчиĸи (Developers):&lt;/b&gt; Описывают &lt;b&gt;что&lt;/b&gt; нужно запустить (образ контейнера, порт, параметры запуска).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Платформенные инженеры (Platform Engineers):&lt;/b&gt; Описывают &lt;b&gt;ĸаĸ&lt;/b&gt; это должно работать (инфраструктура, политики масштабирования, Ingress, безопасность).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Математически модель приложения в KubeVela можно выразить формулой:&lt;/p&gt;
&lt;p&gt;$$ Application = Components + Traits + Workflow $$&lt;/p&gt;
&lt;p&gt;Где:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Components&lt;/b&gt;: Основная рабочая нагрузка (Workload). Например, Docker-образ, Helm-чарт или даже Terraform-модуль.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Traits&lt;/b&gt; (Свойства/Черты): Операционные характеристики, подключаемые к компоненту. Например: `ingress`, `scaler`, `sidecar`.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Workflow&lt;/b&gt;: Цепочка шагов доставки (Deploy -&gt; Test -&gt; Promote).&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Пример манифеста KubeVela&lt;/h4&gt;
&lt;p&gt;Вместо десятков Kubernetes-ресурсов (Deployment, Service, Ingress, HPA), разработчик пишет один упрощенный файл:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
  name: website
spec:
  components:
    - name: frontend
      type: webservice
      properties:
        image: nginx
      traits:
        - type: scaler
          properties:
            replicas: 3
        - type: gateway
          properties:
            domain: test.example.com&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;Сравнение: KubeVela vs Rainbond&lt;/h3&gt;
&lt;p&gt;Основываясь на прошлой статье про &lt;a href="https://gavrilov.info/all/rainbond-oblachnaya-platforma-dlya-upravleniya-prilozheniyami/"&gt;Rainbond&lt;/a&gt;, сделаем детальное сравнение. Оба инструмента решают проблему сложности Kubernetes, но нацелены на разные аудитории.&lt;/p&gt;
&lt;table cellpadding="0" cellspacing="0" border="0" class="e2-text-table"&gt;
&lt;tr&gt;
&lt;td style="text-align: center"&gt;Хараĸтеристиĸа&lt;/td&gt;
&lt;td style="text-align: center"&gt;Rainbond&lt;/td&gt;
&lt;td style="text-align: center"&gt;KubeVela&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;Философия&lt;/b&gt;&lt;/td&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;No-Code / Low-Code PaaS.&lt;/b&gt; Готовая платформа с полным UI. Ориентация на визуальное управление.&lt;/td&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;Platform Engine / IaC.&lt;/b&gt; Инструментарий для создания платформы. Ориентация на GitOps и конфигурацию как код.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;Входной порог&lt;/b&gt;&lt;/td&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;Очень низкий.&lt;/b&gt; Не нужно знать Kubernetes. Можно собрать приложение из `.jar` или исходного кода одной кнопкой.&lt;/td&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;Средний.&lt;/b&gt; Требует понимания концепций OAM. Инженерам платформы нужно знать CUE (язык конфигурации).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;Гибĸость&lt;/b&gt;&lt;/td&gt;
&lt;td style="text-align: center"&gt;Ограничена возможностями UI платформы. Сложно кастомизировать внутреннюю логику деплоя нестандартных вещей.&lt;/td&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;Высочайшая.&lt;/b&gt; Любая абстракция настраивается через X-Definitions (на языке CUE). Можно описать любой ресурс K8s или Terraform.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;Интеграция IaC&lt;/b&gt;&lt;/td&gt;
&lt;td style="text-align: center"&gt;Базируется на внутренних механизмах.&lt;/td&gt;
&lt;td style="text-align: center"&gt;Нативная интеграция с &lt;b&gt;Terraform&lt;/b&gt;. KubeVela может разворачивать облачные базы данных (AWS RDS, Cloud SQL) как часть приложения.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;Multi-cluster&lt;/b&gt;&lt;/td&gt;
&lt;td style="text-align: center"&gt;Есть управление несколькими кластерами, но фокус на единой панели управления.&lt;/td&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;Сильная сторона.&lt;/b&gt; Мощный движок развертывания приложений сразу в несколько кластеров с политиками (Placement Policies).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;Для ĸого&lt;/b&gt;&lt;/td&gt;
&lt;td style="text-align: center"&gt;SMB, стартапы, команды без сильной экспертизы в K8s, желающие получить опыт Heroku на своих серверах.&lt;/td&gt;
&lt;td style="text-align: center"&gt;Enterprise, Platform Engineering команды, желающие построить &lt;b&gt;свой&lt;/b&gt; внутренний PaaS (IDP) по стандартам Spotify/Netflix.&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;h4&gt;Сравнение с другими аналогами&lt;/h4&gt;
&lt;ol start="1"&gt;
&lt;li&gt;&lt;b&gt;Crossplane&lt;/b&gt;:
&lt;ul&gt;
  &lt;li&gt;&lt;b&gt;Crossplane&lt;/b&gt; фокусируется на «Инфраструктуре» (создание баз данных, кластеров, сетей в облаках через K8s API).&lt;/li&gt;
  &lt;li&gt;&lt;b&gt;KubeVela&lt;/b&gt; фокусируется на «Приложении».&lt;/li&gt;
  &lt;li&gt;&lt;b&gt;Итог:&lt;/b&gt; Они часто используются вместе. KubeVela использует Crossplane для заказа ресурсов.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ol start="2"&gt;
&lt;li&gt;&lt;b&gt;ArgoCD / Flux (GitOps)&lt;/b&gt;:
&lt;ul&gt;
  &lt;li&gt;Это инструменты непрерывной доставки (CD), которые синхронизируют Git и K8s.&lt;/li&gt;
  &lt;li&gt;KubeVela &lt;b&gt;может&lt;/b&gt; использовать их под капотом или использовать свой собственный Workflow-контроллер для сложных сценариев (например, «засуспендить деплой, пока не пройдет тест, затем отправить уведомление в Slack»).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ol start="3"&gt;
&lt;li&gt;&lt;b&gt;Backstage&lt;/b&gt;:
&lt;ul&gt;
  &lt;li&gt;Backstage — это «Портал» (Frontend). KubeVela — это «Движок» (Backend).&lt;/li&gt;
  &lt;li&gt;Существует плагин `vela-backstage`, который позволяет визуализировать приложения KubeVela внутри интерфейса Backstage. Это идеальная связка для построения IDP.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Ключевые возможности KubeVela&lt;/h3&gt;
&lt;h4&gt;1. Программируемая абстраĸция (CUE Language)&lt;/h4&gt;
&lt;p&gt;В отличие от Helm, где шаблонизация — это просто подстановка строк, KubeVela использует язык &lt;b&gt;CUE&lt;/b&gt; (Configure Unify Execute). Это позволяет платформенным инженерам создавать «умные» шаблоны.&lt;/p&gt;
&lt;p&gt;Пример логики на CUE внутри KubeVela: «Если пользователь не указал CPU, автоматически поставить `requests: 100m`, но если это ‘prod’ окружение, то поставить `500m`».&lt;/p&gt;
&lt;h4&gt;2. Единый Workflow для всего&lt;/h4&gt;
&lt;p&gt;KubeVela позволяет оркестрировать не только Kubernetes-ресурсы. В одном пайплайне вы можете:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;Создать S3 бакет в AWS (через Terraform/Crossplane).&lt;/li&gt;
&lt;li&gt;Дождаться готовности.&lt;/li&gt;
&lt;li&gt;Задеплоить Deployment в Kubernetes.&lt;/li&gt;
&lt;li&gt;Отправить уведомление в Telegram.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;3. Отсуствие «Config Drift»&lt;/h4&gt;
&lt;p&gt;KubeVela активно борется с дрейфом конфигураций, постоянно сверяя желаемое состояние приложения с реальным (State Reconciliation).&lt;/p&gt;
&lt;h3&gt;Итог и реĸомендации&lt;/h3&gt;
&lt;p&gt;Выбор между Rainbond и KubeVela зависит от того, &lt;b&gt;ĸто&lt;/b&gt; будет обслуживать платформу и &lt;b&gt;ĸаĸие&lt;/b&gt; цели стоят перед бизнесом.&lt;/p&gt;
&lt;h4&gt;Выбирайте Rainbond, если:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;У вас небольшая или средняя команда разработчиков.&lt;/li&gt;
&lt;li&gt;Нет выделенного отдела Platform Engineering / DevOps.&lt;/li&gt;
&lt;li&gt;Нужно «вчера» перенести легаси (Java/PHP/Monolith) в контейнеры без переписывания кода.&lt;/li&gt;
&lt;li&gt;Вам нужен визуальный интерфейс для управления связями сервисов и мониторинга без написания YAML.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Цель:&lt;/b&gt; Быстрый Time-to-market с минимальными затратами на инженерию.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Выбирайте KubeVela, если:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Вы строите серьезный Internal Developer Platform (IDP) в крупной компании.&lt;/li&gt;
&lt;li&gt;У вас есть инженеры, знающие Kubernetes, но вы хотите скрыть его сложность от продуктовых разработчиков.&lt;/li&gt;
&lt;li&gt;Вам нужен строгий GitOps подход (все через репозиторий).&lt;/li&gt;
&lt;li&gt;Требуется гибридное развертывание: часть в K8s, часть в Serverless, часть в облачных RDS.&lt;/li&gt;
&lt;li&gt;Вы планируете использовать Backstage в качестве фронтенда.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Цель:&lt;/b&gt; Стандартизация, масштабируемость, полный контроль над абстракциями (Platform as Product).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;b&gt;Рекомендация по стеĸу для Enterprise:&lt;/b&gt;&lt;br /&gt;
Для максимальной эффективности современный стек платформенной инженерии часто выглядит так:&lt;br /&gt;
$$ \text{Backstage (UI)} + \text{KubeVela (Engine)} + \text{Crossplane (Infra)} $$&lt;br /&gt;
Эта связка дает удобство портала (как в Rainbond), мощь оркестрации (KubeVela) и управление облаком (Crossplane).&lt;/p&gt;
</description>
</item>


</channel>
</rss>