{
    "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": "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",
            "date_published": "2026-09-29T00:02:00+03:00",
            "date_modified": "2026-09-28T23:51:44+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"
                ]
            }
        },
        {
            "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)"
}