{
    "version": "https:\/\/jsonfeed.org\/version\/1.1",
    "title": "Yuriy Gavrilov: posts tagged DevOps",
    "_rss_description": "Welcome to my personal place for love, peace and happiness 🤖 Yuiry Gavrilov",
    "_rss_language": "en",
    "_itunes_email": "yvgavrilov@gmail.com",
    "_itunes_categories_xml": "",
    "_itunes_image": "https:\/\/gavrilov.info\/pictures\/userpic\/userpic-square@2x.jpg?1643451008",
    "_itunes_explicit": "no",
    "home_page_url": "https:\/\/gavrilov.info\/tags\/devops\/",
    "feed_url": "https:\/\/gavrilov.info\/tags\/devops\/json\/",
    "icon": "https:\/\/gavrilov.info\/pictures\/userpic\/userpic@2x.jpg?1643451008",
    "authors": [
        {
            "name": "Yuriy Gavrilov - B[u]g - for charity.gavrilov.eth",
            "url": "https:\/\/gavrilov.info\/",
            "avatar": "https:\/\/gavrilov.info\/pictures\/userpic\/userpic@2x.jpg?1643451008"
        }
    ],
    "items": [
        {
            "id": "365",
            "url": "https:\/\/gavrilov.info\/all\/faucet-stream-i-magiya-webhook-integraciy\/",
            "title": "faucet-stream и магия Webhook-интеграций",
            "content_html": "<p>В мире данных мы привыкли к pull-моделям: регулярно опрашивать API, вычитывать базы данных или забирать файлы. Но современные SaaS-сервисы всё чаще используют <b>Webhooks<\/b> — механизм, при котором событие само «толкается» (push) в ваш эндпоинт.<\/p>\n<p><b>`faucet-stream`<\/b> — это декларативный ETL-инструмент на Rust, который идеально подходит для обработки таких событий. Он настраивается через простой YAML-файл и позволяет за пару минут создать надежный конвейер от HTTP-запроса до современного формата хранения данных, например Apache Iceberg.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/2026-11-02-v-23.31.41.png\" width=\"1596\" height=\"286\" alt=\"\" \/>\n<\/div>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/image-247.png\" width=\"1280\" height=\"640\" alt=\"\" \/>\n<div class=\"e2-text-caption\"><a href=\"https:\/\/github.com\/faucet-hq\/faucet-stream\">https:\/\/github.com\/faucet-hq\/faucet-stream<\/a><\/div>\n<\/div>\n<h4>🌐 Где применим кейс с Webhook?<\/h4>\n<p>Webhook-источник в `faucet` — это универсальный «швейцарский нож». Вам не нужно писать код для интеграции с десятками сервисов, если они умеют отправлять HTTP-запросы:<\/p>\n<ul>\n<li><b>Jira \/ GitLab \/ GitHub:<\/b> Ловить события создания, изменения или закрытия задач (issues) и пулл-реквестов для построения аналитики по продуктивности команд.<\/li>\n<li><b>Stripe \/ ЮKassa:<\/b> Принимать уведомления об успешных платежах или подписках в реальном времени.<\/li>\n<li><b>CRM-системы (HubSpot, amoCRM):<\/b> Ловить изменения статусов сделок.<\/li>\n<li><b>IoT-устройства:<\/b> Отправлять телеметрию при наступлении определенных событий (например, срабатывании датчика).<\/li>\n<\/ul>\n<p>Вы просто настраиваете в Jira Webhook на URL вашего `faucet`, и все изменения задач автоматически превращаются в строки в вашей Iceberg-таблице без единой строчки кода на Python или Java.<\/p>\n<h4>⚠️ Ограничения Webhook-подхода<\/h4>\n<p>Несмотря на удобство, важно понимать природу Webhooks:<\/p>\n<ol start=\"1\">\n<li><b>Доставка «At-most-once» (в лучшем случае):<\/b> Отправитель (например, Jira) отправляет запрос и ждет ответа `200 OK`. Если ваш сервер временно недоступен (упал, перезагружается), событие будет <b>потеряно навсегда<\/b>. Webhooks не гарантируют доставку.<\/li>\n<li><b>Отсутствие идемпотентности:<\/b> Отправитель может отправить одно и то же событие дважды (сетевые сбои). `faucet` запишет оба события (append-only), поэтому вам нужно самостоятельно чистить дубликаты на этапе аналитики.<\/li>\n<li><b>Зависимость от отправителя:<\/b> Если Jira не имеет встроенного механизма повторных попыток (retry) при ошибках, вам придется самостоятельно обеспечивать высокую доступность эндпоинта `faucet`.<\/li>\n<\/ol>\n<p>Для критически важных данных лучше использовать очереди (Kafka) или CDC (Debezium), но для аналитики, логов и некритичных бизнес-событий Webhook + `faucet` — это самое быстрое и дешевое решение.<\/p>\n<hr \/>\n<h3>🛠️ Пошаговая инструкция: Webhook → Iceberg → Trino<\/h3>\n<p>В этой инструкции мы развернем современный Lakehouse-стек:<\/p>\n<ul>\n<li><b>Silo<\/b> (S3-совместимое хранилище с поддержкой STS).<\/li>\n<li><b>Lakekeeper<\/b> (REST-каталог Iceberg).<\/li>\n<li><b>Trino<\/b> (движок запросов).<\/li>\n<li><b>faucet-stream<\/b> (приемщик вебхуков).<br \/>\n*(Стек запускается через Podman на macOS).*<\/li>\n<\/ul>\n<h4>📂 Шаг 1: Подготовка файлов<\/h4>\n<p>Создайте рабочую директорию и положите в нее три файла.<\/p>\n<p><b>1. `docker-compose.yaml`<\/b> (Только инфраструктура):<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">services:\n  silo:\n    image: pgsty\/silo:RELEASE.2026-09-03T13-18-01Z\n    command: server \/data --console-address &quot;:9001&quot;\n    environment:\n      MINIO_ROOT_USER: silo-root-user\n      MINIO_ROOT_PASSWORD: silo-root-password\n    healthcheck:\n      test: [&quot;CMD&quot;, &quot;mc&quot;, &quot;ready&quot;, &quot;local&quot;]\n      interval: 2s\n      timeout: 10s\n      retries: 2\n      start_period: 15s\n    ports:\n      - &quot;9000:9000&quot;\n      - &quot;9001:9001&quot;\n    networks:\n      - iceberg_net\n\n  create-bucket:\n    image: pgsty\/silo:RELEASE.2026-09-03T13-18-01Z\n    depends_on:\n      silo:\n        condition: service_healthy\n    entrypoint: \/bin\/sh\n    command:\n      - -c\n      - |\n        mc alias set local http:\/\/silo:9000 silo-root-user silo-root-password &amp;&amp;\n        mc mb --ignore-existing local\/examples\n    networks:\n      - iceberg_net\n\n  db:\n    image: postgres:17\n    environment:\n      - POSTGRES_PASSWORD=postgres\n    healthcheck:\n      test: [&quot;CMD-SHELL&quot;, &quot;pg_isready -U postgres -p 5432 -d postgres&quot;]\n      interval: 2s\n      timeout: 10s\n      retries: 2\n      start_period: 10s\n    networks:\n      - iceberg_net\n\n  migrate:\n    image: quay.io\/lakekeeper\/catalog:latest-main\n    pull_policy: always\n    environment:\n      - LAKEKEEPER__PG_ENCRYPTION_KEY=This-is-NOT-Secure!\n      - LAKEKEEPER__PG_DATABASE_URL_READ=postgresql:\/\/postgres:postgres@db:5432\/postgres\n      - LAKEKEEPER__PG_DATABASE_URL_WRITE=postgresql:\/\/postgres:postgres@db:5432\/postgres\n      - RUST_LOG=info\n    restart: &quot;no&quot;\n    command: [&quot;migrate&quot;]\n    depends_on:\n      db:\n        condition: service_healthy\n    networks:\n      - iceberg_net\n\n  lakekeeper:\n    image: quay.io\/lakekeeper\/catalog:latest-main\n    pull_policy: always\n    environment:\n      - LAKEKEEPER__PG_ENCRYPTION_KEY=This-is-NOT-Secure!\n      - LAKEKEEPER__PG_DATABASE_URL_READ=postgresql:\/\/postgres:postgres@db:5432\/postgres\n      - LAKEKEEPER__PG_DATABASE_URL_WRITE=postgresql:\/\/postgres:postgres@db:5432\/postgres\n      - RUST_LOG=info\n    command: [&quot;serve&quot;]\n    healthcheck:\n      test: [&quot;CMD&quot;, &quot;\/home\/nonroot\/lakekeeper&quot;, &quot;healthcheck&quot;]\n      interval: 1s\n      timeout: 10s\n      retries: 3\n      start_period: 3s\n    depends_on:\n      migrate:\n        condition: service_completed_successfully\n      db:\n        condition: service_healthy\n      silo:\n        condition: service_healthy\n      create-bucket:\n        condition: service_completed_successfully\n    ports:\n      - &quot;8181:8181&quot;\n    networks:\n      - iceberg_net\n\nnetworks:\n  iceberg_net:\n    driver: bridge<\/code><\/pre><p><b>2. `create-default-warehouse.json`<\/b> (Настройка каталога):<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">{\n  &quot;warehouse-name&quot;: &quot;demo&quot;,\n  &quot;project-id&quot;: &quot;00000000-0000-0000-0000-000000000000&quot;,\n  &quot;storage-profile&quot;: {\n    &quot;type&quot;: &quot;s3&quot;,\n    &quot;bucket&quot;: &quot;examples&quot;,\n    &quot;endpoint&quot;: &quot;http:\/\/silo:9000&quot;,\n    &quot;region&quot;: &quot;us-east-1&quot;,\n    &quot;path-style-access&quot;: true,\n    &quot;flavor&quot;: &quot;s3-compat&quot;,\n    &quot;sts-enabled&quot;: true\n  },\n  &quot;storage-credential&quot;: {\n    &quot;type&quot;: &quot;s3&quot;,\n    &quot;credential-type&quot;: &quot;access-key&quot;,\n    &quot;aws-access-key-id&quot;: &quot;silo-root-user&quot;,\n    &quot;aws-secret-access-key&quot;: &quot;silo-root-password&quot;\n  }\n}<\/code><\/pre><p><b>3. `pipeline.yaml`<\/b> (Конфигурация `faucet` с Webhook):<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">version: 1\npipeline:\n  source:\n    type: webhook\n    config:\n      listen_addr: &quot;0.0.0.0:8080&quot;\n      path: &quot;\/v1\/events&quot;\n      auth_token: &quot;123&quot;\n      max_payloads: 10\n      timeout_secs: 3600\n\n  sink:\n    type: iceberg\n    config:\n      catalog:\n        type: rest\n        uri: &quot;http:\/\/lakekeeper:8181\/catalog&quot;\n        warehouse: &quot;demo&quot;\n        properties:\n          # Включаем использование временных ключей от Lakekeeper\n          rest.vended-credentials-enabled: &quot;true&quot;\n          io-impl: &quot;org.apache.iceberg.aws.s3.S3FileIO&quot;\n          s3.endpoint: &quot;http:\/\/silo:9000&quot;\n          s3.path-style-access: &quot;true&quot;\n          s3.region: &quot;us-east-1&quot;\n      namespace: [&quot;demo&quot;]\n      table: &quot;events&quot;\n      create_table: true\n      write_mode: append<\/code><\/pre><hr \/>\n<h4>🚀 Шаг 2: Запуск и инициализация<\/h4>\n<p>Выполняйте команды строго по одной в терминале.<\/p>\n<p><b>1. Поднимаем инфраструктуру:<\/b><\/p>\n<pre class=\"e2-text-code\"><code class=\"\">podman-compose down -v\npodman-compose up -d\nsleep 10<\/code><\/pre><p>*Убедитесь, что контейнер `faucet-stream_lakekeeper_1` имеет статус `healthy` (`podman ps`).*<\/p>\n<p><b>2. Инициализируем Lakekeeper (Bootstrap и создание Warehouse):<\/b><\/p>\n<pre class=\"e2-text-code\"><code class=\"\"># Bootstrap\npodman run --rm --network faucet-stream_iceberg_net curlimages\/curl \\\n  -X POST http:\/\/lakekeeper:8181\/management\/v1\/bootstrap \\\n  -H &quot;Content-Type: application\/json&quot; \\\n  --data '{&quot;accept-terms-of-use&quot;: true}'\n\n# Создание Warehouse\npodman run --rm --network faucet-stream_iceberg_net \\\n  -v &quot;$(pwd)\/create-default-warehouse.json:\/tmp\/wh.json&quot; \\\n  curlimages\/curl \\\n  -X POST http:\/\/lakekeeper:8181\/management\/v1\/warehouse \\\n  -H &quot;Content-Type: application\/json&quot; --data @\/tmp\/wh.json<\/code><\/pre><p><b>3. Запускаем Trino и настраиваем каталог:<\/b><\/p>\n<pre class=\"e2-text-code\"><code class=\"\"># Запуск Trino\npodman run -d --name trino --network faucet-stream_iceberg_net \\\n  -p 9999:8080 -e CATALOG_MANAGEMENT=dynamic trinodb\/trino:476\n\n# Ждем инициализации Trino (20 секунд)\necho &quot;Waiting for Trino to start...&quot;\nsleep 20\n\n# Создаем каталог lakekeeper в Trino\npodman exec trino trino --execute &quot;\nCREATE CATALOG lakekeeper USING iceberg\nWITH (\n    \\&quot;iceberg.catalog.type\\&quot; = 'rest',\n    \\&quot;iceberg.rest-catalog.uri\\&quot; = 'http:\/\/lakekeeper:8181\/catalog',\n    \\&quot;iceberg.rest-catalog.warehouse\\&quot; = 'demo',\n    \\&quot;iceberg.rest-catalog.nested-namespace-enabled\\&quot; = 'true',\n    \\&quot;iceberg.rest-catalog.vended-credentials-enabled\\&quot; = 'true',\n    \\&quot;iceberg.unique-table-location\\&quot; = 'true',\n    \\&quot;s3.region\\&quot; = 'us-east-1',\n    \\&quot;fs.native-s3.enabled\\&quot; = 'true',\n    \\&quot;s3.path-style-access\\&quot; = 'true',\n    \\&quot;s3.endpoint\\&quot; = 'http:\/\/silo:9000'\n);&quot;<\/code><\/pre><hr \/>\n<h4>🎯 Шаг 3: Запуск faucet и отправка вебхука<\/h4>\n<p><b>1. В новом окне терминала запускаем `faucet`:<\/b><\/p>\n<pre class=\"e2-text-code\"><code class=\"\">podman run --rm -it --network faucet-stream_iceberg_net \\\n  -p 8081:8080 -v &quot;$(pwd)\/pipeline.yaml:\/app\/pipeline.yaml&quot; \\\n  ghcr.io\/faucet-hq\/faucet-stream:full run \/app\/pipeline.yaml<\/code><\/pre><p><b>2. В третьем окне отправляем тестовый вебхук<\/b> (имитируем событие из Jira):<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">curl -X POST http:\/\/127.0.0.1:8081\/v1\/events \\\n  -H &quot;Authorization: Bearer 123&quot; \\\n  -H &quot;Content-Type: application\/json&quot; \\\n  -d '{&quot;event_id&quot;: &quot;evt-001&quot;, &quot;user&quot;: &quot;alice&quot;, &quot;action&quot;: &quot;login&quot;}'<\/code><\/pre><p>*(В логах `faucet` вы увидите `records_written=10` и `succeeded=1`).*<\/p>\n<p><b>3. Проверяем результат в Trino:<\/b><\/p>\n<pre class=\"e2-text-code\"><code class=\"\">podman exec trino trino --execute &quot;SELECT * FROM lakekeeper.demo.events;&quot;<\/code><\/pre><p><b>Вывод:<\/b><\/p>\n<pre class=\"e2-text-code\"><code class=\"\">&quot;evt-001&quot;,&quot;alice&quot;,&quot;login&quot;\n&quot;evt-002&quot;,&quot;alice&quot;,&quot;login&quot;\n...<\/code><\/pre><p>Готово! Вы только что превратили HTTP-события в структурированную Iceberg-таблицу. 🎉<\/p>\n<hr \/>\n<h4>💡 Дополнение 1: За пределами Webhook: faucet как ETL-платформа<\/h4>\n<p>Хотя в этой инструкции мы сфокусировались на кейсе с Webhook, важно понимать, что <b>`faucet-stream` — это не просто приемщик HTTP-запросов, а полноценная ETL-платформа<\/b>.<\/p>\n<p>Помимо Webhook, он поддерживает десятки других паттернов интеграции: от классического чтения REST API (с пагинацией и инкрементальной загрузкой) и баз данных (включая CDC из PostgreSQL и MySQL) до потоковой передачи из Kafka и чтения файлов из S3. Webhook — это лишь один из самых простых и быстрых способов начать, который идеально ложится на интеграцию с Jira, Stripe, GitHub и другими SaaS-сервисами.<\/p>\n<h5>🐛 О решенном баге и версиях<\/h5>\n<p>В ходе настройки мы столкнулись с ошибкой `StorageFactory must be provided for RestCatalog`. На момент выхода версии `v1.15.1` коннектор Iceberg в `faucet` не умел передавать фабрику хранилища для REST-каталогов. Команда проекта оперативно отреагировала на <a href=\"https:\/\/github.com\/faucet-hq\/faucet-stream\/issues\/962\">баг-репорт<\/a>, и исправление уже вошло в последующие релизы (в частности, в рамках PR #966).<\/p>\n<p>Теперь интеграция с Lakekeeper и Silo работает из коробки. Актуальный и стабильный образ всегда можно найти в официальном реестре: <b><a href=\"https:\/\/github.com\/faucet-hq\/faucet-stream\/pkgs\/container\/faucet-stream\">ghcr.io\/faucet-hq\/faucet-stream<\/a><\/b>.<\/p>\n<h5>🍏 Почему мы запускали Trino и Faucet вручную на macOS?<\/h5>\n<p>Если вы обратили внимание, в инструкции мы запускали инфраструктуру (Silo, Lakekeeper) через `podman-compose`, а `Trino` и `faucet` — отдельными командами `podman run`.<\/p>\n<p>Это не архитектурное требование, а вынужденный обход специфичного бага <b>`podman-compose` на macOS<\/b>. На Mac этот инструмент часто пытается объединить все контейнеры в один «Pod» и некорректно обрабатывает сложные цепочки зависимостей (`depends_on: condition: service_healthy`). В результате вы получаете общую ошибку `starting some containers: internal libpod error`, за которой невозможно разглядеть реальные логи контейнера.<\/p>\n<p>Ручной запуск через `podman run` с явным указанием сети (`--network faucet-stream_iceberg_net`) гарантирует, что контейнеры стартуют в правильном порядке, а вы всегда видите их реальные логи. На Linux-серверах или в Docker Desktop для Mac этот шаг можно смело делать через стандартный `docker-compose up -d`.<\/p>\n<p>В итоге, объединив мощь `faucet-stream`, современный REST-каталог Lakekeeper и движок Trino, вы получили гибкий и масштабируемый Lakehouse. Теперь любые события из внешних систем могут напрямую попадать в вашу аналитику без единой строчки кода! 🚀 Но не все так просто и радужно, как может показаться 🌈🦄 ...<\/p>\n<hr \/>\n<h4>💡 Дополнение 2: Архитектура faucet-stream, Push vs Pull и будущее потоковой обработки<\/h4>\n<p>В завершение статьи важно заглянуть «под капот» `faucet-stream` и обсудить два фундаментальных подхода к интеграции данных: <b>Push (Webhook)<\/b> и <b>Pull (REST API)<\/b>. Понимание этой разницы критически важно, так как от нее зависит надежность вашего будущего Lakehouse.<\/p>\n<h5>📤 Push-модель (Webhook) и её ограничения в faucet<\/h5>\n<p>В этой инструкции мы использовали `webhook` — это классическая <b>Push-модель<\/b>. Отправитель (например, Jira или Stripe) сам «толкает» событие на ваш сервер.<\/p>\n<ul>\n<li><b>Плюсы:<\/b> Мгновенная реакция на событие, минимальная задержка, не нужно писать код для опроса.<\/li>\n<li><b>Ограничения `faucet-stream`:<\/b> Важно понимать, что источник `webhook` в `faucet` — это <b>временный буфер<\/b>, а не долгоживущий сервер. Сервер запускается, собирает POST-запросы в память (в течение `timeout_secs` или до `max_payloads`), а затем завершается, сбрасывая данные в Iceberg.<\/li>\n<li><b>Режим `serve`:<\/b> Даже при запуске `faucet serve --triggers` триггер выступает лишь <b>оркестратором<\/b>. Он запускает пайплайн по событию, но само тело вебхука (`${trigger.body}`) не передается в пайплайн как готовый поток записей — его можно использовать только для параметризации источников внутри пайплайна.<\/li>\n<li><b>Риск потери:<\/b> Если сервис перезагрузится или окно приема истечет в момент отправки, событие будет потеряно (доставка *at-most-once*). Установка огромного `timeout_secs` не решает проблему, а лишь откладывает ее, создавая риск переполнения памяти (OOM).<\/li>\n<\/ul>\n<h5>📥 Pull-модель (REST API): Надежность для Production<\/h5>\n<p>Альтернатива — <b>Pull-модель<\/b>. Вместо того чтобы ждать вебхук, `faucet` сам регулярно опрашивает API внешней системы (например, Jira) с помощью источника `rest`.<\/p>\n<ul>\n<li><b>Плюсы:<\/b> Абсолютная надежность. Если `faucet` упал или был перезапущен, при следующем запуске он просто продолжит чтение с того места, где остановился (благодаря параметру `replication_method: Incremental`). Вы не потеряете ни одного события.<\/li>\n<li><b>Где использовать:<\/b> Для критически важных бизнес-данных, аналитики и построения Lakehouse лучше выбирать именно Pull-модель, запуская пайплайн по расписанию (например, через `cron` или `faucet schedule` каждые 5-15 минут).<\/li>\n<\/ul>\n<h5>🌊 Сравнение с RisingWave: Batch\/ETL против True Streaming<\/h5>\n<p>Учитывая слово `stream` в названии, многие ожидают от `faucet` поведения полноценной потоковой платформы, например, <b>RisingWave<\/b>.<\/p>\n<ul>\n<li><b>RisingWave<\/b> — это <b>потоковая база данных (True Streaming)<\/b>. Она выступает в роли постоянного получателя. Вы создаете таблицу с коннектором `webhook`, и все входящие HTTP-запросы немедленно попадают в нее и обрабатываются с низкой задержкой. Она держит состояние постоянно.<\/li>\n<li><b>faucet-stream<\/b> — это, прежде всего, <b>декларативный ETL-инструмент (Batch\/ELT)<\/b>. Его парадигма — выполнение задачи (run) от начала до конца. Несмотря на название, на данный момент `faucet` не является долгоживущим стриминговым движком в том же смысле, что RisingWave или Kafka Streams. Его сила — в огромном количестве коннекторов, эффективной пакетной обработке и простоте YAML-конфигурации.<\/li>\n<\/ul>\n<p><b>Взгляд в будущее:<\/b> Слово «stream» в названии оставляет надежду, что в будущих версиях `faucet` может обзавестись более нативными примитивами для непрерывной потоковой обработки. Но на сегодняшний день, если вам нужен <b>постоянный, вечно живой приемник вебхуков с гарантией сохранности каждого байта<\/b> (что нетривиально при текущем состоянии документации 😁), — это задача для потоковых баз данных (RisingWave) или связки «сервис-приемник + очередь (Kafka\/Redis)». Если же вам нужен <b>мощный и гибкий инструмент для периодического перемещения данных (ETL)<\/b> по расписанию или событию, `faucet-stream` с Pull-моделью — идеальный выбор.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-11-01-v-12.58.16.png\" width=\"1734\" height=\"656\" alt=\"\" \/>\n<\/div>\n",
            "date_published": "2026-10-31T01:37:44+03:00",
            "date_modified": "2026-11-02T23:32:52+03:00",
            "tags": [
                "big data",
                "Data",
                "Data Engineer",
                "DevOps"
            ],
            "image": "https:\/\/gavrilov.info\/pictures\/2026-11-02-v-23.31.41.png",
            "_date_published_rfc2822": "Sat, 31 Oct 2026 01:37:44 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "365",
            "_rss_enclosures": [],
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "highlight\/highlight.js",
                    "highlight\/highlight.css"
                ],
                "og_images": [
                    "https:\/\/gavrilov.info\/pictures\/2026-11-02-v-23.31.41.png",
                    "https:\/\/gavrilov.info\/pictures\/image-247.png",
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-11-01-v-12.58.16.png"
                ]
            }
        },
        {
            "id": "361",
            "url": "https:\/\/gavrilov.info\/all\/kagent-kubernetes-nativny-rantaym-dlya-ai-agentov-arhitektura-pr\/",
            "title": "kagent: Kubernetes-нативный рантайм для AI-агентов — архитектура, практика и альтернативы",
            "content_html": "<p><b>kagent<\/b> — это open-source платформа с открытым исходным кодом, которая привносит «агентный» искусственный интеллект в облачные и Kubernetes-среды. Если говорить проще, она позволяет создавать интеллектуальных агентов, которые понимают запросы на естественном языке и автономно выполняют сложные операции по управлению инфраструктурой: диагностируют проблемы с сетью, анализируют логи, управляют развертываниями. <a href=\"https:\/\/kagent.dev\">https:\/\/kagent.dev<\/a><\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-10-23-v-02.24.05.png\" width=\"1690\" height=\"554\" alt=\"\" \/>\n<\/div>\n<p>kagent был создан компанией Solo.io в 2025 году и является sandbox-проектом CNCF (Cloud Native Computing Foundation). Его главная идея — превратить AI-агента из простого чат-бота в полноценного участника DevOps- и платформенных процессов, управляемого теми же инструментами, что и остальные рабочие нагрузки в кластере.<\/p>\n<h3>🎯 Что дает kagent и как он работает<\/h3>\n<h4>Ключевые возможности<\/h4>\n<ul>\n<li><b>Управление как кодом (GitOps)<\/b>: Агенты определяются как обычные Kubernetes-ресурсы (Custom Resources). Вы можете версионировать их, применять через `kubectl` и управлять ими так же, как и любыми другими компонентами инфраструктуры.<\/li>\n<li><b>Автоматизация рутины<\/b>: Агенты самостоятельно выполняют многошаговые задачи по устранению неполадок и мониторингу, снижая нагрузку на специалистов.<\/li>\n<li><b>Гибкость и отсутствие привязки<\/b>: kagent поддерживает множество LLM-провайдеров (OpenAI, Anthropic, Gemini и другие), а также любые <b>OpenAI-совместимые эндпоинты<\/b>, что позволяет использовать практически любую модель, включая вашу собственную.<\/li>\n<li><b>Интеграция с инструментами<\/b>: Агенты могут использовать широкий набор инструментов (Tools) через Model Context Protocol (MCP) для взаимодействия с системами: просмотр логов подов, запросы к Prometheus, управление ресурсами Kubernetes и многое другое.<\/li>\n<li><b>Human-in-the-loop<\/b>: Встроенные механизмы подтверждения действий, когда агент предлагает изменить ресурс, но ждет одобрения человека, а также поддержка инициативных вопросов от агента.<\/li>\n<li><b>Долговременная память<\/b>: Векторная память, позволяющая агентам помнить контекст между сессиями, а не только в рамках последнего запроса.<\/li>\n<li><b>Agent-to-Agent (A2A)<\/b>: Агенты могут обнаруживать друг друга и вызывать, что позволяет строить многоагентные рабочие процессы с делегированием задач.<\/li>\n<\/ul>\n<h4>Архитектура<\/h4>\n<p>Архитектура kagent состоит из трех основных компонентов:<\/p>\n<ol start=\"1\">\n<li><b>Tools (Инструменты)<\/b>: Это «руки» агента — набор функций, которые агент может вызывать для выполнения реальных действий в инфраструктуре. kagent поставляется с предустановленными инструментами для отображения логов подов, запросов к Prometheus, генерации ресурсов и многого другого.<\/li>\n<li><b>Agents (Агенты)<\/b>: Это «мозг» системы — автономные системы, которые планируют, выполняют и анализируют задачи, используя доступные инструменты. Вы описываете агента в YAML-файле, указывая его системный промпт (роль), модель LLM и список инструментов, к которым у него есть доступ. Агенты могут объединяться в команды, где планирующий агент распределяет задачи между исполнителями.<\/li>\n<li><b>Framework (Фреймворк)<\/b>: Гибкий интерфейс, позволяющий запускать агентов как через UI, так и декларативно. Он построен на фреймворке Google ADK, но также поддерживает LangGraph, CrewAI и собственные фреймворки.<\/li>\n<\/ol>\n<h3>🐳 Практический пример: запуск kagent в Podman на macOS<\/h3>\n<p>Перейдем от теории к практике. Наш тестовый агент `myagent` был успешно запущен локально на MacBook (Intel) с использованием Podman. Он продемонстрировал возможность вызова инструментов (Tool Calling) и ведения диалога на русском языке.<\/p>\n<h4>Ключевые шаги развертывания<\/h4>\n<ol start=\"1\">\n<li><b>Подготовка окружения<\/b>: Установка Podman и `podman-compose`, а также создание скрипта-обертки `docker-compose`, который перенаправляет вызовы в `podman-compose`, поскольку kagent ожидает наличия именно этой утилиты.<\/li>\n<li><b>Создание проекта агента<\/b>: Генерация структуры проекта командой `kagent init adk python myagent`.<\/li>\n<li><b>Настройка базового образа<\/b>: В `Dockerfile` базовый образ был заменен на `kagent-adk:0.10.4-full`, так как distroless-вариант не содержит оболочки `\/bin\/sh`, необходимой для выполнения команд установки зависимостей. Также был добавлен шаг `RUN uv pip install “google-adk[extensions]”` для поддержки LiteLLM и указана переменная `ENV PYTHONPATH=\/app`.<\/li>\n<li><b>Конфигурация подключения к LLM<\/b>: В файле `agent.py` (и\/или `docker-compose.yaml`) были явно указаны параметры для подключения к вашему эндпоинту ProxyAPI:\n<ul>\n  <li>Модель: `openai\/z-ai\/glm-5.3-flash` (обязательный префикс `openai\/` для LiteLLM).<\/li>\n  <li>Базовый URL: `<a href=\"https:\/\/api.proxyapi.ru\/v1\">https:\/\/api.proxyapi.ru\/v1<\/a>`.<\/li>\n  <li>API-ключ: ваш токен от ProxyAPI.<\/li>\n<\/ul>\n<\/li>\n<li><b>Запуск<\/b>: Сборка образа и запуск контейнера через `podman-compose up`.<\/li>\n<\/ol>\n<h4>Результат: работающий агент с Tool Calling<\/h4>\n<p>На скриншоте видно, как агент `myagent` в интерактивном режиме `kagent run` отвечает на приветствие, а затем по запросу «давай кинем» выполняет вызов инструмента:<\/p>\n<ul>\n<li><b>Tool Call<\/b>: `roll_die` с параметром `{“sides”: 6}`.<\/li>\n<li><b>Tool Result<\/b>: `{“result”: 1}`.<\/li>\n<li><b>Ответ агента<\/b>: «Выпала <b>1<\/b> 🎲. На кубике с 6 гранями это минимальный результат — не повезло, но такое бывает! Хочешь бросить ещё раз или проверить число на простоту?»<\/li>\n<\/ul>\n<p>Этот пример наглядно демонстрирует полный цикл работы агента: понимание запроса, выбор и вызов нужного инструмента, обработка результата и формирование естественно-языкового ответа.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-10-23-v-02.18.20.png\" width=\"1796\" height=\"1008\" alt=\"\" \/>\n<\/div>\n<h3>⚖️ Сравнение с альтернативами<\/h3>\n<p>kagent — не единственный инструмент в этой нише. Чтобы понять его место, полезно сравнить его с другими решениями, которые решают смежные, но зачастую разные задачи. Подробные обзоры на Asya и Agent Autoscale появятся в отдельных статьях, а здесь мы приведем краткое сравнение.<\/p>\n<table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" class=\"e2-text-table\">\n<tr>\n<td style=\"text-align: center\">Инструмент<\/td>\n<td style=\"text-align: center\">Категория<\/td>\n<td style=\"text-align: center\">Ключевая абстракция<\/td>\n<td style=\"text-align: center\">Модель выполнения<\/td>\n<td style=\"text-align: center\">Масштабирование<\/td>\n<td style=\"text-align: center\">Основной сценарий<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: center\"><b>kagent<\/b><\/td>\n<td style=\"text-align: center\">Фреймворк для AI-агентов<\/td>\n<td style=\"text-align: center\">`Agent` (системный промпт + инструменты + LLM)<\/td>\n<td style=\"text-align: center\">Синхронный запрос\/ответ через контроллер и движок ADK<\/td>\n<td style=\"text-align: center\">❌ Нет встроенного автомасштабирования (агенты — это процессы, управляемые контроллером)<\/td>\n<td style=\"text-align: center\">Одиночные агенты для операционных задач в K8s (troubleshooting, мониторинг)<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: center\"><b>Asya<\/b><\/td>\n<td style=\"text-align: center\">Меш акторов для AI-нагрузок<\/td>\n<td style=\"text-align: center\">`AsyncActor` (stateless обработчик + очередь + sidecar)<\/td>\n<td style=\"text-align: center\">Хореография: сообщение несет свой маршрут через меш акторов<\/td>\n<td style=\"text-align: center\">✅ Поакторное масштабирование через KEDA (по глубине очереди), scale-to-zero<\/td>\n<td style=\"text-align: center\">Многошаговые AI\/ML пайплайны, агентные рабочие процессы в масштабе<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: center\"><b>Agent Autoscale<\/b><\/td>\n<td style=\"text-align: center\">Оператор для управления AI-агентами<\/td>\n<td style=\"text-align: center\">`SwarmAgent` (LLM-агент с промптом, инструментами, guardrails и автомасштабированием)<\/td>\n<td style=\"text-align: center\">Управляется оператором, масштабирование на основе KEDA<\/td>\n<td style=\"text-align: center\">✅ Масштабирование на основе спроса и scale-to-zero через KEDA<\/td>\n<td style=\"text-align: center\">Управление флотами AI-агентов как first-class ресурсами с автомасштабированием<\/td>\n<\/tr>\n<\/table>\n<h4>Другие заметные инструменты в экосистеме<\/h4>\n<p>Помимо вышеперечисленных, стоит упомянуть специализированные утилиты, которые часто используются в связке или как альтернативы для конкретных задач:<\/p>\n<ul>\n<li><b>K8sGPT<\/b>: AI-ассистент для быстрой диагностики кластера. Он сканирует ресурсы и объясняет проблемы на понятном языке, но по умолчанию не вносит изменений (read-only).<\/li>\n<li><b>HolmesGPT<\/b>: Инструмент для более глубокого расследования инцидентов. Он анализирует логи, метрики и состояние кластера для поиска корневой причины, с возможностью опционального исправления с подтверждением.<\/li>\n<li><b>kubectl-ai<\/b>: Утилита, которая превращает запросы на естественном языке в команды `kubectl`. Перед выполнением спрашивает подтверждение у пользователя.<\/li>\n<\/ul>\n<h3>💎 Заключение<\/h3>\n<p>kagent занимает важную нишу, предлагая Kubernetes-нативный, декларативный подход к управлению AI-агентами. Он идеально подходит для платформенных инженеров и DevOps-специалистов, которые хотят автоматизировать рутинные операции, используя знакомые им инструменты (YAML, `kubectl`, GitOps). В отличие от специализированных ассистентов (K8sGPT, HolmesGPT), kagent — это именно <b>фреймворк<\/b> для создания собственных агентов под конкретные задачи вашей инфраструктуры.<\/p>\n<p>Хотя kagent не является решением для масштабных асинхронных пайплайнов (как Asya) или автоматического масштабирования флотов агентов (как Agent Autoscale), его сила — в простоте, гибкости и глубокой интеграции с экосистемой Kubernetes.<\/p>\n<p>Успешный запуск агента с поддержкой Tool Calling в локальном окружении подтверждает, что kagent готов к практическому использованию и может стать мощным инструментом в арсенале вашей команды.<\/p>\n",
            "date_published": "2026-10-23T02:27:23+03:00",
            "date_modified": "2026-10-23T02:26:58+03:00",
            "tags": [
                "Agents",
                "AI",
                "DevOps"
            ],
            "image": "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-10-23-v-02.24.05.png",
            "_date_published_rfc2822": "Fri, 23 Oct 2026 02:27:23 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "361",
            "_rss_enclosures": [],
            "_e2_data": {
                "is_favourite": false,
                "links_required": [],
                "og_images": [
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-10-23-v-02.24.05.png",
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-10-23-v-02.18.20.png"
                ]
            }
        },
        {
            "id": "358",
            "url": "https:\/\/gavrilov.info\/all\/chasti-3-nastroyka-https-s-samopodpisnymi-sertifikatami-dlya-tra\/",
            "title": "Части 3: Настройка HTTPS с самоподписными сертификатами для Traefik и Marimo",
            "content_html": "<p>В предыдущей части мы настроили балансировку нагрузки через Traefik, но весь трафик ходил по HTTP — незашифрованным. В этой части мы добавим HTTPS с помощью самоподписных сертификатов. Это хороший способ подготовить инфраструктуру к продакшену, где вы сможете заменить их на сертификаты от Let’s Encrypt буквально одной строкой.<\/p>\n<p>Часть 1: <a href=\"https:\/\/gavrilov.info\/all\/hashicorp-nomad-razvertyvanie-interaktivnogo-python-prilozheniya\/\">HashiCorp Nomad: развертывание интерактивного Python-приложения на macOS<\/a><br \/>\nЧасть 2: <a href=\"https:\/\/gavrilov.info\/all\/chast-2-balansirovka-nagruzki-i-servis-diskaveri-s-consul-i-trae\/\">Балансировка нагрузки и сервис-дискавери с Consul и Traefik для Marimo<\/a><\/p>\n<hr \/>\n<h3>Зачем нужен HTTPS<\/h3>\n<ul>\n<li><b>Шифрование<\/b>: данные между браузером и сервером передаются в зашифрованном виде.<\/li>\n<li><b>Подготовка к продакшену<\/b>: архитектура с двумя точками входа (HTTP и HTTPS) и автоматическим редиректом — это стандартная схема.<\/li>\n<li><b>Доверие к сертификатам<\/b>: локально вы можете использовать самоподписные сертификаты, а в продакшене — Let’s Encrypt, не меняя структуру конфигурации.<\/li>\n<\/ul>\n<hr \/>\n<h3>Шаг 1: Генерация самоподписного сертификата<\/h3>\n<p>Для создания сертификата используем `openssl`, который уже есть в macOS. Важный момент: обязательно указываем <b>Subject Alternative Name (SAN)<\/b> с именем домена, иначе современные браузеры откажутся считать сертификат валидным.<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">mkdir -p ~\/marimo-app\/certs &amp;&amp; cd ~\/marimo-app\/certs\n\nopenssl req -x509 -nodes -days 365 \\\n  -newkey rsa:2048 \\\n  -keyout marimo.localhost.key \\\n  -out marimo.localhost.crt \\\n  -subj &quot;\/CN=marimo.localhost&quot; \\\n  -addext &quot;subjectAltName=DNS:marimo.localhost&quot;<\/code><\/pre><p>В папке `certs` появятся два файла:<\/p>\n<ul>\n<li>`marimo.localhost.key` — приватный ключ.<\/li>\n<li>`marimo.localhost.crt` — сертификат.<\/li>\n<\/ul>\n<p><b>Что произойдёт при открытии сайта<\/b>: браузер покажет предупреждение, что сертификат не является доверенным. Это ожидаемое поведение — вы можете принять исключение для домена `marimo.localhost`. В продакшене предупреждения не будет, так как сертификаты Let’s Encrypt автоматически доверенные.<\/p>\n<hr \/>\n<h3>Шаг 2: Обновление конфигурации Traefik<\/h3>\n<p>Traefik поддерживает разделение конфигурации на <b>статическую<\/b> (точки входа, провайдеры) и <b>динамическую<\/b> (маршруты, сертификаты). Мы создадим обе через блоки `template` в задании Nomad.<\/p>\n<h4>Статическая конфигурация<\/h4>\n<p>Добавим точку входа `websecure` на порту 8443 и настроим автоматический редирект с HTTP на HTTPS:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">[entryPoints]\n  [entryPoints.web]\n    address = &quot;:8080&quot;\n    [entryPoints.web.http.redirections.entryPoint]\n      to = &quot;websecure&quot;\n      scheme = &quot;https&quot;\n      permanent = true\n\n  [entryPoints.websecure]\n    address = &quot;:8443&quot;\n\n  [entryPoints.traefik]\n    address = &quot;:8081&quot;\n\n[api]\n  dashboard = true\n  insecure  = true\n\n[providers.consulCatalog]\n  prefix           = &quot;traefik&quot;\n  exposedByDefault = false\n\n  [providers.consulCatalog.endpoint]\n    address = &quot;127.0.0.1:8500&quot;\n    scheme  = &quot;http&quot;\n\n[providers.file]\n  filename = &quot;${NOMAD_TASK_DIR}\/dynamic.toml&quot;\n  watch    = true<\/code><\/pre><p><b>Ключевые моменты:<\/b><\/p>\n<ul>\n<li>`websecure` на порту `8443` — точка входа для HTTPS-трафика.<\/li>\n<li>`[entryPoints.web.http.redirections.entryPoint]` — HTTP-запросы автоматически перенаправляются на HTTPS.<\/li>\n<li>`[providers.file]` — подключает динамический провайдер, который читает наш файл с сертификатами.<\/li>\n<\/ul>\n<h4>Динамическая конфигурация<\/h4>\n<p>Здесь мы описываем сам сертификат:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">[tls]\n  [tls.stores]\n    [tls.stores.default]\n      [tls.stores.default.defaultCertificate]\n        certFile = &quot;\/Users\/yuriygavrilov\/marimo-app\/certs\/marimo.localhost.crt&quot;\n        keyFile  = &quot;\/Users\/yuriygavrilov\/marimo-app\/certs\/marimo.localhost.key&quot;\n\n  [[tls.certificates]]\n    certFile = &quot;\/Users\/yuriygavrilov\/marimo-app\/certs\/marimo.localhost.crt&quot;\n    keyFile  = &quot;\/Users\/yuriygavrilov\/marimo-app\/certs\/marimo.localhost.key&quot;<\/code><\/pre><p>Обратите внимание, что здесь используются <b>абсолютные пути<\/b>. Это работает, потому что мы запускаем Traefik через `raw_exec` — процесс работает прямо на хосте и имеет доступ ко всем файлам.<\/p>\n<hr \/>\n<h3>Шаг 3: Полное задание Traefik<\/h3>\n<p>Вот итоговый файл `traefik.nomad` с обеими конфигурациями:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">job &quot;traefik&quot; {\n  region      = &quot;global&quot;\n  datacenters = [&quot;dc1&quot;]\n  type        = &quot;service&quot;\n\n  group &quot;traefik&quot; {\n    count = 1\n\n    network {\n      port &quot;http&quot; {\n        static = 8080\n      }\n      port &quot;api&quot; {\n        static = 8081\n      }\n    }\n\n    service {\n      name = &quot;traefik&quot;\n      check {\n        name     = &quot;alive&quot;\n        type     = &quot;tcp&quot;\n        port     = &quot;http&quot;\n        interval = &quot;10s&quot;\n        timeout  = &quot;2s&quot;\n      }\n    }\n\n    task &quot;traefik&quot; {\n      driver = &quot;raw_exec&quot;\n\n      # Статическая конфигурация Traefik\n      template {\n        data = &lt;&lt;EOF\n[entryPoints]\n  [entryPoints.web]\n    address = &quot;:8080&quot;\n    [entryPoints.web.http.redirections.entryPoint]\n      to = &quot;websecure&quot;\n      scheme = &quot;https&quot;\n      permanent = true\n\n  [entryPoints.websecure]\n    address = &quot;:8443&quot;\n\n  [entryPoints.traefik]\n    address = &quot;:8081&quot;\n\n[api]\n  dashboard = true\n  insecure  = true\n\n[providers.consulCatalog]\n  prefix           = &quot;traefik&quot;\n  exposedByDefault = false\n\n  [providers.consulCatalog.endpoint]\n    address = &quot;127.0.0.1:8500&quot;\n    scheme  = &quot;http&quot;\n\n[providers.file]\n  filename = &quot;${NOMAD_TASK_DIR}\/dynamic.toml&quot;\n  watch    = true\nEOF\n        destination = &quot;local\/traefik.toml&quot;\n      }\n\n      # Динамическая конфигурация: сертификаты по абсолютному пути\n      template {\n        data = &lt;&lt;EOF\n[tls]\n  [tls.stores]\n    [tls.stores.default]\n      [tls.stores.default.defaultCertificate]\n        certFile = &quot;\/Users\/yuriygavrilov\/marimo-app\/certs\/marimo.localhost.crt&quot;\n        keyFile  = &quot;\/Users\/yuriygavrilov\/marimo-app\/certs\/marimo.localhost.key&quot;\n\n  [[tls.certificates]]\n    certFile = &quot;\/Users\/yuriygavrilov\/marimo-app\/certs\/marimo.localhost.crt&quot;\n    keyFile  = &quot;\/Users\/yuriygavrilov\/marimo-app\/certs\/marimo.localhost.key&quot;\nEOF\n        destination = &quot;local\/dynamic.toml&quot;\n      }\n\n      config {\n        command = &quot;\/usr\/local\/bin\/traefik&quot;\n        args = [\n          &quot;--configfile&quot;,\n          &quot;${NOMAD_TASK_DIR}\/traefik.toml&quot;\n        ]\n      }\n\n      resources {\n        cpu    = 100\n        memory = 128\n      }\n    }\n  }\n}<\/code><\/pre><hr \/>\n<h3>Шаг 4: Обновление тегов Marimo<\/h3>\n<p>В задании `marimo-job.nomad` нужно переключить сервис на точку входа `websecure` и включить TLS:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">service {\n  name = &quot;marimo-app&quot;\n  port = &quot;http&quot;\n  tags = [\n    &quot;traefik.enable=true&quot;,\n    &quot;traefik.http.routers.marimo.rule=Host(`marimo.localhost`)&quot;,\n    &quot;traefik.http.routers.marimo.entrypoints=websecure&quot;,\n    &quot;traefik.http.routers.marimo.tls=true&quot;,\n    &quot;traefik.http.services.marimo-app.loadbalancer.sticky.cookie=true&quot;,\n  ]\n  check {\n    type     = &quot;http&quot;\n    path     = &quot;\/&quot;\n    interval = &quot;2s&quot;\n    timeout  = &quot;2s&quot;\n  }\n}<\/code><\/pre><p><b>Что изменилось:<\/b><\/p>\n<ul>\n<li>`entrypoints=websecure` — трафик направляется на HTTPS-точку входа.<\/li>\n<li>`tls=true` — включена TLS-терминация для этого маршрута.<\/li>\n<\/ul>\n<hr \/>\n<h3>Шаг 5: Запуск и проверка<\/h3>\n<p>Перезапустите оба задания:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">nomad job stop traefik\nnomad job run traefik.nomad\nnomad job run marimo-job.nomad<\/code><\/pre><p>Откройте в браузере:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">https:\/\/marimo.localhost:8443<\/code><\/pre><div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-09-28-v-23.38.21.png\" width=\"1176\" height=\"1114\" alt=\"\" \/>\n<\/div>\n<p>Браузер покажет предупреждение о недоверенном сертификате. Нажмите «Дополнительно» → «Перейти на marimo.localhost (небезопасно)» или добавьте сертификат в системный раздел доверенных сертов (в macOS это делается через «Связка ключей»).<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-09-28-v-23.49.01.png\" width=\"1154\" height=\"736\" alt=\"\" \/>\n<\/div>\n<p>Если вы попробуете открыть `<a href=\"http:\/\/marimo.localhost:8080\">http:\/\/marimo.localhost:8080<\/a>`, Traefik автоматически перенаправит вас на HTTPS.<\/p>\n<hr \/>\n<h3>Возможные ошибки и их решения<\/h3>\n<h4>Ошибка: `Unclosed configuration block`<\/h4>\n<p><b>Причина:<\/b> незакрытые блоки `{ ... }` в HCL-файле.<\/p>\n<p><b>Решение:<\/b> внимательно проверьте, что все блоки `job`, `group`, `task`, `config`, `template` закрыты. В HCL отступы не важны, но фигурные скобки — критичны.<\/p>\n<h4>Ошибка: `Missing required argument: “command”` при `raw_exec`<\/h4>\n<p><b>Причина:<\/b> конфигурация скопирована из задания для `docker` (с полями `image`, `volumes`, `network_mode`), но применён драйвер `raw_exec`.<\/p>\n<p><b>Решение:<\/b> для `raw_exec` используйте поля `command` и `args`. Файлы конфигурации создавайте через `template`.<\/p>\n<h4>Ошибка: `volume_mount` не поддерживается драйвером `raw_exec`<\/h4>\n<p><b>Причина:<\/b> драйвер `raw_exec` работает напрямую с процессами хоста и не поддерживает монтирование томов.<\/p>\n<p><b>Решение:<\/b> используйте <b>абсолютные пути<\/b> к файлам (как мы сделали в динамической конфигурации). Или переходите на драйвер `docker`\/`podman` — там `volume_mount` работает штатно.<\/p>\n<h4>Ошибка: `entryPoint “websecure” doesn’t exist`<\/h4>\n<p><b>Причина:<\/b> имя точки входа в тегах сервиса не совпадает с именем в статической конфигурации Traefik.<\/p>\n<p><b>Решение:<\/b> убедитесь, что в конфиге есть блок `[entryPoints.websecure]`, а в тегах указано `traefik.http.routers.marimo.entrypoints=websecure`.<\/p>\n<hr \/>\n<h3>Итог<\/h3>\n<p>Мы добавили HTTPS в наш стек. Теперь архитектура выглядит так:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">Пользователь (HTTPS)\n     │\n     ▼\n  Traefik  ──►  Marimo 1 \/ 2 \/ 3\n(8443)              │\n     │              ▼\n     └─────────► Consul\n                    ▲\n                    │\n             Nomad Cluster<\/code><\/pre><ul>\n<li><b>HTTP-трафик<\/b> на порту 8080 автоматически перенаправляется на HTTPS.<\/li>\n<li><b>HTTPS-трафик<\/b> на порту 8443 расшифровывается Traefik и направляется на реплики Marimo.<\/li>\n<li><b>Сертификаты<\/b> хранятся на хосте и подключаются через абсолютные пути.<\/li>\n<\/ul>\n<p>В продакшене достаточно заменить пути к сертификатам и указать Traefik получать их автоматически от Let’s Encrypt — структура конфигурации останется той же. Мы разберём это в следующей части.<\/p>\n<p>А вот еще бонус:<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-10-01-v-21.07.56.png\" width=\"1728\" height=\"1268\" alt=\"\" \/>\n<\/div>\n<p>Все это через туннель <a href=\"https:\/\/cloudpub.ru\/docs\/http\">https:\/\/cloudpub.ru\/docs\/http<\/a><\/p>\n<p>Вот так:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">yuriygavrilov@MacBookPro ~ % .\/clo publish https marimo.localhost:8443\nСервис опубликован: https:\/\/marimo.localhost:8443 -&gt; https:\/\/thirdly-fast-bushbuck.cloudpub.ru:443<\/code><\/pre><p>Ну и конечно нужно DNS поправить, но работает и с авторизацией<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-10-01-v-21.10.22.png\" width=\"1360\" height=\"966\" alt=\"\" \/>\n<\/div>\n<p>Но лучше так не делать, все такие весь трафик так проходит через cloudpub<\/p>\n",
            "date_published": "2026-09-29T00:02:00+03:00",
            "date_modified": "2026-10-01T22:05:19+03:00",
            "tags": [
                "Dev",
                "DevOps",
                "IT"
            ],
            "image": "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-09-28-v-23.38.21.png",
            "_date_published_rfc2822": "Tue, 29 Sep 2026 00:02:00 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "358",
            "_rss_enclosures": [],
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "highlight\/highlight.js",
                    "highlight\/highlight.css"
                ],
                "og_images": [
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-09-28-v-23.38.21.png",
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-09-28-v-23.49.01.png",
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-10-01-v-21.07.56.png",
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-10-01-v-21.10.22.png"
                ]
            }
        },
        {
            "id": "357",
            "url": "https:\/\/gavrilov.info\/all\/chast-2-balansirovka-nagruzki-i-servis-diskaveri-s-consul-i-trae\/",
            "title": "Часть 2: Балансировка нагрузки и сервис-дискавери с Consul и Traefik для Marimo",
            "content_html": "<p>В первой части мы развернули приложение Marimo через Nomad, используя драйвер `raw_exec` и динамические порты. Приложение работало, но у него был существенный недостаток: каждая реплика была доступна по отдельному порту, а пользователю приходилось вручную искать актуальный адрес. Кроме того, отсутствовала отказоустойчивость — при падении одной реплики трафик не перераспределялся.<\/p>\n<p>Первая часть тут <a href=\"https:\/\/gavrilov.info\/all\/hashicorp-nomad-razvertyvanie-interaktivnogo-python-prilozheniya\/\">HashiCorp Nomad: развертывание интерактивного Python-приложения на macOS<\/a><\/p>\n<p>В этой статье мы решим эти проблемы с помощью <b>Consul<\/b> (сервис-дискавери) и <b>Traefik<\/b> (балансировщик нагрузки). Мы также настроим <b>липкие сессии<\/b> (sticky sessions), которые критически важны для stateful-приложений, таких как Marimo.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-09-28-v-22.55.24.png\" width=\"444\" height=\"160\" alt=\"\" \/>\n<\/div>\n<hr \/>\n<h3>Зачем нужны Consul и Traefik?<\/h3>\n<ul>\n<li><b>Consul<\/b> — распределённый каталог сервисов. Он хранит информацию о работающих репликах, проверяет их здоровье и предоставляет DNS-интерфейс. Без него балансировщик не знал бы, куда направлять трафик.<\/li>\n<li><b>Traefik<\/b> — современный обратный прокси и балансировщик нагрузки. Он автоматически обнаруживает сервисы через Consul Catalog Provider и распределяет запросы между здоровыми репликами.<\/li>\n<li><b>Nomad<\/b> — оркестратор, который управляет всеми компонентами.<\/li>\n<\/ul>\n<p>Итоговая архитектура:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">Пользователь\n     │\n     ▼\n  Traefik  ──►  Marimo 1 \/ 2 \/ 3\n(балансировщик)      │\n     │               ▼\n     └──────────► Consul\n                     ▲\n                     │\n              Nomad Cluster<\/code><\/pre><hr \/>\n<h3>Предварительные требования<\/h3>\n<p>Убедитесь, что у вас есть:<\/p>\n<ol start=\"1\">\n<li><b>Работающий Nomad<\/b> в режиме разработки (`nomad agent -dev -bind=0.0.0.0`).<\/li>\n<li><b>Проект `~\/marimo-app`<\/b> с файлами `app.py`, `pyproject.toml`, `uv.lock` и виртуальным окружением.<\/li>\n<li><b>Установленный Consul<\/b>.<\/li>\n<li><b>Установленный Traefik<\/b> (мы будем запускать его через Nomad, но бинарник должен быть доступен).<\/li>\n<\/ol>\n<h4>Установка Consul<\/h4>\n<pre class=\"e2-text-code\"><code class=\"\">brew tap hashicorp\/tap\nbrew install hashicorp\/tap\/consul<\/code><\/pre><h4>Установка Traefik<\/h4>\n<pre class=\"e2-text-code\"><code class=\"\">brew install traefik<\/code><\/pre><p>Проверьте пути:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">which consul   # \/usr\/local\/bin\/consul\nwhich traefik  # \/usr\/local\/bin\/traefik<\/code><\/pre><hr \/>\n<h3>Шаг 1: Запуск Consul в режиме разработки<\/h3>\n<p>Откройте отдельный терминал и запустите Consul:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">consul agent -dev -bind=0.0.0.0 -client=0.0.0.0<\/code><\/pre><p>Consul будет доступен по адресу `<a href=\"http:\/\/127.0.0.1:8500\">http:\/\/127.0.0.1:8500<\/a>`. Оставьте терминал открытым.<\/p>\n<p><b>Важно:<\/b> Consul должен быть запущен <b>до<\/b> того, как вы запустите Nomad с интеграцией с Consul, иначе сервисы не зарегистрируются.<\/p>\n<hr \/>\n<h3>Шаг 2: Обновление задания Marimo<\/h3>\n<p>Мы добавим в задание блок `service`, который зарегистрирует приложение в Consul и снабдит его тегами для Traefik. Также включим <b>липкие сессии<\/b>, чтобы запросы одного пользователя всегда попадали на одну и ту же реплику.<\/p>\n<p>Создайте файл `marimo-job.nomad`:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">job &quot;marimo-app&quot; {\n  datacenters = [&quot;dc1&quot;]\n  type = &quot;service&quot;\n\n  group &quot;app&quot; {\n    count = 3\n\n    network {\n      port &quot;http&quot; {}\n    }\n\n    service {\n      name = &quot;marimo-app&quot;\n      port = &quot;http&quot;\n      tags = [\n        &quot;traefik.enable=true&quot;,\n        &quot;traefik.http.routers.marimo.rule=Host(`marimo.localhost`)&quot;,\n        &quot;traefik.http.routers.marimo.entrypoints=web&quot;,\n        &quot;traefik.http.services.marimo-app.loadbalancer.sticky.cookie=true&quot;,\n      ]\n      check {\n        type     = &quot;http&quot;\n        path     = &quot;\/&quot;\n        interval = &quot;2s&quot;\n        timeout  = &quot;2s&quot;\n      }\n    }\n\n    task &quot;marimo&quot; {\n      driver = &quot;raw_exec&quot;\n\n      config {\n        command = &quot;\/Users\/yuriygavrilov\/marimo-app\/.venv\/bin\/marimo&quot;\n        args = [\n          &quot;run&quot;, &quot;app.py&quot;,\n          &quot;--headless&quot;,\n          &quot;--port&quot;, &quot;${NOMAD_PORT_http}&quot;,\n          &quot;--host&quot;, &quot;0.0.0.0&quot;\n        ]\n        work_dir = &quot;\/Users\/yuriygavrilov\/marimo-app&quot;\n      }\n\n      resources {\n        cpu    = 500\n        memory = 256\n      }\n    }\n  }\n}<\/code><\/pre><p><b>Что изменилось:<\/b><\/p>\n<ul>\n<li>`count = 3` — запускаем три реплики.<\/li>\n<li>Блок `service` — регистрирует сервис в Consul.<\/li>\n<li>Теги Traefik:\n<ul>\n  <li>`traefik.enable=true` — разрешает Traefik обслуживать сервис.<\/li>\n  <li>`traefik.http.routers.marimo.rule=Host(\\`marimo.localhost\\`)` — правило маршрутизации по домену.<\/li>\n  <li>`traefik.http.routers.marimo.entrypoints=web` — точка входа (должна совпадать с именем в конфиге Traefik).<\/li>\n  <li>`traefik.http.services.marimo-app.loadbalancer.sticky.cookie=true` — включает липкие сессии на основе cookie.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<hr \/>\n<p><b>Примечание про домен `.localhost`<\/b><\/p>\n<p>В примере используется домен `marimo.localhost`. <b>Добавлять его в `\/etc\/hosts` не нужно<\/b> — имена, заканчивающиеся на `.localhost`, автоматически резолвятся в `127.0.0.1` согласно стандарту <b>RFC 6761<\/b> (раздел 6.3) и <b>RFC 6762<\/b>. Это поведение поддерживается:<\/p>\n<ul>\n<li>системным резолвером macOS (`mDNSResponder`),<\/li>\n<li>всеми современными браузерами (Chrome, Safari, Firefox),<\/li>\n<li>а также большинством инструментов командной строки (`curl`, `ping` и т.д.).<\/li>\n<\/ul>\n<p>Именно поэтому `<a href=\"http:\/\/marimo.localhost:8080\">http:\/\/marimo.localhost:8080<\/a>` будет работать «из коробки» без правки системных файлов.<\/p>\n<p><b>Когда `\/etc\/hosts` всё-таки понадобится:<\/b><\/p>\n<p>Если вы хотите использовать другое имя — например, `marimo.dev`, `marimo.internal` или реальный домен вроде `marimo.example.com`, — тогда запись нужна:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">echo &quot;127.0.0.1 marimo.dev&quot; | sudo tee -a \/etc\/hosts<\/code><\/pre><p>Для локальной разработки и тестирования домен `.localhost` — самый удобный вариант: он не требует никаких дополнительных настроек и работает одинаково на любой машине.<\/p>\n<hr \/>\n<h3>Шаг 3: Создание задания для Traefik<\/h3>\n<p>Traefik будет запущен через `raw_exec`. Конфигурационный файл создаётся динамически с помощью шаблона Nomad.<\/p>\n<p>Создайте файл `traefik.nomad`:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">job &quot;traefik&quot; {\n  region      = &quot;global&quot;\n  datacenters = [&quot;dc1&quot;]\n  type        = &quot;service&quot;\n\n  group &quot;traefik&quot; {\n    count = 1\n\n    network {\n      port &quot;http&quot; {\n        static = 8080\n      }\n      port &quot;api&quot; {\n        static = 8081\n      }\n    }\n\n    service {\n      name = &quot;traefik&quot;\n      check {\n        name     = &quot;alive&quot;\n        type     = &quot;tcp&quot;\n        port     = &quot;http&quot;\n        interval = &quot;10s&quot;\n        timeout  = &quot;2s&quot;\n      }\n    }\n\n    task &quot;traefik&quot; {\n      driver = &quot;raw_exec&quot;\n\n      # Создаём конфигурационный файл Traefik через шаблон\n      template {\n        data = &lt;&lt;EOF\n[entryPoints]\n  [entryPoints.web]\n    address = &quot;:8080&quot;\n  [entryPoints.traefik]\n    address = &quot;:8081&quot;\n\n[api]\n  dashboard = true\n  insecure  = true\n\n# Включаем Consul Catalog Provider\n[providers.consulCatalog]\n  prefix           = &quot;traefik&quot;\n  exposedByDefault = false\n\n  [providers.consulCatalog.endpoint]\n    address = &quot;127.0.0.1:8500&quot;\n    scheme  = &quot;http&quot;\nEOF\n        destination = &quot;local\/traefik.toml&quot;\n      }\n\n      config {\n        command = &quot;\/usr\/local\/bin\/traefik&quot;\n        args = [\n          &quot;--configfile&quot;,\n          &quot;${NOMAD_TASK_DIR}\/traefik.toml&quot;\n        ]\n      }\n\n      resources {\n        cpu    = 100\n        memory = 128\n      }\n    }\n  }\n}<\/code><\/pre><p><b>Ключевые моменты:<\/b><\/p>\n<ul>\n<li>`driver = “raw_exec”` — запускаем бинарник напрямую.<\/li>\n<li>`template` — создаёт файл `traefik.toml` в директории задачи.<\/li>\n<li>`command` — полный путь к Traefik.<\/li>\n<li>`args` — указываем путь к созданному конфигу через переменную `${NOMAD_TASK_DIR}`.<\/li>\n<li>В конфиге Traefik точка входа называется `web` (порт 8080). Это имя должно совпадать с тегом `traefik.http.routers.marimo.entrypoints=web`.<\/li>\n<\/ul>\n<hr \/>\n<h3>Шаг 4: Запуск всех компонентов<\/h3>\n<p>Убедитесь, что Consul и Nomad запущены в отдельных терминалах.<\/p>\n<p>Затем в терминале, где вы работаете с заданиями:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">cd ~\/marimo-app\n\n# Запускаем приложение\nnomad job run marimo-job.nomad\n\n# Запускаем Traefik\nnomad job run traefik.nomad<\/code><\/pre><p>Проверьте статус:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">nomad job status marimo-app\nnomad job status traefik<\/code><\/pre><hr \/>\n<h3>Шаг 5: Проверка работы<\/h3>\n<ol start=\"1\">\n<li><b>Consul<\/b>: откройте `<a href=\"http:\/\/127.0.0.1:8500\">http:\/\/127.0.0.1:8500<\/a>` и убедитесь, что сервис `marimo-app` зарегистрирован.<\/li>\n<\/ol>\n<ol start=\"2\">\n<li><b>Traefik Dashboard<\/b>: откройте `<a href=\"http:\/\/127.0.0.1:8081\/dashboard\/\">http:\/\/127.0.0.1:8081\/dashboard\/<\/a>`. В разделе <b>HTTP Routers<\/b> должен быть маршрут `marimo` со статусом <b>Enabled<\/b>. В <b>HTTP Services<\/b> — сервис `marimo-app` с тремя здоровыми репликами.<\/li>\n<\/ol>\n<ol start=\"3\">\n<li><b>Приложение<\/b>: откройте `<a href=\"http:\/\/marimo.localhost:8080\">http:\/\/marimo.localhost:8080<\/a>`. Вы должны увидеть интерфейс Marimo с полем ввода имени и диаграммой. Проверьте, что при нажатии кнопки данные обновляются, а ошибка `Invalid session id` не появляется.<\/li>\n<\/ol>\n<hr \/>\n<h3>Возможные ошибки и их решения<\/h3>\n<p>В процессе настройки мы столкнулись с несколькими типичными проблемами. Вот они и способы их устранения.<\/p>\n<h4>Ошибка 1: `EntryPoint doesn’t exist entryPointName=web`<\/h4>\n<p><b>Причина:<\/b> в тегах сервиса указана точка входа `web`, а в конфигурации Traefik она называется иначе (например, `http`).<\/p>\n<p><b>Решение:<\/b> приведите имена к единому виду. В нашем случае мы переименовали точку входа в конфиге Traefik в `web`:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">[entryPoints]\n  [entryPoints.web]\n    address = &quot;:8080&quot;<\/code><\/pre><p>И в тегах указали `traefik.http.routers.marimo.entrypoints=web`.<\/p>\n<h4>Ошибка 2: `Invalid session id: s_bpy8u7`<\/h4>\n<p><b>Причина:<\/b> отсутствие липких сессий. Marimo — stateful-приложение, оно хранит сессию на конкретной реплике. Если запросы распределяются случайно, браузер попадает на разные реплики, и сессия теряется.<\/p>\n<p><b>Решение:<\/b> добавьте тег `traefik.http.services.marimo-app.loadbalancer.sticky.cookie=true`. Traefik установит cookie и будет направлять все запросы от одного браузера на одну и ту же реплику.<\/p>\n<h4>Ошибка 3: `Missing required argument: “command”` при использовании `raw_exec`<\/h4>\n<p><b>Причина:<\/b> конфигурация написана для драйвера `docker` (с полями `image`, `volumes`, `network_mode`), но применён `raw_exec`.<\/p>\n<p><b>Решение:<\/b> для `raw_exec` используйте `command` и `args`. Файлы конфигурации создавайте через `template`. Пример для Traefik приведён выше.<\/p>\n<h4>Ошибка 4: `raw_exec` отключён<\/h4>\n<p><b>Причина:<\/b> в некоторых сборках Nomad драйвер `raw_exec` отключён по умолчанию.<\/p>\n<p><b>Решение:<\/b> добавьте в конфигурацию клиента Nomad (например, `\/etc\/nomad.d\/nomad.hcl`):<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">plugin &quot;raw_exec&quot; {\n  config {\n    enabled = true\n  }\n}<\/code><\/pre><p>и перезапустите агент Nomad, но в моем случае это не понадобилось.<\/p>\n<hr \/>\n<h3>Итог<\/h3>\n<p>Мы добавили к нашему приложению Marimo два важных компонента:<\/p>\n<ul>\n<li><b>Consul<\/b> — для сервис-дискавери и проверки здоровья.<\/li>\n<li><b>Traefik<\/b> — для балансировки нагрузки и единой точки входа.<\/li>\n<\/ul>\n<p>Теперь приложение доступно по адресу `<a href=\"http:\/\/marimo.localhost:8080\">http:\/\/marimo.localhost:8080<\/a>`, а трафик автоматически распределяется между тремя репликами. Благодаря липким сессиям пользователи не теряют состояние при работе с интерактивным интерфейсом.<\/p>\n<p>Этот подход является основой для построения отказоустойчивых и масштабируемых систем. В следующих статьях мы рассмотрим настройку TLS\/HTTPS, использование Vault для управления секретами и автоматическое масштабирование.<\/p>\n<p>Поздравляю! Теперь ваше приложение готово к более серьёзным нагрузкам. 🚀<\/p>\n",
            "date_published": "2026-09-28T23:37:19+03:00",
            "date_modified": "2026-09-28T22:56:36+03:00",
            "tags": [
                "Dev",
                "DevOps",
                "IT"
            ],
            "image": "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-09-28-v-22.55.24.png",
            "_date_published_rfc2822": "Mon, 28 Sep 2026 23:37:19 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "357",
            "_rss_enclosures": [],
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "highlight\/highlight.js",
                    "highlight\/highlight.css"
                ],
                "og_images": [
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-09-28-v-22.55.24.png"
                ]
            }
        },
        {
            "id": "356",
            "url": "https:\/\/gavrilov.info\/all\/hashicorp-nomad-razvertyvanie-interaktivnogo-python-prilozheniya\/",
            "title": "HashiCorp Nomad: развертывание интерактивного Python-приложения на macOS",
            "content_html": "<p>В мире оркестрации контейнеров и приложений существует множество инструментов, но одним из самых гибких и простых в освоении является <b>HashiCorp Nomad<\/b>. Это оркестратор рабочих нагрузок, который позволяет развертывать как контейнеризированные, так и обычные приложения с использованием единого декларативного подхода.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-09-26-v-21.45.50.png\" width=\"286\" height=\"94\" alt=\"\" \/>\n<\/div>\n<p>В этой статье мы разберем, что такое Nomad, установим его на macOS, создадим простое интерактивное приложение на Marimo с полем ввода имени и генерацией случайных данных на столбчатой диаграмме, а затем развернем его с помощью Nomad, используя драйвер `raw_exec`. В конце обсудим ограничения такого подхода и путь к настоящему продакшену.<\/p>\n<hr \/>\n<h3>Что такое HashiCorp Nomad?<\/h3>\n<p><b>Nomad<\/b> — это гибкий оркестратор рабочих нагрузок, который позволяет развертывать и управлять любыми контейнеризированными или устаревшими приложениями с помощью единого рабочего процесса.<\/p>\n<h4>Ключевые особенности<\/h4>\n<ul>\n<li><b>Универсальность<\/b>: Nomad может запускать Docker-контейнеры, обычные приложения (без контейнеризации), микросервисы и пакетные задачи на одной инфраструктуре.<\/li>\n<li><b>Простота<\/b>: Nomad распространяется как единый бинарный файл, не требует внешних зависимостей для хранения или координации и автоматически обрабатывает сбои приложений, узлов и драйверов.<\/li>\n<li><b>Масштабируемость<\/b>: Nomad способен масштабироваться до 10 000+ узлов и имеет встроенную поддержку GPU-нагрузок.<\/li>\n<li><b>Гибкость<\/b>: Поддерживает различные типы задач: сервисы, пакетные задания и системные задачи. Работает на Linux, macOS и Windows.<\/li>\n<\/ul>\n<hr \/>\n<h3>Установка Nomad на macOS<\/h3>\n<p>Для установки Nomad на macOS удобнее всего использовать Homebrew. Выполните следующие команды в терминале:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\"># Добавляем репозиторий HashiCorp\nbrew tap hashicorp\/tap\n\n# Устанавливаем Nomad\nbrew install hashicorp\/tap\/nomad<\/code><\/pre><p>После установки проверьте версию:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">nomad version<\/code><\/pre><p>Пример вывода:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">Nomad v2.0.5\nBuildDate 2026-08-12T18:22:41Z\nRevision 5c8612bba6eb8e44cc3fc434125cb1b13463c309<\/code><\/pre><p><b>Важно<\/b>: На Intel Mac (x86_64) могут возникнуть проблемы с загрузкой бинарников из-за прекращения поддержки Apple. В этом случае рекомендуется использовать VPN для доступа к ресурсам HashiCorp. Если Nomad не устанавливается через Homebrew, можно собрать его из исходников или использовать MacPorts.<\/p>\n<hr \/>\n<h3>Запуск Nomad в режиме разработки<\/h3>\n<p>Для тестирования и разработки Nomad поддерживает режим `-dev`, который запускает одноузловой кластер на вашем компьютере:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">nomad agent -dev -bind=0.0.0.0<\/code><\/pre><p>Эта команда запустит Nomad, и веб-интерфейс будет доступен по адресу `<a href=\"http:\/\/127.0.0.1:4646\">http:\/\/127.0.0.1:4646<\/a>`. Оставьте этот терминал открытым — это ваш “сервер” Nomad.<\/p>\n<hr \/>\n<h3>Создание Marimo-приложения<\/h3>\n<h4>Установка `uv`<\/h4>\n<p>`uv` — это современный и быстрый менеджер пакетов для Python. Установите его:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">curl -LsSf https:\/\/astral.sh\/uv\/install.sh | sh<\/code><\/pre><h4>Создание проекта<\/h4>\n<pre class=\"e2-text-code\"><code class=\"\">mkdir ~\/marimo-app\ncd ~\/marimo-app\nuv init\nuv add marimo altair pandas numpy<\/code><\/pre><h4>Написание приложения<\/h4>\n<p>Создайте файл `app.py` со следующим содержимым. Приложение будет иметь поле для ввода имени, кнопку для генерации случайных значений и столбчатую диаграмму:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">import marimo\n\n__generated_with = &quot;0.11.0&quot;\napp = marimo.App()\n\n\n@app.cell\ndef _():\n    import marimo as mo\n    import altair as alt\n    import pandas as pd\n    import numpy as np\n    return alt, mo, np, pd\n\n\n@app.cell\ndef _(mo):\n    # Поле для ввода имени\n    name_input = mo.ui.text(\n        placeholder=&quot;Введите ваше имя...&quot;,\n        label=&quot;Имя пользователя&quot;\n    )\n    name_input\n    return (name_input,)\n\n\n@app.cell\ndef _(mo, name_input):\n    # Отображение приветствия\n    if name_input.value:\n        mo.md(f&quot;## Привет, **{name_input.value}**! 👋&quot;)\n    else:\n        mo.md(&quot;## Введите имя выше&quot;)\n    return\n\n\n@app.cell\ndef _(mo):\n    # Кнопка для генерации данных\n    generate_button = mo.ui.button(\n        label=&quot;🎲 Сгенерировать данные&quot;,\n        value=0,\n        on_click=lambda value: value + 1\n    )\n    generate_button\n    return (generate_button,)\n\n\n@app.cell\ndef _(alt, generate_button, mo, np, pd):\n    # Генерация случайных данных при нажатии кнопки\n    np.random.seed(generate_button.value)\n    categories = [&quot;Категория A&quot;, &quot;Категория B&quot;, &quot;Категория C&quot;, &quot;Категория D&quot;, &quot;Категория E&quot;]\n    values = np.random.randint(10, 100, size=5)\n    \n    df = pd.DataFrame({\n        &quot;Категория&quot;: categories,\n        &quot;Значение&quot;: values\n    })\n    \n    chart = alt.Chart(df).mark_bar().encode(\n        x=alt.X(&quot;Категория&quot;, sort=None),\n        y=&quot;Значение&quot;,\n        color=alt.value(&quot;#4C78A8&quot;)\n    ).properties(\n        title=&quot;Случайные значения по категориям&quot;,\n        width=500,\n        height=300\n    )\n    \n    mo.ui.altair_chart(chart)\n    return (chart,)\n\n\nif __name__ == &quot;__main__&quot;:\n    app.run()<\/code><\/pre><h4>Проверка локально<\/h4>\n<p>Запустите приложение в режиме редактирования:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">uv run marimo run app.py<\/code><\/pre><p>Убедитесь, что приложение работает: введите имя, нажмите кнопку — диаграмма должна обновиться. Закройте сервер (`Ctrl+C`).<\/p>\n<hr \/>\n<h3>Создание задания для Nomad<\/h3>\n<p>Создайте файл `marimo-job.nomad` в папке проекта:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">job &quot;marimo-app&quot; {\n  datacenters = [&quot;dc1&quot;]\n  type = &quot;service&quot;\n\n  group &quot;app&quot; {\n    count = 2\n\n    network {\n      port &quot;http&quot; {}\n    }\n\n    task &quot;marimo&quot; {\n      driver = &quot;raw_exec&quot;\n\n      config {\n        command = &quot;\/Users\/yuriygavrilov\/marimo-app\/.venv\/bin\/marimo&quot;\n        args = [\n          &quot;run&quot;, &quot;app.py&quot;,\n          &quot;--headless&quot;,\n          &quot;--port&quot;, &quot;${NOMAD_PORT_http}&quot;,\n          &quot;--host&quot;, &quot;0.0.0.0&quot;\n        ]\n        work_dir = &quot;\/Users\/yuriygavrilov\/marimo-app&quot;\n      }\n\n      resources {\n        cpu    = 500\n        memory = 256\n      }\n    }\n  }\n}<\/code><\/pre><p><b>Пояснения:<\/b><\/p>\n<ul>\n<li>`driver = “raw_exec”` — запускает процесс напрямую на хосте без изоляции.<\/li>\n<li>`count = 2` — запускаем две реплики приложения. Nomad автоматически распределит их по узлам (в нашем случае — по одному узлу, но на разных портах).<\/li>\n<li>`port “http” {}` — динамический порт. Nomad резервирует случайный свободный порт для каждой аллокации. Такой подход избавляет от конфликтов портов при запуске нескольких реплик.<\/li>\n<li>`command` — полный путь к исполняемому файлу `marimo` в виртуальном окружении.<\/li>\n<li>`${NOMAD_PORT_http}` — Nomad автоматически подставляет динамически выделенный порт через переменную окружения `NOMAD_PORT_<label>`.<\/li>\n<li>`work_dir` — рабочая директория, где находится `app.py`.<\/li>\n<\/ul>\n<p><b>Важно<\/b>: замените `\/Users\/yuriygavrilov\/` на реальный путь к вашей домашней директории.<\/p>\n<hr \/>\n<h3>Запуск задания<\/h3>\n<p>Убедитесь, что Nomad работает (в первом терминале). Во втором терминале выполните:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">nomad job run marimo-job.nomad<\/code><\/pre><p>Пример успешного вывода:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">==&gt; 2026-09-26T21:26:27+03:00: Monitoring evaluation &quot;cba695bd&quot;\n    2026-09-26T21:26:27+03:00: Allocation &quot;2327286c&quot; created: node &quot;7f836ccd&quot;, group &quot;app&quot;\n==&gt; 2026-09-26T21:26:27+03:00: Monitoring deployment &quot;9ea4ff53&quot;\n  ✓ Deployment &quot;9ea4ff53&quot; successful\n    \n    Deployed\n    Task Group  Desired  Placed  Healthy  Unhealthy  Progress Deadline\n    app         1        1       1        0<\/code><\/pre><hr \/>\n<h3>Как узнать порт приложения<\/h3>\n<p>После запуска задания выполните:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">nomad job status marimo-app<\/code><\/pre><p>Вы увидите таблицу аллокаций с их ID. Для каждой аллокации выполните:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">nomad alloc status &lt;ID_аллокации&gt;<\/code><\/pre><p>В выводе найдите секцию `Allocation Addresses`:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">Allocation Addresses:\nLabel  Dynamic  Address\n*http  yes      127.0.0.1:20938<\/code><\/pre><p>Откройте в браузере `<a href=\"http:\/\/127.0.0.1:20938\">http:\/\/127.0.0.1:20938<\/a>` — ваше приложение работает.<\/p>\n<p>Повторите команду для остальных аллокаций, чтобы получить порты всех реплик.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-09-26-v-21.48.33.png\" width=\"1172\" height=\"1080\" alt=\"\" \/>\n<\/div>\n<hr \/>\n<h3>Ограничения текущего подхода<\/h3>\n<p>Мы успешно запустили приложение, но важно понимать, что <b>это ещё не полноценное масштабированное решение<\/b>. Вот почему:<\/p>\n<h4>1. Нет балансировщика нагрузки<\/h4>\n<p>У нас работают <b>две реплики<\/b> на разных портах (`20938`, `21001` и т.д.). Чтобы обратиться к приложению, пользователь должен знать конкретный порт каждой реплики. Это неудобно:<\/p>\n<ul>\n<li>Нельзя дать пользователям один адрес, например `<a href=\"http:\/\/marimo.mycompany.com\">http:\/\/marimo.mycompany.com<\/a>`.<\/li>\n<li>Нет автоматического распределения трафика между репликами.<\/li>\n<li>Нет отказоустойчивости: если одна реплика упадёт, клиент, знающий только её порт, потеряет доступ.<\/li>\n<\/ul>\n<h4>2. Нет сервис-дискавери<\/h4>\n<p>Nomad не регистрирует сервисы автоматически (для этого нужен <b>Consul<\/b>). Без сервис-дискавери балансировщик не сможет узнать, какие реплики сейчас работают.<\/p>\n<h4>3. Минимальная изоляция<\/h4>\n<p>Драйвер `raw_exec` запускает процессы прямо на хосте. В продакшене это небезопасно: приложение имеет доступ ко всей файловой системе и может влиять на систему.<\/p>\n<hr \/>\n<h3>Путь к продакшену: что нужно добавить<\/h3>\n<h4>1. Балансировщик нагрузки (Traefik, NGINX, HAProxy)<\/h4>\n<p><b>Traefik<\/b> — самый простой вариант для интеграции с Nomad. Он умеет:<\/p>\n<ul>\n<li>Автоматически обнаруживать сервисы через Consul.<\/li>\n<li>Распределять трафик между всеми здоровыми репликами.<\/li>\n<li>Работать с TLS\/HTTPS “из коробки”.<\/li>\n<li>Иметь удобную панель управления.<\/li>\n<\/ul>\n<p><b>Как это выглядит в задании<\/b>: вы добавляете специальные теги в блок `service` вашего приложения, и Traefik сам настраивает маршрутизацию:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">service {\n  name = &quot;marimo-app&quot;\n  port = &quot;http&quot;\n  tags = [\n    &quot;traefik.enable=true&quot;,\n    &quot;traefik.http.routers.marimo.rule=Host(`marimo.localhost`)&quot;,\n  ]\n}<\/code><\/pre><p>Затем запускаете Traefik как отдельную задачу в Nomad, и он становится единой точкой входа для всех пользователей.<\/p>\n<h4>2. Сервис-дискавери (Consul)<\/h4>\n<p><b>Consul<\/b> — это распределённый каталог сервисов. Он:<\/p>\n<ul>\n<li>Хранит информацию о том, какие реплики сейчас работают и на каких портах.<\/li>\n<li>Предоставляет DNS-интерфейс: ваше приложение доступно как `marimo-app.service.consul`.<\/li>\n<li>Позволяет балансировщику динамически обновлять список бэкендов.<\/li>\n<\/ul>\n<h4>3. Контейнеризация (Docker\/Podman)<\/h4>\n<p>В продакшене используйте <b>контейнерные драйверы<\/b> вместо `raw_exec`:<\/p>\n<ul>\n<li><b>Изоляция<\/b>: приложение работает в своём окружении, не влияя на хост.<\/li>\n<li><b>Воспроизводимость<\/b>: одинаковое поведение на любой машине.<\/li>\n<li><b>Безопасность<\/b>: ограничения по ресурсам и правам доступа.<\/li>\n<\/ul>\n<h4>4. Полноценный кластер Nomad<\/h4>\n<p>В продакшене Nomad работает не как один процесс, а как <b>кластер<\/b>:<\/p>\n<ul>\n<li><b>3 серверных узла<\/b> (минимум) — хранят состояние и принимают решения о планировании.<\/li>\n<li><b>N клиентских узлов<\/b> — выполняют задачи.<\/li>\n<li>Интеграция с <b>Vault<\/b> для управления секретами.<\/li>\n<\/ul>\n<hr \/>\n<h3>Итоговая архитектура для продакшена<\/h3>\n<pre class=\"e2-text-code\"><code class=\"\">Пользователь\n     │\n     ▼\n  Traefik  ──►  Marimo 1 \/ 2 \/ 3\n(балансировщик)      │\n                     ▼\n                  Consul\n                     ▲\n                     │\n              Nomad Cluster<\/code><\/pre><hr \/>\n<h3>Заключение<\/h3>\n<p>Мы рассмотрели базовый процесс развертывания Python-приложения с помощью HashiCorp Nomad:<\/p>\n<ol start=\"1\">\n<li><b>Установили Nomad<\/b> через Homebrew.<\/li>\n<li><b>Запустили Nomad<\/b> в режиме разработки.<\/li>\n<li><b>Создали Marimo-приложение<\/b> с полем ввода имени и интерактивной диаграммой.<\/li>\n<li><b>Написали задание Nomad<\/b> с драйвером `raw_exec` и динамическим портом.<\/li>\n<li><b>Запустили две реплики<\/b> приложения и научились находить их порты.<\/li>\n<\/ol>\n<p>Этот подход отлично работает для <b>разработки и тестирования<\/b>. Но для настоящего продакшена необходимо:<\/p>\n<ul>\n<li>Добавить <b>балансировщик нагрузки<\/b> (Traefik, NGINX, HAProxy), чтобы пользователи обращались к одному адресу.<\/li>\n<li>Внедрить <b>сервис-дискавери<\/b> (Consul), чтобы балансировщик знал о работающих репликах.<\/li>\n<li>Использовать <b>контейнерные драйверы<\/b> (Docker, Podman) для изоляции.<\/li>\n<li>Построить <b>полноценный кластер Nomad<\/b> с тремя серверными узлами.<\/li>\n<\/ul>\n<p>Nomad — это мощный и гибкий инструмент, который подходит как для простых задач, так и для сложных распределённых систем. Его простота и универсальность делают его отличным выбором для разработчиков, которые хотят быстро развертывать приложения без излишней сложности Kubernetes.<\/p>\n<p>Следующий логичный шаг — добавить Traefik и Consul, чтобы превратить наш прототип в отказоустойчивое и масштабируемое решение. Об этом — в следующих статьях.<\/p>\n",
            "date_published": "2026-09-27T00:59:39+03:00",
            "date_modified": "2026-09-27T00:59:13+03:00",
            "tags": [
                "Dev",
                "DevOps",
                "IT"
            ],
            "image": "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-09-26-v-21.45.50.png",
            "_date_published_rfc2822": "Sun, 27 Sep 2026 00:59:39 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "356",
            "_rss_enclosures": [],
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "highlight\/highlight.js",
                    "highlight\/highlight.css"
                ],
                "og_images": [
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-09-26-v-21.45.50.png",
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-09-26-v-21.48.33.png"
                ]
            }
        },
        {
            "id": "307",
            "url": "https:\/\/gavrilov.info\/all\/svoy-heroku-doma-zapustil-dokku-doma-i-nakatil-novogodnego\/",
            "title": "Свой Heroku: запустил Dokku дома и накатил новогоднего :)",
            "content_html": "<p>Мы привыкли, что для развертывания веб-приложений нужно платить за VPS или разбираться в дебрях Kubernetes. Но если у вас есть домашний сервер (в моем случае — Asustor), вы можете создать свою собственную PaaS-платформу (Platform as a Service), которая работает по принципу *“git push — и готово”*.<\/p>\n<p>Сегодня я расскажу, как настроить <b>Dokku<\/b> через Portainer, запустить веселое Python-приложение к Новому 2026 году и поделюсь лайфхаками по оптимизации сборки и масштабированию.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-01-01-v-22.53.49.png\" width=\"2546\" height=\"936\" alt=\"\" \/>\n<\/div>\n<hr \/>\n<h3>Часть 1. Фундамент: Запуск Dokku в Portainer<\/h3>\n<p>Dokku — это Docker-контейнер, который управляет другими Docker-контейнерами. Чтобы он заработал на NAS, его нужно правильно запустить. Я использовал Portainer Stack.<\/p>\n<h4>Docker Compose конфигурация<\/h4>\n<p>Вот рабочий `docker-compose.yml`, который решает главную проблему — доступность приложений из локальной сети.<\/p>\n<pre class=\"e2-text-code\"><code class=\"\"># version: '3.2'\nservices:\n  agent:\n    image: dokku\/dokku:${VERSION}\n    pid: host.  # ⚠️ важно для работы на NAS\n    network_mode: bridge # ⚠️ важно для работы на NAS\n    environment:\n      DOKKU_HOSTNAME: ${DOKKU_HOSTNAME}\n      DOKKU_HOST_ROOT: ${DOKKU_HOST_ROOT}\n    volumes:\n      - \/var\/run\/docker.sock:\/var\/run\/docker.sock\n      - ${VOLUME_PATH:-\/var\/lib\/dokku}:\/mnt\/dokku\n    ports:\n      - &quot;3022:22&quot; # ⚠️ важно для работы через ssh и что бы не конфликтовал с 22 \n      - &quot;80:80&quot;    # Внешний порт 80 -&gt; порт 80 внутри контейнера Dokku\n      - &quot;443:443&quot;<\/code><\/pre><p><b>Нюансы сети:<\/b><br \/>\nЧтобы обращаться к приложениям по красивым домегам типа `my-app.dokku.datahub.mother`, я настроил <b>AdGuard Home<\/b> в качестве локального DNS-сервера (фильтры получил бонусом – где-то 20% это всякие счетчики, ужас:). Добавил правило Rewrite: `*.dokku.datahub.mother` → `192.168.0.20` (IP моего NAS). Теперь все поддомены (приложения) автоматически ведут на Dokku.<\/p>\n<p>Также для удобства я настроил `~\/.ssh\/config` на ноутбуке, чтобы не вводить порты вручную:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">Host dokku.datahub.mother\n  HostName 192.168.0.20\n  Port 3022\n  User dokku<\/code><\/pre><p>ну и ключ сам так можно добавить<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">echo &quot;ВАШ_ПУБЛИЧНЫЙ_КЛЮЧ&quot; | dokku ssh-keys:add dokku<\/code><\/pre><hr \/>\n<h3>Часть 2. Приложение “my-first-app”: Новогоднее гадание<\/h3>\n<p>Для теста я написал простое Flask-приложение, которое рассчитывает ваш возраст в наступающем 2026 году.<\/p>\n<h4>Код приложения (`app.py`)<\/h4>\n<pre class=\"e2-text-code\"><code class=\"\">from flask import Flask, request\n\napp = Flask(__name__)\n\n@app.route('\/')\ndef home():\n    return &quot;&quot;&quot;\n    &lt;h1&gt;Приветствую в игре Нового 2026 года! 🎉&lt;\/h1&gt;\n    &lt;p&gt;Это веселая интерактивная игра в честь Нового года. Угадай свой возраст на 1 января 2026!&lt;\/p&gt;\n    &lt;p&gt;Введи свой год рождения:&lt;\/p&gt;\n    &lt;form action=&quot;\/result&quot; method=&quot;get&quot;&gt;\n        &lt;input type=&quot;number&quot; name=&quot;birth_year&quot; min=&quot;1900&quot; max=&quot;2025&quot; required&gt;\n        &lt;button type=&quot;submit&quot;&gt;Угадать возраст на Новогодний 2026!&lt;\/button&gt;\n    &lt;\/form&gt;\n    &quot;&quot;&quot;\n\n@app.route('\/result')\ndef result():\n    birth_year = request.args.get('birth_year')\n    if not birth_year or not birth_year.isdigit():\n        return &quot;Ошибка: введи корректный год рождения!&quot;\n    \n    birth_year = int(birth_year)\n    age_in_2026 = 2026 - birth_year\n    return f&quot;&quot;&quot;\n    &lt;h2&gt;Результат: 🎇&lt;\/h2&gt;\n    &lt;p&gt;В 2026 году тебе будет {age_in_2026} лет!&lt;\/p&gt;\n    &lt;p&gt;Счастливого Нового 2026 года! Пусть все твои желания сбудутся!&lt;\/p&gt;\n    &lt;a href=&quot;\/&quot;&gt;Играть снова&lt;\/a&gt;\n    &quot;&quot;&quot;\n\nif __name__ == '__main__':\n    app.run(host='0.0.0.0', port=5000)<\/code><\/pre><h4>Подготовка к деплою<\/h4>\n<p>Чтобы Dokku понял, как запускать это чудо, нужны два файла в корне проекта:<\/p>\n<ol start=\"1\">\n<li><b>`requirements.txt`<\/b> (зависимости):<\/li>\n<\/ol>\n<pre class=\"e2-text-code\"><code class=\"\">Flask==2.3.2\ngunicorn==20.1.0\nWerkzeug==2.3.3<\/code><\/pre><ol start=\"2\">\n<li><b>`Procfile`<\/b> (команда запуска):<\/li>\n<\/ol>\n<pre class=\"e2-text-code\"><code class=\"\">web: gunicorn app:app --bind 0.0.0.0:5000<\/code><\/pre><ol start=\"3\">\n<li><b>`.python-version`<\/b> (опционально, явная версия Python):<\/li>\n<\/ol>\n<pre class=\"e2-text-code\"><code class=\"\">3.11.14<\/code><\/pre><h4>Процесс деплоя<\/h4>\n<p>Все делается через Git, как на “взрослых” платформах:<\/p>\n<ol start=\"1\">\n<li><b>Создаем приложение на сервере:<\/b><\/li>\n<\/ol>\n<pre class=\"e2-text-code\"><code class=\"\">ssh dokku@dokku.datahub.mother apps:create my-first-app<\/code><\/pre><ol start=\"2\">\n<li><b>Отправляем код:<\/b><\/li>\n<\/ol>\n<pre class=\"e2-text-code\"><code class=\"\">git init\n    git add .\n    git commit -m &quot;Happy New Year 2026 version&quot;\n    git remote add dokku dokku@dokku.datahub.mother:my-first-app\n    git push dokku master<\/code><\/pre><p>Если вы до этого еще что-то деплоили, то лучше проверить куда гит смотрит<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">git remote -v<\/code><\/pre><p>ну и поменяем еще не верное<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">git remote remove dokku<\/code><\/pre><pre class=\"e2-text-code\"><code class=\"\">git init\n    git add .\n    git commit -m &quot;Happy New Year 2026 version&quot;\n    git remote add dokku dokku@dokku.datahub.mother:my-first-app\n    git push dokku master<\/code><\/pre><p>После пуша Dokku сам скачает Python, установит Flask и запустит Gunicorn. Через минуту-две приложение доступно по адресу `<a href=\"http:\/\/my-first-app.dokku.datahub.mother\">http:\/\/my-first-app.dokku.datahub.mother<\/a>`.<\/p>\n<p>Еще нужно домен установить<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">ssh dokku@dokku.datahub.mother domains:set my-first-app my-first-app.dokku.datahub.mother<\/code><\/pre><p>или можно сразу глобально его установить:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">ssh dokku@dokku.datahub.mother domains:set-global dokku.datahub.mother\n# но тогда придется удалить ручную привязку \nssh dokku@dokku.datahub.mother domains:clear my-first-app\n\n# не забыть перебрать nginx ( на всякий случай ) \nssh dokku@dokku.datahub.mother proxy:build-config my-first-app<\/code><\/pre><p>Сертификат сгенерировать<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">ssh dokku@dokku.datahub.mother certs:generate my-first-app my-first-app.dokku.datahub.mother<\/code><\/pre><p>и проверить порты<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">ssh dokku@dokku.datahub.mother ports:report my-first-app<\/code><\/pre><p>если порты не корректные, то можно их установить так:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">ssh dokku@dokku.datahub.mother ports:set my-first-app http:80:5000 https:443:5000<\/code><\/pre><hr \/>\n<h3>Часть 3. Уровень PRO: Скорость (uv) и Marimo<\/h3>\n<p>Аппетит приходит во время еды. После простого Flask-приложения я решил развернуть что-то посерьезнее — Data Science ноутбук на <b>Marimo<\/b>, и столкнулся с реальными сложностями и особенностями. Для примера брал их дело ноутбук <a href=\"https:\/\/marimo.app\">https:\/\/marimo.app<\/a><\/p>\n<h4>1. Ускорение сборки с `uv`<\/h4>\n<p>Стандартный `pip` устанавливает пакеты медленно. Если проект большой, деплой может висеть минутами.<br \/>\nЯ перешел на <b>uv<\/b> — новый менеджер пакетов на Rust.<\/p>\n<p>Вместо `requirements.txt` я использовал `pyproject.toml` и `uv.lock`. Dokku (благодаря современным buildpacks) увидел `uv.lock` и переключился на быстрый режим. Время сборки сократилось <b>в разы<\/b>.<\/p>\n<h4>2. Ловушка масштабирования (Scaling)<\/h4>\n<p>Marimo — это <b>stateful<\/b> приложение (хранит состояние в памяти). Flask, который мы делали выше — <b>stateless<\/b>.<\/p>\n<p>Когда я задеплоил Marimo, Dokku по умолчанию все было хорошо, но потом я решил масштабировать его и сделал так<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">ssh dokku@dokku.datahub.mother ps:scale my-marimo-app web=3<\/code><\/pre><p>далее Dokku запустил <b>3 копии<\/b> контейнера (`web=3`).<br \/>\nНачался хаос:<\/p>\n<ul>\n<li>Интерфейс открывался.<\/li>\n<li>При нажатии кнопок вылетала ошибка `Invalid server token`.<\/li>\n<\/ul>\n<p><b>Почему?<\/b> Браузер загружал страницу с *Контейнера 1*, а WebSocket-запрос улетал в *Контейнер 2*, который ничего не знал про мою сессию.<\/p>\n<p><b>Решение:<\/b><br \/>\nДля интерактивных приложений (Streamlit, Marimo, Jupyter) всегда принудительно ставьте одну реплику:<br \/>\nНу ли придется делать липкие сессии на nginx или еще что-то.<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">ssh dokku.datahub.mother ps:scale my-marimo-app web=1 # все вернуло в рабочее состояние.<\/code><\/pre><p>А если не хватает мощности — лучше дайте этому единственному контейнеру больше ресурсов, чем пытаться плодить клонов или дайте каждому запускать свой:<\/p>\n<p>Вот так можно установить лимиты или повысить их:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">ssh dokku.datahub.mother resource:limit my-marimo-app --memory 2G --cpu 2<\/code><\/pre><h4>3. SSL в локальной сети<\/h4>\n<p>Браузеры блокируют микрофон и иногда WebSockets на HTTP-сайтах. Для локальной сети Let’s Encrypt не сработает (нет публичного IP), ну и его чуть сложнее запускать.<br \/>\nЯ решил вопрос генерацией самоподписанного сертификата одной командой Dokku:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">ssh dokku.datahub.mother certs:generate my-first-app my-first-app.dokku.datahub.mother<\/code><\/pre><p>Браузер ругается, но приложение работает полноценно.<\/p>\n<hr \/>\n<p>Еще я прогнал стресс тесты<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">ab -n 10000 -k -c 2000 ...<\/code><\/pre><p>Много они не показали, решением было подкрутить nginx, настроить кеш ssl, горизонтальное масштабирование не приносило больших результатов. я упирался в ограничения клиента при тестах нагрузки.<\/p>\n<h3>Итог<\/h3>\n<p>Dokku на домашнем сервере — это отличный инструмент.<\/p>\n<ul>\n<li><b>Для простых API (Flask\/FastAPI):<\/b> Работает “из коробки” идеально.<\/li>\n<li><b>Для сложных задач:<\/b> Использование `uv` делает работу комфортной, а понимание разницы между *Stateless* и *Stateful* приложениями спасает от занудных ошибок и отладки.<\/li>\n<\/ul>\n<p>Теперь my-first-app готово предсказывать возраст всем гостям на Новый год, а сервер готов к новым экспериментам! 🎄 Пожалуй оставлю его для будущих экспериментов. Прижился как-то быстро. Кстати у Dokku есть коммерческая PRO версия, а точнее не версия, а полноценный UI с кнопочками и стоит он 900$. <a href=\"https:\/\/dokku.com\/docs\/enterprise\/pro\/\">https:\/\/dokku.com\/docs\/enterprise\/pro\/<\/a><\/p>\n<p>Пора чего-нибудь накатить новогоднего еще :)<\/p>\n",
            "date_published": "2026-01-01T22:59:46+03:00",
            "date_modified": "2026-01-05T13:51:37+03:00",
            "tags": [
                "Dev",
                "DevOps",
                "IT",
                "Management"
            ],
            "image": "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-01-01-v-22.56.17.png",
            "_date_published_rfc2822": "Thu, 01 Jan 2026 22:59:46 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "307",
            "_rss_enclosures": [],
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "highlight\/highlight.js",
                    "highlight\/highlight.css"
                ],
                "og_images": [
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-01-01-v-22.56.17.png",
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2026-01-01-v-22.53.49.png"
                ]
            }
        },
        {
            "id": "300",
            "url": "https:\/\/gavrilov.info\/all\/kubevela-sovremennaya-dostavka-prilozheniy\/",
            "title": "KubeVela: Современная доставка приложений",
            "content_html": "<p>В мире Cloud-Native технологий Kubernetes стал операционной системой облака. Однако, как было отмечено в материалах про Rainbond, «ванильный» Kubernetes сложен для разработчиков. Если Rainbond решает эту проблему, предлагая готовый <b>PaaS «из коробки»<\/b> с упором на UI, то <b>KubeVela<\/b> подходит к задаче с другой стороны: это <b>двигатель для построения собственной платформы<\/b> (Platform Engine), основанный на модели OAM (Open Application Model).<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-01.09.35.png\" width=\"558\" height=\"632\" alt=\"\" \/>\n<div class=\"e2-text-caption\"><a href=\"https:\/\/kubevela.io\">https:\/\/kubevela.io<\/a><\/div>\n<\/div>\n<p>KubeVela позволяет создавать простые и ориентированные на приложение (Application-centric) абстракции, сохраняя при этом полную гибкость и управляемость через код (Infrastructure as Code).<\/p>\n<h3>Основная философия: OAM и разделение ответственности<\/h3>\n<p>В основе KubeVela лежит стандарт <b>OAM (Open Application Model)<\/b>. Эта модель разделяет ответственность между двумя ролями:<\/p>\n<ol start=\"1\">\n<li><b>Разработчиĸи (Developers):<\/b> Описывают <b>что<\/b> нужно запустить (образ контейнера, порт, параметры запуска).<\/li>\n<li><b>Платформенные инженеры (Platform Engineers):<\/b> Описывают <b>ĸаĸ<\/b> это должно работать (инфраструктура, политики масштабирования, Ingress, безопасность).<\/li>\n<\/ol>\n<p>Математически модель приложения в KubeVela можно выразить формулой:<\/p>\n<p>$$ Application = Components + Traits + Workflow $$<\/p>\n<p>Где:<\/p>\n<ul>\n<li><b>Components<\/b>: Основная рабочая нагрузка (Workload). Например, Docker-образ, Helm-чарт или даже Terraform-модуль.<\/li>\n<li><b>Traits<\/b> (Свойства\/Черты): Операционные характеристики, подключаемые к компоненту. Например: `ingress`, `scaler`, `sidecar`.<\/li>\n<li><b>Workflow<\/b>: Цепочка шагов доставки (Deploy -> Test -> Promote).<\/li>\n<\/ul>\n<h4>Пример манифеста KubeVela<\/h4>\n<p>Вместо десятков Kubernetes-ресурсов (Deployment, Service, Ingress, HPA), разработчик пишет один упрощенный файл:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">apiVersion: core.oam.dev\/v1beta1\nkind: Application\nmetadata:\n  name: website\nspec:\n  components:\n    - name: frontend\n      type: webservice\n      properties:\n        image: nginx\n      traits:\n        - type: scaler\n          properties:\n            replicas: 3\n        - type: gateway\n          properties:\n            domain: test.example.com<\/code><\/pre><h3>Сравнение: KubeVela vs Rainbond<\/h3>\n<p>Основываясь на прошлой статье про <a href=\"https:\/\/gavrilov.info\/all\/rainbond-oblachnaya-platforma-dlya-upravleniya-prilozheniyami\/\">Rainbond<\/a>, сделаем детальное сравнение. Оба инструмента решают проблему сложности Kubernetes, но нацелены на разные аудитории.<\/p>\n<table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" class=\"e2-text-table\">\n<tr>\n<td style=\"text-align: center\">Хараĸтеристиĸа<\/td>\n<td style=\"text-align: center\">Rainbond<\/td>\n<td style=\"text-align: center\">KubeVela<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: center\"><b>Философия<\/b><\/td>\n<td style=\"text-align: center\"><b>No-Code \/ Low-Code PaaS.<\/b> Готовая платформа с полным UI. Ориентация на визуальное управление.<\/td>\n<td style=\"text-align: center\"><b>Platform Engine \/ IaC.<\/b> Инструментарий для создания платформы. Ориентация на GitOps и конфигурацию как код.<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: center\"><b>Входной порог<\/b><\/td>\n<td style=\"text-align: center\"><b>Очень низкий.<\/b> Не нужно знать Kubernetes. Можно собрать приложение из `.jar` или исходного кода одной кнопкой.<\/td>\n<td style=\"text-align: center\"><b>Средний.<\/b> Требует понимания концепций OAM. Инженерам платформы нужно знать CUE (язык конфигурации).<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: center\"><b>Гибĸость<\/b><\/td>\n<td style=\"text-align: center\">Ограничена возможностями UI платформы. Сложно кастомизировать внутреннюю логику деплоя нестандартных вещей.<\/td>\n<td style=\"text-align: center\"><b>Высочайшая.<\/b> Любая абстракция настраивается через X-Definitions (на языке CUE). Можно описать любой ресурс K8s или Terraform.<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: center\"><b>Интеграция IaC<\/b><\/td>\n<td style=\"text-align: center\">Базируется на внутренних механизмах.<\/td>\n<td style=\"text-align: center\">Нативная интеграция с <b>Terraform<\/b>. KubeVela может разворачивать облачные базы данных (AWS RDS, Cloud SQL) как часть приложения.<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: center\"><b>Multi-cluster<\/b><\/td>\n<td style=\"text-align: center\">Есть управление несколькими кластерами, но фокус на единой панели управления.<\/td>\n<td style=\"text-align: center\"><b>Сильная сторона.<\/b> Мощный движок развертывания приложений сразу в несколько кластеров с политиками (Placement Policies).<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: center\"><b>Для ĸого<\/b><\/td>\n<td style=\"text-align: center\">SMB, стартапы, команды без сильной экспертизы в K8s, желающие получить опыт Heroku на своих серверах.<\/td>\n<td style=\"text-align: center\">Enterprise, Platform Engineering команды, желающие построить <b>свой<\/b> внутренний PaaS (IDP) по стандартам Spotify\/Netflix.<\/td>\n<\/tr>\n<\/table>\n<h4>Сравнение с другими аналогами<\/h4>\n<ol start=\"1\">\n<li><b>Crossplane<\/b>:\n<ul>\n  <li><b>Crossplane<\/b> фокусируется на «Инфраструктуре» (создание баз данных, кластеров, сетей в облаках через K8s API).<\/li>\n  <li><b>KubeVela<\/b> фокусируется на «Приложении».<\/li>\n  <li><b>Итог:<\/b> Они часто используются вместе. KubeVela использует Crossplane для заказа ресурсов.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n<ol start=\"2\">\n<li><b>ArgoCD \/ Flux (GitOps)<\/b>:\n<ul>\n  <li>Это инструменты непрерывной доставки (CD), которые синхронизируют Git и K8s.<\/li>\n  <li>KubeVela <b>может<\/b> использовать их под капотом или использовать свой собственный Workflow-контроллер для сложных сценариев (например, «засуспендить деплой, пока не пройдет тест, затем отправить уведомление в Slack»).<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n<ol start=\"3\">\n<li><b>Backstage<\/b>:\n<ul>\n  <li>Backstage — это «Портал» (Frontend). KubeVela — это «Движок» (Backend).<\/li>\n  <li>Существует плагин `vela-backstage`, который позволяет визуализировать приложения KubeVela внутри интерфейса Backstage. Это идеальная связка для построения IDP.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n<h3>Ключевые возможности KubeVela<\/h3>\n<h4>1. Программируемая абстраĸция (CUE Language)<\/h4>\n<p>В отличие от Helm, где шаблонизация — это просто подстановка строк, KubeVela использует язык <b>CUE<\/b> (Configure Unify Execute). Это позволяет платформенным инженерам создавать «умные» шаблоны.<\/p>\n<p>Пример логики на CUE внутри KubeVela: «Если пользователь не указал CPU, автоматически поставить `requests: 100m`, но если это ‘prod’ окружение, то поставить `500m`».<\/p>\n<h4>2. Единый Workflow для всего<\/h4>\n<p>KubeVela позволяет оркестрировать не только Kubernetes-ресурсы. В одном пайплайне вы можете:<\/p>\n<ol start=\"1\">\n<li>Создать S3 бакет в AWS (через Terraform\/Crossplane).<\/li>\n<li>Дождаться готовности.<\/li>\n<li>Задеплоить Deployment в Kubernetes.<\/li>\n<li>Отправить уведомление в Telegram.<\/li>\n<\/ol>\n<h4>3. Отсуствие «Config Drift»<\/h4>\n<p>KubeVela активно борется с дрейфом конфигураций, постоянно сверяя желаемое состояние приложения с реальным (State Reconciliation).<\/p>\n<h3>Итог и реĸомендации<\/h3>\n<p>Выбор между Rainbond и KubeVela зависит от того, <b>ĸто<\/b> будет обслуживать платформу и <b>ĸаĸие<\/b> цели стоят перед бизнесом.<\/p>\n<h4>Выбирайте Rainbond, если:<\/h4>\n<ul>\n<li>У вас небольшая или средняя команда разработчиков.<\/li>\n<li>Нет выделенного отдела Platform Engineering \/ DevOps.<\/li>\n<li>Нужно «вчера» перенести легаси (Java\/PHP\/Monolith) в контейнеры без переписывания кода.<\/li>\n<li>Вам нужен визуальный интерфейс для управления связями сервисов и мониторинга без написания YAML.<\/li>\n<li><b>Цель:<\/b> Быстрый Time-to-market с минимальными затратами на инженерию.<\/li>\n<\/ul>\n<h4>Выбирайте KubeVela, если:<\/h4>\n<ul>\n<li>Вы строите серьезный Internal Developer Platform (IDP) в крупной компании.<\/li>\n<li>У вас есть инженеры, знающие Kubernetes, но вы хотите скрыть его сложность от продуктовых разработчиков.<\/li>\n<li>Вам нужен строгий GitOps подход (все через репозиторий).<\/li>\n<li>Требуется гибридное развертывание: часть в K8s, часть в Serverless, часть в облачных RDS.<\/li>\n<li>Вы планируете использовать Backstage в качестве фронтенда.<\/li>\n<li><b>Цель:<\/b> Стандартизация, масштабируемость, полный контроль над абстракциями (Platform as Product).<\/li>\n<\/ul>\n<p><b>Рекомендация по стеĸу для Enterprise:<\/b><br \/>\nДля максимальной эффективности современный стек платформенной инженерии часто выглядит так:<br \/>\n$$ \\text{Backstage (UI)} + \\text{KubeVela (Engine)} + \\text{Crossplane (Infra)} $$<br \/>\nЭта связка дает удобство портала (как в Rainbond), мощь оркестрации (KubeVela) и управление облаком (Crossplane).<\/p>\n",
            "date_published": "2025-12-08T01:10:10+03:00",
            "date_modified": "2025-12-08T01:10:06+03:00",
            "tags": [
                "Dev",
                "DevOps",
                "Programming"
            ],
            "image": "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-01.09.35.png",
            "_date_published_rfc2822": "Mon, 08 Dec 2025 01:10:10 +0300",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "300",
            "_rss_enclosures": [],
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "highlight\/highlight.js",
                    "highlight\/highlight.css"
                ],
                "og_images": [
                    "https:\/\/gavrilov.info\/pictures\/Snimok-ekrana-2025-12-08-v-01.09.35.png"
                ]
            }
        }
    ],
    "_e2_version": 4171,
    "_e2_ua_string": "Aegea 11.4 (v4171e)"
}