<?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>faucet-stream и магия Webhook-интеграций</title>
<guid isPermaLink="false">365</guid>
<link>https://gavrilov.info/all/faucet-stream-i-magiya-webhook-integraciy/</link>
<pubDate>Sat, 31 Oct 2026 01:37:44 +0300</pubDate>
<author></author>
<comments>https://gavrilov.info/all/faucet-stream-i-magiya-webhook-integraciy/</comments>
<description>
&lt;p&gt;В мире данных мы привыкли к pull-моделям: регулярно опрашивать API, вычитывать базы данных или забирать файлы. Но современные SaaS-сервисы всё чаще используют &lt;b&gt;Webhooks&lt;/b&gt; — механизм, при котором событие само «толкается» (push) в ваш эндпоинт.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;`faucet-stream`&lt;/b&gt; — это декларативный ETL-инструмент на Rust, который идеально подходит для обработки таких событий. Он настраивается через простой YAML-файл и позволяет за пару минут создать надежный конвейер от HTTP-запроса до современного формата хранения данных, например Apache Iceberg.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/2026-11-02-v-23.31.41.png" width="1596" height="286" alt="" /&gt;
&lt;/div&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/image-247.png" width="1280" height="640" alt="" /&gt;
&lt;div class="e2-text-caption"&gt;&lt;a href="https://github.com/faucet-hq/faucet-stream"&gt;https://github.com/faucet-hq/faucet-stream&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h4&gt;🌐 Где применим кейс с Webhook?&lt;/h4&gt;
&lt;p&gt;Webhook-источник в `faucet` — это универсальный «швейцарский нож». Вам не нужно писать код для интеграции с десятками сервисов, если они умеют отправлять HTTP-запросы:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Jira / GitLab / GitHub:&lt;/b&gt; Ловить события создания, изменения или закрытия задач (issues) и пулл-реквестов для построения аналитики по продуктивности команд.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Stripe / ЮKassa:&lt;/b&gt; Принимать уведомления об успешных платежах или подписках в реальном времени.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CRM-системы (HubSpot, amoCRM):&lt;/b&gt; Ловить изменения статусов сделок.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IoT-устройства:&lt;/b&gt; Отправлять телеметрию при наступлении определенных событий (например, срабатывании датчика).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Вы просто настраиваете в Jira Webhook на URL вашего `faucet`, и все изменения задач автоматически превращаются в строки в вашей Iceberg-таблице без единой строчки кода на Python или Java.&lt;/p&gt;
&lt;h4&gt;⚠️ Ограничения Webhook-подхода&lt;/h4&gt;
&lt;p&gt;Несмотря на удобство, важно понимать природу Webhooks:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;&lt;b&gt;Доставка «At-most-once» (в лучшем случае):&lt;/b&gt; Отправитель (например, Jira) отправляет запрос и ждет ответа `200 OK`. Если ваш сервер временно недоступен (упал, перезагружается), событие будет &lt;b&gt;потеряно навсегда&lt;/b&gt;. Webhooks не гарантируют доставку.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Отсутствие идемпотентности:&lt;/b&gt; Отправитель может отправить одно и то же событие дважды (сетевые сбои). `faucet` запишет оба события (append-only), поэтому вам нужно самостоятельно чистить дубликаты на этапе аналитики.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Зависимость от отправителя:&lt;/b&gt; Если Jira не имеет встроенного механизма повторных попыток (retry) при ошибках, вам придется самостоятельно обеспечивать высокую доступность эндпоинта `faucet`.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Для критически важных данных лучше использовать очереди (Kafka) или CDC (Debezium), но для аналитики, логов и некритичных бизнес-событий Webhook + `faucet` — это самое быстрое и дешевое решение.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;🛠️ Пошаговая инструкция: Webhook → Iceberg → Trino&lt;/h3&gt;
&lt;p&gt;В этой инструкции мы развернем современный Lakehouse-стек:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Silo&lt;/b&gt; (S3-совместимое хранилище с поддержкой STS).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Lakekeeper&lt;/b&gt; (REST-каталог Iceberg).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Trino&lt;/b&gt; (движок запросов).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;faucet-stream&lt;/b&gt; (приемщик вебхуков).&lt;br /&gt;
*(Стек запускается через Podman на macOS).*&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;📂 Шаг 1: Подготовка файлов&lt;/h4&gt;
&lt;p&gt;Создайте рабочую директорию и положите в нее три файла.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;1. `docker-compose.yaml`&lt;/b&gt; (Только инфраструктура):&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;services:
  silo:
    image: pgsty/silo:RELEASE.2026-09-03T13-18-01Z
    command: server /data --console-address &amp;quot;:9001&amp;quot;
    environment:
      MINIO_ROOT_USER: silo-root-user
      MINIO_ROOT_PASSWORD: silo-root-password
    healthcheck:
      test: [&amp;quot;CMD&amp;quot;, &amp;quot;mc&amp;quot;, &amp;quot;ready&amp;quot;, &amp;quot;local&amp;quot;]
      interval: 2s
      timeout: 10s
      retries: 2
      start_period: 15s
    ports:
      - &amp;quot;9000:9000&amp;quot;
      - &amp;quot;9001:9001&amp;quot;
    networks:
      - iceberg_net

  create-bucket:
    image: pgsty/silo:RELEASE.2026-09-03T13-18-01Z
    depends_on:
      silo:
        condition: service_healthy
    entrypoint: /bin/sh
    command:
      - -c
      - |
        mc alias set local http://silo:9000 silo-root-user silo-root-password &amp;amp;&amp;amp;
        mc mb --ignore-existing local/examples
    networks:
      - iceberg_net

  db:
    image: postgres:17
    environment:
      - POSTGRES_PASSWORD=postgres
    healthcheck:
      test: [&amp;quot;CMD-SHELL&amp;quot;, &amp;quot;pg_isready -U postgres -p 5432 -d postgres&amp;quot;]
      interval: 2s
      timeout: 10s
      retries: 2
      start_period: 10s
    networks:
      - iceberg_net

  migrate:
    image: quay.io/lakekeeper/catalog:latest-main
    pull_policy: always
    environment:
      - LAKEKEEPER__PG_ENCRYPTION_KEY=This-is-NOT-Secure!
      - LAKEKEEPER__PG_DATABASE_URL_READ=postgresql://postgres:postgres@db:5432/postgres
      - LAKEKEEPER__PG_DATABASE_URL_WRITE=postgresql://postgres:postgres@db:5432/postgres
      - RUST_LOG=info
    restart: &amp;quot;no&amp;quot;
    command: [&amp;quot;migrate&amp;quot;]
    depends_on:
      db:
        condition: service_healthy
    networks:
      - iceberg_net

  lakekeeper:
    image: quay.io/lakekeeper/catalog:latest-main
    pull_policy: always
    environment:
      - LAKEKEEPER__PG_ENCRYPTION_KEY=This-is-NOT-Secure!
      - LAKEKEEPER__PG_DATABASE_URL_READ=postgresql://postgres:postgres@db:5432/postgres
      - LAKEKEEPER__PG_DATABASE_URL_WRITE=postgresql://postgres:postgres@db:5432/postgres
      - RUST_LOG=info
    command: [&amp;quot;serve&amp;quot;]
    healthcheck:
      test: [&amp;quot;CMD&amp;quot;, &amp;quot;/home/nonroot/lakekeeper&amp;quot;, &amp;quot;healthcheck&amp;quot;]
      interval: 1s
      timeout: 10s
      retries: 3
      start_period: 3s
    depends_on:
      migrate:
        condition: service_completed_successfully
      db:
        condition: service_healthy
      silo:
        condition: service_healthy
      create-bucket:
        condition: service_completed_successfully
    ports:
      - &amp;quot;8181:8181&amp;quot;
    networks:
      - iceberg_net

networks:
  iceberg_net:
    driver: bridge&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;2. `create-default-warehouse.json`&lt;/b&gt; (Настройка каталога):&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;{
  &amp;quot;warehouse-name&amp;quot;: &amp;quot;demo&amp;quot;,
  &amp;quot;project-id&amp;quot;: &amp;quot;00000000-0000-0000-0000-000000000000&amp;quot;,
  &amp;quot;storage-profile&amp;quot;: {
    &amp;quot;type&amp;quot;: &amp;quot;s3&amp;quot;,
    &amp;quot;bucket&amp;quot;: &amp;quot;examples&amp;quot;,
    &amp;quot;endpoint&amp;quot;: &amp;quot;http://silo:9000&amp;quot;,
    &amp;quot;region&amp;quot;: &amp;quot;us-east-1&amp;quot;,
    &amp;quot;path-style-access&amp;quot;: true,
    &amp;quot;flavor&amp;quot;: &amp;quot;s3-compat&amp;quot;,
    &amp;quot;sts-enabled&amp;quot;: true
  },
  &amp;quot;storage-credential&amp;quot;: {
    &amp;quot;type&amp;quot;: &amp;quot;s3&amp;quot;,
    &amp;quot;credential-type&amp;quot;: &amp;quot;access-key&amp;quot;,
    &amp;quot;aws-access-key-id&amp;quot;: &amp;quot;silo-root-user&amp;quot;,
    &amp;quot;aws-secret-access-key&amp;quot;: &amp;quot;silo-root-password&amp;quot;
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;3. `pipeline.yaml`&lt;/b&gt; (Конфигурация `faucet` с Webhook):&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;version: 1
pipeline:
  source:
    type: webhook
    config:
      listen_addr: &amp;quot;0.0.0.0:8080&amp;quot;
      path: &amp;quot;/v1/events&amp;quot;
      auth_token: &amp;quot;123&amp;quot;
      max_payloads: 10
      timeout_secs: 3600

  sink:
    type: iceberg
    config:
      catalog:
        type: rest
        uri: &amp;quot;http://lakekeeper:8181/catalog&amp;quot;
        warehouse: &amp;quot;demo&amp;quot;
        properties:
          # Включаем использование временных ключей от Lakekeeper
          rest.vended-credentials-enabled: &amp;quot;true&amp;quot;
          io-impl: &amp;quot;org.apache.iceberg.aws.s3.S3FileIO&amp;quot;
          s3.endpoint: &amp;quot;http://silo:9000&amp;quot;
          s3.path-style-access: &amp;quot;true&amp;quot;
          s3.region: &amp;quot;us-east-1&amp;quot;
      namespace: [&amp;quot;demo&amp;quot;]
      table: &amp;quot;events&amp;quot;
      create_table: true
      write_mode: append&lt;/code&gt;&lt;/pre&gt;&lt;hr /&gt;
&lt;h4&gt;🚀 Шаг 2: Запуск и инициализация&lt;/h4&gt;
&lt;p&gt;Выполняйте команды строго по одной в терминале.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;1. Поднимаем инфраструктуру:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;podman-compose down -v
podman-compose up -d
sleep 10&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;*Убедитесь, что контейнер `faucet-stream_lakekeeper_1` имеет статус `healthy` (`podman ps`).*&lt;/p&gt;
&lt;p&gt;&lt;b&gt;2. Инициализируем Lakekeeper (Bootstrap и создание Warehouse):&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;# Bootstrap
podman run --rm --network faucet-stream_iceberg_net curlimages/curl \
  -X POST http://lakekeeper:8181/management/v1/bootstrap \
  -H &amp;quot;Content-Type: application/json&amp;quot; \
  --data '{&amp;quot;accept-terms-of-use&amp;quot;: true}'

# Создание Warehouse
podman run --rm --network faucet-stream_iceberg_net \
  -v &amp;quot;$(pwd)/create-default-warehouse.json:/tmp/wh.json&amp;quot; \
  curlimages/curl \
  -X POST http://lakekeeper:8181/management/v1/warehouse \
  -H &amp;quot;Content-Type: application/json&amp;quot; --data @/tmp/wh.json&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;3. Запускаем Trino и настраиваем каталог:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;# Запуск Trino
podman run -d --name trino --network faucet-stream_iceberg_net \
  -p 9999:8080 -e CATALOG_MANAGEMENT=dynamic trinodb/trino:476

# Ждем инициализации Trino (20 секунд)
echo &amp;quot;Waiting for Trino to start...&amp;quot;
sleep 20

# Создаем каталог lakekeeper в Trino
podman exec trino trino --execute &amp;quot;
CREATE CATALOG lakekeeper USING iceberg
WITH (
    \&amp;quot;iceberg.catalog.type\&amp;quot; = 'rest',
    \&amp;quot;iceberg.rest-catalog.uri\&amp;quot; = 'http://lakekeeper:8181/catalog',
    \&amp;quot;iceberg.rest-catalog.warehouse\&amp;quot; = 'demo',
    \&amp;quot;iceberg.rest-catalog.nested-namespace-enabled\&amp;quot; = 'true',
    \&amp;quot;iceberg.rest-catalog.vended-credentials-enabled\&amp;quot; = 'true',
    \&amp;quot;iceberg.unique-table-location\&amp;quot; = 'true',
    \&amp;quot;s3.region\&amp;quot; = 'us-east-1',
    \&amp;quot;fs.native-s3.enabled\&amp;quot; = 'true',
    \&amp;quot;s3.path-style-access\&amp;quot; = 'true',
    \&amp;quot;s3.endpoint\&amp;quot; = 'http://silo:9000'
);&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;hr /&gt;
&lt;h4&gt;🎯 Шаг 3: Запуск faucet и отправка вебхука&lt;/h4&gt;
&lt;p&gt;&lt;b&gt;1. В новом окне терминала запускаем `faucet`:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;podman run --rm -it --network faucet-stream_iceberg_net \
  -p 8081:8080 -v &amp;quot;$(pwd)/pipeline.yaml:/app/pipeline.yaml&amp;quot; \
  ghcr.io/faucet-hq/faucet-stream:full run /app/pipeline.yaml&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;2. В третьем окне отправляем тестовый вебхук&lt;/b&gt; (имитируем событие из Jira):&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;curl -X POST http://127.0.0.1:8081/v1/events \
  -H &amp;quot;Authorization: Bearer 123&amp;quot; \
  -H &amp;quot;Content-Type: application/json&amp;quot; \
  -d '{&amp;quot;event_id&amp;quot;: &amp;quot;evt-001&amp;quot;, &amp;quot;user&amp;quot;: &amp;quot;alice&amp;quot;, &amp;quot;action&amp;quot;: &amp;quot;login&amp;quot;}'&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;*(В логах `faucet` вы увидите `records_written=10` и `succeeded=1`).*&lt;/p&gt;
&lt;p&gt;&lt;b&gt;3. Проверяем результат в Trino:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;podman exec trino trino --execute &amp;quot;SELECT * FROM lakekeeper.demo.events;&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;b&gt;Вывод:&lt;/b&gt;&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;&amp;quot;evt-001&amp;quot;,&amp;quot;alice&amp;quot;,&amp;quot;login&amp;quot;
&amp;quot;evt-002&amp;quot;,&amp;quot;alice&amp;quot;,&amp;quot;login&amp;quot;
...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Готово! Вы только что превратили HTTP-события в структурированную Iceberg-таблицу. 🎉&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;💡 Дополнение 1: За пределами Webhook: faucet как ETL-платформа&lt;/h4&gt;
&lt;p&gt;Хотя в этой инструкции мы сфокусировались на кейсе с Webhook, важно понимать, что &lt;b&gt;`faucet-stream` — это не просто приемщик HTTP-запросов, а полноценная ETL-платформа&lt;/b&gt;.&lt;/p&gt;
&lt;p&gt;Помимо Webhook, он поддерживает десятки других паттернов интеграции: от классического чтения REST API (с пагинацией и инкрементальной загрузкой) и баз данных (включая CDC из PostgreSQL и MySQL) до потоковой передачи из Kafka и чтения файлов из S3. Webhook — это лишь один из самых простых и быстрых способов начать, который идеально ложится на интеграцию с Jira, Stripe, GitHub и другими SaaS-сервисами.&lt;/p&gt;
&lt;h5&gt;🐛 О решенном баге и версиях&lt;/h5&gt;
&lt;p&gt;В ходе настройки мы столкнулись с ошибкой `StorageFactory must be provided for RestCatalog`. На момент выхода версии `v1.15.1` коннектор Iceberg в `faucet` не умел передавать фабрику хранилища для REST-каталогов. Команда проекта оперативно отреагировала на &lt;a href="https://github.com/faucet-hq/faucet-stream/issues/962"&gt;баг-репорт&lt;/a&gt;, и исправление уже вошло в последующие релизы (в частности, в рамках PR #966).&lt;/p&gt;
&lt;p&gt;Теперь интеграция с Lakekeeper и Silo работает из коробки. Актуальный и стабильный образ всегда можно найти в официальном реестре: &lt;b&gt;&lt;a href="https://github.com/faucet-hq/faucet-stream/pkgs/container/faucet-stream"&gt;ghcr.io/faucet-hq/faucet-stream&lt;/a&gt;&lt;/b&gt;.&lt;/p&gt;
&lt;h5&gt;🍏 Почему мы запускали Trino и Faucet вручную на macOS?&lt;/h5&gt;
&lt;p&gt;Если вы обратили внимание, в инструкции мы запускали инфраструктуру (Silo, Lakekeeper) через `podman-compose`, а `Trino` и `faucet` — отдельными командами `podman run`.&lt;/p&gt;
&lt;p&gt;Это не архитектурное требование, а вынужденный обход специфичного бага &lt;b&gt;`podman-compose` на macOS&lt;/b&gt;. На Mac этот инструмент часто пытается объединить все контейнеры в один «Pod» и некорректно обрабатывает сложные цепочки зависимостей (`depends_on: condition: service_healthy`). В результате вы получаете общую ошибку `starting some containers: internal libpod error`, за которой невозможно разглядеть реальные логи контейнера.&lt;/p&gt;
&lt;p&gt;Ручной запуск через `podman run` с явным указанием сети (`--network faucet-stream_iceberg_net`) гарантирует, что контейнеры стартуют в правильном порядке, а вы всегда видите их реальные логи. На Linux-серверах или в Docker Desktop для Mac этот шаг можно смело делать через стандартный `docker-compose up -d`.&lt;/p&gt;
&lt;p&gt;В итоге, объединив мощь `faucet-stream`, современный REST-каталог Lakekeeper и движок Trino, вы получили гибкий и масштабируемый Lakehouse. Теперь любые события из внешних систем могут напрямую попадать в вашу аналитику без единой строчки кода! 🚀 Но не все так просто и радужно, как может показаться 🌈🦄 ...&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;💡 Дополнение 2: Архитектура faucet-stream, Push vs Pull и будущее потоковой обработки&lt;/h4&gt;
&lt;p&gt;В завершение статьи важно заглянуть «под капот» `faucet-stream` и обсудить два фундаментальных подхода к интеграции данных: &lt;b&gt;Push (Webhook)&lt;/b&gt; и &lt;b&gt;Pull (REST API)&lt;/b&gt;. Понимание этой разницы критически важно, так как от нее зависит надежность вашего будущего Lakehouse.&lt;/p&gt;
&lt;h5&gt;📤 Push-модель (Webhook) и её ограничения в faucet&lt;/h5&gt;
&lt;p&gt;В этой инструкции мы использовали `webhook` — это классическая &lt;b&gt;Push-модель&lt;/b&gt;. Отправитель (например, Jira или Stripe) сам «толкает» событие на ваш сервер.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Плюсы:&lt;/b&gt; Мгновенная реакция на событие, минимальная задержка, не нужно писать код для опроса.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ограничения `faucet-stream`:&lt;/b&gt; Важно понимать, что источник `webhook` в `faucet` — это &lt;b&gt;временный буфер&lt;/b&gt;, а не долгоживущий сервер. Сервер запускается, собирает POST-запросы в память (в течение `timeout_secs` или до `max_payloads`), а затем завершается, сбрасывая данные в Iceberg.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Режим `serve`:&lt;/b&gt; Даже при запуске `faucet serve --triggers` триггер выступает лишь &lt;b&gt;оркестратором&lt;/b&gt;. Он запускает пайплайн по событию, но само тело вебхука (`${trigger.body}`) не передается в пайплайн как готовый поток записей — его можно использовать только для параметризации источников внутри пайплайна.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Риск потери:&lt;/b&gt; Если сервис перезагрузится или окно приема истечет в момент отправки, событие будет потеряно (доставка *at-most-once*). Установка огромного `timeout_secs` не решает проблему, а лишь откладывает ее, создавая риск переполнения памяти (OOM).&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;📥 Pull-модель (REST API): Надежность для Production&lt;/h5&gt;
&lt;p&gt;Альтернатива — &lt;b&gt;Pull-модель&lt;/b&gt;. Вместо того чтобы ждать вебхук, `faucet` сам регулярно опрашивает API внешней системы (например, Jira) с помощью источника `rest`.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Плюсы:&lt;/b&gt; Абсолютная надежность. Если `faucet` упал или был перезапущен, при следующем запуске он просто продолжит чтение с того места, где остановился (благодаря параметру `replication_method: Incremental`). Вы не потеряете ни одного события.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Где использовать:&lt;/b&gt; Для критически важных бизнес-данных, аналитики и построения Lakehouse лучше выбирать именно Pull-модель, запуская пайплайн по расписанию (например, через `cron` или `faucet schedule` каждые 5-15 минут).&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;🌊 Сравнение с RisingWave: Batch/ETL против True Streaming&lt;/h5&gt;
&lt;p&gt;Учитывая слово `stream` в названии, многие ожидают от `faucet` поведения полноценной потоковой платформы, например, &lt;b&gt;RisingWave&lt;/b&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;RisingWave&lt;/b&gt; — это &lt;b&gt;потоковая база данных (True Streaming)&lt;/b&gt;. Она выступает в роли постоянного получателя. Вы создаете таблицу с коннектором `webhook`, и все входящие HTTP-запросы немедленно попадают в нее и обрабатываются с низкой задержкой. Она держит состояние постоянно.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;faucet-stream&lt;/b&gt; — это, прежде всего, &lt;b&gt;декларативный ETL-инструмент (Batch/ELT)&lt;/b&gt;. Его парадигма — выполнение задачи (run) от начала до конца. Несмотря на название, на данный момент `faucet` не является долгоживущим стриминговым движком в том же смысле, что RisingWave или Kafka Streams. Его сила — в огромном количестве коннекторов, эффективной пакетной обработке и простоте YAML-конфигурации.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;b&gt;Взгляд в будущее:&lt;/b&gt; Слово «stream» в названии оставляет надежду, что в будущих версиях `faucet` может обзавестись более нативными примитивами для непрерывной потоковой обработки. Но на сегодняшний день, если вам нужен &lt;b&gt;постоянный, вечно живой приемник вебхуков с гарантией сохранности каждого байта&lt;/b&gt; (что нетривиально при текущем состоянии документации 😁), — это задача для потоковых баз данных (RisingWave) или связки «сервис-приемник + очередь (Kafka/Redis)». Если же вам нужен &lt;b&gt;мощный и гибкий инструмент для периодического перемещения данных (ETL)&lt;/b&gt; по расписанию или событию, `faucet-stream` с Pull-моделью — идеальный выбор.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2026-11-01-v-12.58.16.png" width="1734" height="656" alt="" /&gt;
&lt;/div&gt;
</description>
</item>

<item>
<title>kagent: Kubernetes-нативный рантайм для AI-агентов — архитектура, практика и альтернативы</title>
<guid isPermaLink="false">361</guid>
<link>https://gavrilov.info/all/kagent-kubernetes-nativny-rantaym-dlya-ai-agentov-arhitektura-pr/</link>
<pubDate>Fri, 23 Oct 2026 02:27:23 +0300</pubDate>
<author></author>
<comments>https://gavrilov.info/all/kagent-kubernetes-nativny-rantaym-dlya-ai-agentov-arhitektura-pr/</comments>
<description>
&lt;p&gt;&lt;b&gt;kagent&lt;/b&gt; — это open-source платформа с открытым исходным кодом, которая привносит «агентный» искусственный интеллект в облачные и Kubernetes-среды. Если говорить проще, она позволяет создавать интеллектуальных агентов, которые понимают запросы на естественном языке и автономно выполняют сложные операции по управлению инфраструктурой: диагностируют проблемы с сетью, анализируют логи, управляют развертываниями. &lt;a href="https://kagent.dev"&gt;https://kagent.dev&lt;/a&gt;&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2026-10-23-v-02.24.05.png" width="1690" height="554" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;kagent был создан компанией Solo.io в 2025 году и является sandbox-проектом CNCF (Cloud Native Computing Foundation). Его главная идея — превратить AI-агента из простого чат-бота в полноценного участника DevOps- и платформенных процессов, управляемого теми же инструментами, что и остальные рабочие нагрузки в кластере.&lt;/p&gt;
&lt;h3&gt;🎯 Что дает kagent и как он работает&lt;/h3&gt;
&lt;h4&gt;Ключевые возможности&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Управление как кодом (GitOps)&lt;/b&gt;: Агенты определяются как обычные Kubernetes-ресурсы (Custom Resources). Вы можете версионировать их, применять через `kubectl` и управлять ими так же, как и любыми другими компонентами инфраструктуры.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Автоматизация рутины&lt;/b&gt;: Агенты самостоятельно выполняют многошаговые задачи по устранению неполадок и мониторингу, снижая нагрузку на специалистов.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Гибкость и отсутствие привязки&lt;/b&gt;: kagent поддерживает множество LLM-провайдеров (OpenAI, Anthropic, Gemini и другие), а также любые &lt;b&gt;OpenAI-совместимые эндпоинты&lt;/b&gt;, что позволяет использовать практически любую модель, включая вашу собственную.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Интеграция с инструментами&lt;/b&gt;: Агенты могут использовать широкий набор инструментов (Tools) через Model Context Protocol (MCP) для взаимодействия с системами: просмотр логов подов, запросы к Prometheus, управление ресурсами Kubernetes и многое другое.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Human-in-the-loop&lt;/b&gt;: Встроенные механизмы подтверждения действий, когда агент предлагает изменить ресурс, но ждет одобрения человека, а также поддержка инициативных вопросов от агента.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Долговременная память&lt;/b&gt;: Векторная память, позволяющая агентам помнить контекст между сессиями, а не только в рамках последнего запроса.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Agent-to-Agent (A2A)&lt;/b&gt;: Агенты могут обнаруживать друг друга и вызывать, что позволяет строить многоагентные рабочие процессы с делегированием задач.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Архитектура&lt;/h4&gt;
&lt;p&gt;Архитектура kagent состоит из трех основных компонентов:&lt;/p&gt;
&lt;ol start="1"&gt;
&lt;li&gt;&lt;b&gt;Tools (Инструменты)&lt;/b&gt;: Это «руки» агента — набор функций, которые агент может вызывать для выполнения реальных действий в инфраструктуре. kagent поставляется с предустановленными инструментами для отображения логов подов, запросов к Prometheus, генерации ресурсов и многого другого.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Agents (Агенты)&lt;/b&gt;: Это «мозг» системы — автономные системы, которые планируют, выполняют и анализируют задачи, используя доступные инструменты. Вы описываете агента в YAML-файле, указывая его системный промпт (роль), модель LLM и список инструментов, к которым у него есть доступ. Агенты могут объединяться в команды, где планирующий агент распределяет задачи между исполнителями.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Framework (Фреймворк)&lt;/b&gt;: Гибкий интерфейс, позволяющий запускать агентов как через UI, так и декларативно. Он построен на фреймворке Google ADK, но также поддерживает LangGraph, CrewAI и собственные фреймворки.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;🐳 Практический пример: запуск kagent в Podman на macOS&lt;/h3&gt;
&lt;p&gt;Перейдем от теории к практике. Наш тестовый агент `myagent` был успешно запущен локально на MacBook (Intel) с использованием Podman. Он продемонстрировал возможность вызова инструментов (Tool Calling) и ведения диалога на русском языке.&lt;/p&gt;
&lt;h4&gt;Ключевые шаги развертывания&lt;/h4&gt;
&lt;ol start="1"&gt;
&lt;li&gt;&lt;b&gt;Подготовка окружения&lt;/b&gt;: Установка Podman и `podman-compose`, а также создание скрипта-обертки `docker-compose`, который перенаправляет вызовы в `podman-compose`, поскольку kagent ожидает наличия именно этой утилиты.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Создание проекта агента&lt;/b&gt;: Генерация структуры проекта командой `kagent init adk python myagent`.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Настройка базового образа&lt;/b&gt;: В `Dockerfile` базовый образ был заменен на `kagent-adk:0.10.4-full`, так как distroless-вариант не содержит оболочки `/bin/sh`, необходимой для выполнения команд установки зависимостей. Также был добавлен шаг `RUN uv pip install “google-adk[extensions]”` для поддержки LiteLLM и указана переменная `ENV PYTHONPATH=/app`.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Конфигурация подключения к LLM&lt;/b&gt;: В файле `agent.py` (и/или `docker-compose.yaml`) были явно указаны параметры для подключения к вашему эндпоинту ProxyAPI:
&lt;ul&gt;
  &lt;li&gt;Модель: `openai/z-ai/glm-5.3-flash` (обязательный префикс `openai/` для LiteLLM).&lt;/li&gt;
  &lt;li&gt;Базовый URL: `&lt;a href="https://api.proxyapi.ru/v1"&gt;https://api.proxyapi.ru/v1&lt;/a&gt;`.&lt;/li&gt;
  &lt;li&gt;API-ключ: ваш токен от ProxyAPI.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Запуск&lt;/b&gt;: Сборка образа и запуск контейнера через `podman-compose up`.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Результат: работающий агент с Tool Calling&lt;/h4&gt;
&lt;p&gt;На скриншоте видно, как агент `myagent` в интерактивном режиме `kagent run` отвечает на приветствие, а затем по запросу «давай кинем» выполняет вызов инструмента:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Tool Call&lt;/b&gt;: `roll_die` с параметром `{“sides”: 6}`.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Tool Result&lt;/b&gt;: `{“result”: 1}`.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ответ агента&lt;/b&gt;: «Выпала &lt;b&gt;1&lt;/b&gt; 🎲. На кубике с 6 гранями это минимальный результат — не повезло, но такое бывает! Хочешь бросить ещё раз или проверить число на простоту?»&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Этот пример наглядно демонстрирует полный цикл работы агента: понимание запроса, выбор и вызов нужного инструмента, обработка результата и формирование естественно-языкового ответа.&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2026-10-23-v-02.18.20.png" width="1796" height="1008" alt="" /&gt;
&lt;/div&gt;
&lt;h3&gt;⚖️ Сравнение с альтернативами&lt;/h3&gt;
&lt;p&gt;kagent — не единственный инструмент в этой нише. Чтобы понять его место, полезно сравнить его с другими решениями, которые решают смежные, но зачастую разные задачи. Подробные обзоры на Asya и Agent Autoscale появятся в отдельных статьях, а здесь мы приведем краткое сравнение.&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;Категория&lt;/td&gt;
&lt;td style="text-align: center"&gt;Ключевая абстракция&lt;/td&gt;
&lt;td style="text-align: center"&gt;Модель выполнения&lt;/td&gt;
&lt;td style="text-align: center"&gt;Масштабирование&lt;/td&gt;
&lt;td style="text-align: center"&gt;Основной сценарий&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;kagent&lt;/b&gt;&lt;/td&gt;
&lt;td style="text-align: center"&gt;Фреймворк для AI-агентов&lt;/td&gt;
&lt;td style="text-align: center"&gt;`Agent` (системный промпт + инструменты + LLM)&lt;/td&gt;
&lt;td style="text-align: center"&gt;Синхронный запрос/ответ через контроллер и движок ADK&lt;/td&gt;
&lt;td style="text-align: center"&gt;❌ Нет встроенного автомасштабирования (агенты — это процессы, управляемые контроллером)&lt;/td&gt;
&lt;td style="text-align: center"&gt;Одиночные агенты для операционных задач в K8s (troubleshooting, мониторинг)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;Asya&lt;/b&gt;&lt;/td&gt;
&lt;td style="text-align: center"&gt;Меш акторов для AI-нагрузок&lt;/td&gt;
&lt;td style="text-align: center"&gt;`AsyncActor` (stateless обработчик + очередь + sidecar)&lt;/td&gt;
&lt;td style="text-align: center"&gt;Хореография: сообщение несет свой маршрут через меш акторов&lt;/td&gt;
&lt;td style="text-align: center"&gt;✅ Поакторное масштабирование через KEDA (по глубине очереди), scale-to-zero&lt;/td&gt;
&lt;td style="text-align: center"&gt;Многошаговые AI/ML пайплайны, агентные рабочие процессы в масштабе&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center"&gt;&lt;b&gt;Agent Autoscale&lt;/b&gt;&lt;/td&gt;
&lt;td style="text-align: center"&gt;Оператор для управления AI-агентами&lt;/td&gt;
&lt;td style="text-align: center"&gt;`SwarmAgent` (LLM-агент с промптом, инструментами, guardrails и автомасштабированием)&lt;/td&gt;
&lt;td style="text-align: center"&gt;Управляется оператором, масштабирование на основе KEDA&lt;/td&gt;
&lt;td style="text-align: center"&gt;✅ Масштабирование на основе спроса и scale-to-zero через KEDA&lt;/td&gt;
&lt;td style="text-align: center"&gt;Управление флотами AI-агентов как first-class ресурсами с автомасштабированием&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;h4&gt;Другие заметные инструменты в экосистеме&lt;/h4&gt;
&lt;p&gt;Помимо вышеперечисленных, стоит упомянуть специализированные утилиты, которые часто используются в связке или как альтернативы для конкретных задач:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;K8sGPT&lt;/b&gt;: AI-ассистент для быстрой диагностики кластера. Он сканирует ресурсы и объясняет проблемы на понятном языке, но по умолчанию не вносит изменений (read-only).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HolmesGPT&lt;/b&gt;: Инструмент для более глубокого расследования инцидентов. Он анализирует логи, метрики и состояние кластера для поиска корневой причины, с возможностью опционального исправления с подтверждением.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;kubectl-ai&lt;/b&gt;: Утилита, которая превращает запросы на естественном языке в команды `kubectl`. Перед выполнением спрашивает подтверждение у пользователя.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;💎 Заключение&lt;/h3&gt;
&lt;p&gt;kagent занимает важную нишу, предлагая Kubernetes-нативный, декларативный подход к управлению AI-агентами. Он идеально подходит для платформенных инженеров и DevOps-специалистов, которые хотят автоматизировать рутинные операции, используя знакомые им инструменты (YAML, `kubectl`, GitOps). В отличие от специализированных ассистентов (K8sGPT, HolmesGPT), kagent — это именно &lt;b&gt;фреймворк&lt;/b&gt; для создания собственных агентов под конкретные задачи вашей инфраструктуры.&lt;/p&gt;
&lt;p&gt;Хотя kagent не является решением для масштабных асинхронных пайплайнов (как Asya) или автоматического масштабирования флотов агентов (как Agent Autoscale), его сила — в простоте, гибкости и глубокой интеграции с экосистемой Kubernetes.&lt;/p&gt;
&lt;p&gt;Успешный запуск агента с поддержкой Tool Calling в локальном окружении подтверждает, что kagent готов к практическому использованию и может стать мощным инструментом в арсенале вашей команды.&lt;/p&gt;
</description>
</item>

<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;
&lt;p&gt;А вот еще бонус:&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2026-10-01-v-21.07.56.png" width="1728" height="1268" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Все это через туннель &lt;a href="https://cloudpub.ru/docs/http"&gt;https://cloudpub.ru/docs/http&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Вот так:&lt;/p&gt;
&lt;pre class="e2-text-code"&gt;&lt;code class=""&gt;yuriygavrilov@MacBookPro ~ % ./clo publish https marimo.localhost:8443
Сервис опубликован: https://marimo.localhost:8443 -&amp;gt; https://thirdly-fast-bushbuck.cloudpub.ru:443&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Ну и конечно нужно DNS поправить, но работает и с авторизацией&lt;/p&gt;
&lt;div class="e2-text-picture"&gt;
&lt;img src="https://gavrilov.info/pictures/Snimok-ekrana-2026-10-01-v-21.10.22.png" width="1360" height="966" alt="" /&gt;
&lt;/div&gt;
&lt;p&gt;Но лучше так не делать, все такие весь трафик так проходит через cloudpub&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>