Komodo - оркестрация на docker-compose

Нашел на просторах интернета очень крутой проект Komodo

Что что-то типа кубера, дженкинса и ансибла в одном флаконе, но для docker-compose.

Почему я не выбрал кубер для дома?

  1. Чистый K8S мега прожорлив и требует кучу серверов
  2. k3s, minikube и аналоги поинтересней для 1 сервера, но если у вас 2-3 ноды, то не все нормально кластеризируются
  3. Я пользую не только кубер, иногда чистый линукс, иногда виртуалки
  4. Кубер stateless работа с хранилищами не всегда оптимальна, быстра, и есть. Если у вас облако с безконечным выделением дисков, то один вопрос, если мини ПК, то вместо полноценного PVC имеем какие-то папки на сервере
  5. Довольно таки сложная конфигурация сервисов, если вы их сами пишете управляете, то это удобно, но для большинства самохостинг проектов есть docker run и все, в последнее время часто docker compose предлагается
  6. У меня несколько нод проксмокса, но я сам распределяю сервисы по назначению, нагрузке на хранилище, наличию всяких аппаратных компонентов типа корала, размазывать условный vaultwarden с БД на одной ноде и сервером на другой смысла не вижу, мне проще хранить все в одном компоненте и при желании переносить на другие ноды, заморачиваться с этим в кубере нет желания
  7. Самое простое бекапирование сервисов реализуется в Proxmox простым бэкапом контейнера

Мой подход к домашнему самохостингу описал в теме про Pulse, но если кратко, то

  1. Кластер Proxmox
  2. LXC контейнер с Alpine Linux и docker (смотрю в сторону podman) для каждого сервиса
  3. В LXC контейнере
    3.1. docker-compose с сервисом или сервисами если они связаны, например, frigate, doubletake, compreface
    3.2. вспомогательные агенты типа docke, portainer-agent, komodo-periphery
    3.3. proxmox-backup-client в docker-compose с сервисом для бэкапирования файлов на сервер PBS
  4. LXC контейнеры бэкапятся на PBS целиком
  5. Для критически важных сервисов Proxmox HA

Что я пробовал еще

  1. SSH и руки, но понятно, что не очень круто, зато работает
  2. CI/CD в целом, ок для деплоя, для мониторинга то-то надо искать
  3. Dockge - круто, что умеет docker run to docker-compose, но не очень круто работает с агентами, хорошо работает с существующими стеками
  4. Portainer - швейцарский нож, но у меня не удалось нормально работать с существующими стеками, надо создавать из самого портейнера
  5. Arcane - новый и перспективный проект, работает с существующими стеками, но агенты у него работают через одно место и очень плохо
  6. Komodo - пока понравился, умеет очень многое и даже больше, с существующими стеками не очень хорошо работает, но их можно типа импортировать
  7. Semaphore - это ansible UI, пока не осилил, но аналог 2 пункта с деплоем сервисов

Что умеет komodo

  1. Управлять стеками docker-compose, причем, как это сделано в dockge когда создается папка с именем стека и туда складывается compose.yaml файл, портейнер мне не понравился тем, что у него свои правила
  2. Работать с несколькими серверами, причем, это самая крутая тулза из всех
  3. Собирать образы
  4. Деплоить стеки
  5. Всякие автоматизации
  6. Мотиторинг, консоль, уведомления

Пока только первый день щупаю, ниже буду описывать сценарии использования

5 лайков

Сценарий 1

Самый просто сценарий

  1. Заходим в komodo
  2. Заходим в стеки
  3. Жмакаем создать новый стек и вводим имя
  4. Выбираем сервер для разворачивания (по-умолчанию это сам komodo, но можно добавить еще)
  5. Выбираем тип UI Defined и заполняем compose файл
  6. Далее жмакаем сохранить, деплой, и ждем успеха

Ну тут все как в docke, portainer особого смысла не вижу останавливаться подробно

Но…
в отличие от аналогов, тут конфиг сервиса хранится на севере с komodo, и потом деплоится на рабочие ноды, т.е. при потере связи с нодой вы не теряете ее стеки и можете передеплоить сервис на другую ноду

2 лайка

Сценарий 2

Чуть сложнее, мне нравится больше, может на нем остановлюсь

  1. Создаем GIT репозиторий на вашем Git сервере, это может быть github ну или в режиме самохостинга я остановился на Forgejo
  2. Создаем ветку с названием сервиса, например pulse и добавляем туда README и compose.yaml

    Причем, важно, чтобы был именно compose.yaml
  3. Создаем пользователя в GIT и добавляем креды в Komodo, ну понятно, это чтобы он мог скачать репу
  4. Создаем стек

    Тут надо указать сервер с репой, аккаунт, название репы и ветку
    Поскольку, у нас будет один репозиторий и в нем для каждого сервиса своя ветка, то указываем нужную ветку
  5. Настраиваем вебхук

    Тут по поводу Force deploy я пока не уверен
  6. Нажимаем Save а потом Deploy

    Видим, что у нас сервис задеплоился, мы сюда ее добавили ссылку
  7. Можем посмотреть что там у нас задеплоилось


    И даже провалиться в консоль контейнера

    А еще для сервера посмотреть системные ресурсы
  8. Идем в GIT и добавляем CI/CD с тригером деплоя сервиса

У меня ChatGPT вот такой скрипт написал

# Определяем текущую ветку без refs/heads/
        BRANCH="${GITHUB_REF#refs/heads/}"

        echo "Deploying branch: $BRANCH"

        # Подставляем ветку в URL
        URL="${KOMODO_URL/\{BRANCH\}/$BRANCH}"
        echo "Final webhook URL: $URL"

        # Готовим payload в формате GitHub push event
        PAYLOAD=$(jq -n \
          --arg ref "$GITHUB_REF" \
          --arg repo "$GITHUB_REPOSITORY" \
          --arg pusher "$GITHUB_ACTOR" \
          '{ref: $ref, repository: {full_name: $repo}, pusher: {name: $pusher}}'
        )

        echo "Payload: $PAYLOAD"

        # Считаем подпись
        SIGNATURE="sha256=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | sed 's/^.* //')"

        echo "Signature: $SIGNATURE"

        # Отправляем запрос и сохраняем ответ
        RESPONSE=$(mktemp)
        HTTP_CODE=$(curl -s -w "%{http_code}" -o "$RESPONSE" -X POST "$URL" \
          -H "Content-Type: application/json" \
          -H "X-Hub-Signature-256: $SIGNATURE" \
          -d "$PAYLOAD")

        echo "---- Komodo Response ----"
        cat "$RESPONSE"
        echo "-------------------------"

        if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then
          echo "❌ Webhook failed with HTTP status $HTTP_CODE"
          exit 1
        fi

        echo "✅ Webhook sent successfully! Status: $HTTP_CODE"

Тут важно учесть, что ветка в гите должна называться аналогично названию стека в komodo т.к. ссылка для webhook генерируется на основе ветки и CD тригеррит именно тот стек, который у нас сейчас обновился
9. Настраиваем секреты и переменны и коммитим код


Видим, что при коммите происходит обновление сервиса, а на 6 шаге видно было, что он деплоился

Ну и в качестве бонуса: можно создать шаблон сервиса, чтобы меньше вводить при его создании

2 лайка

Сценарий 3

Вот еще статья по настройке How To: Automate version updates for your self-hosted Docker containers with Gitea, Renovate, and Komodo
Но я пока не понял момент с Renovate

Статья описывает похожий на Сценарий 2

  1. Но разделение на стеки идет не на уровне веток, а одна ветка с каталогами стеков
  2. Далее, через Komodo идет скачивание GIT репозитория на сервер
  3. Стеки создаются на на основе git репы, а на основе файлов на сервере
  4. Через Renovate и вебхуки идет нотификация Komodo
  5. Komodo обновляет стеки при изменении
  6. В конце статьи еще описывается механизм обновления в случае выхода новых версий docker образов или типа того, но надо разбираться, сам komodo умеет обновлять стеки

Кстати, по поводу 6 пункта, из всех опробованных мною проектов только komodo имеет в бесплатной версии функционал отслеживания обновлений образов, watchtower подобные проекты остаются только.

2 лайка

Сценарий 0

Начал осваивать Semaphore UI - веб сервер для запуска Ansible, Python, Terrafor, Terragrunt, OpenTofu и что-то там еще

Написал Ansible Playbook, который получает на вход чистую операционку

  1. Обновляет пакеты, настраивает часовой пояс и прочее
  2. Устанавливает докер
  3. Устанавливает туда komodo агента
  4. Регистрирует сервер в Komodo
---
- name: Настройка ноды Komodo
  hosts: komodo_nodes
  gather_facts: false
  become: false
  roles:
    - common
    - docker
    - komodo_periphery
    - komodo_register

Полную версию тут не публикую по причине сырости и размеров

После этих манипуляций в komodo появляется новый сервер, на который можно развернуть приложение. По сути, даже не надо заходить в консоль для этого.

1 лайк

Сценарий 2.1

В продолжение спама про данный продукт
Освоил шаблоны для серверов и стеков

Суть в том, что можно настроить сервер или стек в качестве шаблона, и при добавлении нового просто выбрать нужный из списка и поля будут уже заполнены значениями из шаблона.

  • Я шаблон сервера использую при регистрации нового сервера ansible
  • Шаблоны стеков, например, для моего кластера трафика
  • Ну и мой основной шаблон стека git-template где заполнены все поля, при создании нового стека достаточно заполнить
  1. Название стека
  2. Север для разворачивания
  3. Название ветки в git репозитории

Все остальные данные заполняются автоматом, даже CI/CD описанный мною выше работает автоматически

не согласная я(с)

Очень хорошая заметка-статья получается.
Уверен, что найдет своего читателя.

Ром, лайк \ подписка \ колокольчик.

2 лайка

Подойдем к ложкам дегтя
Переменные окружения редактируются глобально для стека


При этом нет версионирования и безопасность так себе

Как я решил эту проблему?
OpenBao

services:
  guacamole:
    container_name: guacamole_compose
    env_file:
      - ./secrets/guacamole.env
    image: guacamole/guacamole:1.6.0
    volumes:
      - ./record:/record:rw
    restart: unless-stopped
  vault-agent:
    image: ghcr.io/openbao/openbao:latest
    cap_add:
      - IPC_LOCK
    env_file: .env
    volumes:
      - ./vault/agent-config.hcl:/etc/vault/agent-config.hcl
      - ./vault/templates:/templates
      - ./vault/secrets:/auth:ro
      - ./secrets:/secrets  
    command: ["agent", "-config=/etc/vault/agent-config.hcl"]

vault-agent подключается к хранилищу секретов и формирует .env файл для сервиса, при этом в самом гите этих кредов нет, через komodo они не задаются, а подгружаются диамически в зависимости от шаблонов и настроек доступа конкретного сервиса

И вот пример структуры сервиса в gitе

Тут есть

  • CI/CD pipline
  • Настройки волта с шаблонами для каждого сервиса
  • compose.yaml как основной файл, описывающий сервис
  • prepare.sh - пока временный костыль нужен для создания пустых .env файлов до старта агента т.к. docker compose не дает запустить агента, формирующего env файлы до того, как эти файлы будут созданы
  • README c описанием сервиса у меня он содержит название и краткое описание, ссылки на развернутое приложение, ссылки на github, сайт, документацию, информацию по бэкапированию, точки монтирования

prepare.sh, кстати, прописал в komodo

Продолжаю спамить плюшками: поддержка docker registry

Вроде данная фишка есть и в portainer, но я ранее настраивал через Ci/CD пайплайны и там была проблема в том. что если ты запускаешь docker pull не из пайплайна, а руками, то достпа к репозиторию нет.

Что это такое?
Если в используете собственные приватные билды докер образов, то приходится добавлять аутентификацию в приватном репозитории.

Часто используется когда вы разрабатываете свое приложение и публируете его в docker registry, а потом оно деплоится в окружение.

Для чего может понадобиться в самохостинге спросите вы?

Для удобной и чистой сборки кастомных образов готовых систем, например, у меня это на текущий момент

  1. OpenBao с добавленным softhsm2 для простого auto unseal в домашних условиях
  2. Nginx с кастомными страницами ошибок, чтобы интегрировать его в traefik

Ранее еще использовал на работе для не совсем каноничного, но рабочего подхода успорения пайплайнов, это когда вы вместо того, чтобы писать кучу apt-get install и настроек просто подключаете заранее собранный раннер, в котором уже установлены и настроены нужные компоненты, например, готовил такое для gitlab

Задача: автоматический деплой кластера из сервисов

Данную задачу можно реализовать средствами самого komodo автодеплоем при обновлении, но я предпочитаю webhook

Итак, пошагово

  1. В Komodo создаем шаблон сервиса

    Тут важно указать clone path и project name т.к. без этого не будет работать красиво
  2. Создаем стек на основе шаблона

    Указываем название стека, добавляем нужный тег и указываем сервер для разворачивания
  3. В итоге получаем вот такой список из стеков, развернутых на основе единой конфигурации
  4. Создаем Action deploy-traefik

    И прописываем в качестве action file следующий код
const TAG='traefik';
const isParallel = false;
const waitSecs = 60;

// Run actions using the pre initialized 'komodo' client.
const version: Types.GetVersionResponse = await komodo.read('GetVersion', {});
console.log('🦎 Komodo version:', version.version, '🦎\n');

const stacks: Types.ListStacksResponse = await komodo.read('ListStacks', {query: {tags: [ TAG ]}});
const pattern: string = stacks.map(item => item.name).join(', ');
console.log('🦎 Stacks: ', pattern);

function sleep(seconds: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, seconds * 1000));
}

if (isParallel) {
  await komodo.execute('BatchDeployStack', { pattern })
} else {
  for (const stack of stacks) {
    await komodo.execute('DeployStack', { stack: stack.id })
    await sleep(waitSecs);
  }
}

Что он делает?

  1. Вывод версии komodo (оставил из плейсхолдера)
  2. Выбирает стеки по тегу
  3. Вызывает деплой стеков по списку, который смогли найти
    При этом, если isParallel=true, то запускается параллельный деплой, а если isParallel=false , то по очереди, с задержкой waitSecs секунд, в итоге кластерные сервисы раскатываются постепенно
  1. Прописываем адрес с вебхука в forgejo

  2. В пайплайне указываем нужный адрес для тригрринья акшона

  3. В качестве альтернативы, можем без CI/CD прописать сам вебхук в настройках репозитория

1 лайк

Пример моего сервиса на примере komodo

В git создается следующая структура

.forgejo
└── workflows
    └── deploy.yml // Скрипт деплоя, описывал выше
├── compose.yaml // docker-compose файл
├── prepare.sh // костыль
├── README.md // Описание
└── vault
    ├── agent-config.hcl // Конфиг волта
    └── templates // Шаблоны волта
        ├── service.env.tpl
        ├── mongo.env.tpl
        ├── periphery.env.tpl
        └── proxmox-backup-client.env.tpl

compose.yaml

services:
  mongo:
    image: mongo
    labels:
      komodo.skip: # Prevent Komodo from stopping with StopAllContainers
    command: --quiet --wiredTigerCacheSizeGB 0.25
    restart: unless-stopped
    depends_on:
      vault-agent:
        condition: service_healthy
    volumes:
      - ./mongo-data:/data/db
      - ./mongo-config:/data/configdb
  
  core:
    image: ghcr.io/moghtech/komodo-core:${COMPOSE_KOMODO_IMAGE_TAG:-latest}
    labels:
      komodo.skip: # Prevent Komodo from stopping with StopAllContainers
    restart: unless-stopped
    depends_on:
      mongo:
        condition: service_started
      vault-agent:
        condition: service_healthy
    ports:
      - 9120:9120
    env_file:
      - secrets/komodo.env
    volumes:
      - ./backups:/backups

  periphery:
    image: ghcr.io/moghtech/komodo-periphery:${COMPOSE_KOMODO_IMAGE_TAG:-latest}
    labels:
      komodo.skip: # Prevent Komodo from stopping with StopAllContainers
    restart: unless-stopped
    env_file:
      - secrets/periphery.env
    depends_on:
      vault-agent:
        condition: service_healthy
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /proc:/proc
      - ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}:${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}
      - ${PERIPHERY_STACK_DIR:-/srv}:${PERIPHERY_STACK_DIR:-/srv}

  vault-agent:
    image: ghcr.io/openbao/openbao:latest
    cap_add:
      - IPC_LOCK
    env_file: .env
    volumes:
      - ./vault/agent-config.hcl:/etc/vault/agent-config.hcl
      - ./vault/templates:/templates
      - ./vault/secrets:/auth
      - ./secrets:/secrets
      - ./certs:/certs  
    command: ["agent", "-config=/etc/vault/agent-config.hcl"]
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "sh", "-c", "[ -s /secrets/komodo.env ] && [ -s /secrets/periphery.env ] && [ -s /secrets/mongo.env ] && [ -s /secrets/proxmox-backup-client.env ]" ]
      interval: 5s
      retries: 10
      start_period: 5s

  proxmox-backup-client:
    image: fdrake/proxmox-backup-client:latest
    env_file:
      - secrets/proxmox-backup-client.env
    environment:
      - BACKUP_TARGETS=backups.pxar:/mnt/backups
      - CRON_SCHEDULE=18 2 * * *
      - CUSTOM_HOST=komodo --ns apps
      - TZ=Europe/Moscow
    tmpfs: /tmp
    volumes:
      - ./backups/:/mnt/backups:ro
    restart: unless-stopped

Помимо самого komodo добавлены 2 сервиса

  1. vault-agent - Подключается к серверу BAO_ADDR с кредами в vault/secrets, берет шаблоны из vault/templates/ и ненерирует файлы секретов secrets/
healthcheck:
  test: ["CMD", "sh", "-c", "[ -s /secrets/komodo.env ] && [ -s /secrets/periphery.env ] && [ -s /secrets/mongo.env ] && [ -s /secrets/proxmox-backup-client.env ]" ]

Вот эта штука добавлена для того, чтобы ожидать наполнения .env файлов, изначально скриптом prepare.sh создаются пустые файлы и при помощи health check реализовано ошидание наполнения этих файлов секретами и только после этого запускаются зависимые сервисы

У сервиса, соответственно указывается

  depends_on:
      vault-agent:
        condition: service_healthy
  1. proxmox-backup-client - клиент бекапов, раз в сутки создает архив из содержимого ./backups и отправляет их в Proxmox Backup Server

prepare.sh

#!/bin/sh
if [ ! -d  secrets ]; then
  mkdir secrets
fi

chmod 777 secrets
chmod 777 vault/secrets

for f in vault/templates/*.env.tpl ; do
  env_file="secrets/$(basename $f .tpl)"
  echo -n  "Checking $env_file: "
  if [ ! -f $env_file ] ; then
    echo "create"
    touch $env_file ;
  else
    echo "ok"
  fi
done

Это прям костыль-костыль, тут есть проблема с правами и пока временно 777 ставится, но я местами поправил + при старте docker compose up ругается, что env_file: secrets/service.env не существует, поэтому файл создается пустым, а потом vault-agent его заполняет содержимым

vault/agent-config.hcl

auto_auth {
  method "approle" {
    mount_path = "auth/approle"
    config = {
      role_id_file_path                   = "/auth/role_id"
      secret_id_file_path                 = "/auth/secret_id"
      remove_secret_id_file_after_reading = false
    }
  }

  sink "file" {
    config = {
      path = "/auth/vault-token"
    }
  }
}

template {
  source      = "/templates/komodo.env.tpl"
  destination = "/secrets/komodo.env"
}

template {
  source      = "/templates/periphery.env.tpl"
  destination = "/secrets/periphery.env"
}

template {
  source      = "/templates/mongo.env.tpl"
  destination = "/secrets/mongo.env"
}

template {
  source      = "/templates/proxmox-backup-client.env.tpl"
  destination = "/secrets/proxmox-backup-client.env"
}

Тут все просто: берем креды из /auth/role_id и /auth/secret_id получаем токен доступа в /auth/vault-token и перегоняем template в файл секретов

vault/templates/komodo.env.tpl

{{ with secret "kv/services/komodo/komodo" }}
{{ range $key, $value := .Data.data -}}
{{ printf "%s=%s\n" $key $value }}
{{- end }}
{{- end }}

Шаблон для генерации .env файла на основе секретов

.forgejo/workflows/deploy.yml
Тут описывал выше, используется для отправки вебхука в komodo для запуска деплоя обновленного сервиса

Подготовка

  1. В OpenBao создается роль bao write auth/approle/role/komodo-role secret_id_ttl=0 token_ttl=60m token_max_ttl=120m policies='komodo, pbs-client'
  2. В OpenBao создается политика доступа komodo
# Чтение конкретного ключа (v2 требует префикса data/)
path "kv/data/services/komodo/*" {
  capabilities = ["read"]
}

# Возможность просматривать список ключей (опционально)
path "kv/metadata/services/komodo/*" {
  capabilities = ["list"]
}
  1. Вызывается скрипт ~/bin/deploy-vault-secret komodo-role komodo komodo
    С примерно таким содержимым
ROLE_ID=$(bao read -format=json auth/approle/role/$ROLE/role-id | jq -r '.data.role_id')
SECRET_ID=$(bao write -format=json -f auth/approle/role/$ROLE/secret-id | jq -r '.data.secret_id')

$SSH "cd /srv/; mkdir -p $SERVICE/vault/secrets; cd $SERVICE/vault/secrets; echo $ROLE_ID > role_id; echo $SECRET_ID > secret_id"

Который получает креды сервиса и создает 2 файла на сервере
4. Заносим креды в OpenBao


Деплой сервиса

Описывал выше

Только надо прописать переменную окружения в .env файл

Плюсы и минусы данного подхода

  1. IaC для сервисов, конфигурация сервиса находится в гите, из него деплоится
  2. Секреты хранятся в волте, автоматически подтягиваются в стеке на сервере + можно переиспользовать 1 секрет во вножестве сервисов
  3. Инкрементальное бекапирование волюмов на proxmox backup server
  4. Можно потерять сервер с сервисами и восстановить за считанные минуты (конфигуацию из гита, секреты из волта, вольюмы и БД из бекапа) но не на момент потери, а из резервной копии
  5. Komodo может отслеживать обновление образов то текущему тегу и по кнопке или автоматически обновлять сервис
  6. Полный контроль за работой сервисов, удобное обновление конфигурации без работы на удаленном сервере + мониторинг и у правление через веб интерфейс
  7. В этом после я ничего не блюрил т.к. и так все безопасно и приватно

Ну и куда же без минусов

  1. Сложнее, чем простой запуск через runtpi

Ну и в качестве бонуса как это выглядит в PBS

Сценарий 3. Автоматическая сборка docker образов

Для начала, я создал отдельный LXC контейнер для сборщика, ну чтобы разделить основной управляющий и сборочный
Создал builder с новым сервером

Создаем git репозиторий для нашего образа


Тут мы видим, что присутствует Dockerfile который и содержит инструкции для сборки

Создаем новый builder в komodo


Указываем откуда брать исходники

Куда заливать собранный образ

И варианты задания тегов

В toml это выглядит так

[[build]]
name = "netbox"
[build.config]
builder = "builder"
version = "3.4.1"
auto_increment_version = false
include_commit_tag = false
git_provider = "git.domain.com"
git_account = "komodo-bot"
repo = "docker/netbox"
image_registry = [
  { domain = "git.domain.com", account = "komodo-bot", organization = "docker" }
]

Можно настраивать через UI, а можно файл с этим содержимым добавить в отдельный git репозиторий и подтягивать конфигурацию оттуда

Жмем Build и получаем сборку с заливкой в репозиторий

После сборки получаем образ в репозитории

Чтобы сборка автоматически запускалась после пуша кода настраиваем webhook

Опционально, если не хочется использовать docker-compose, то можно задеплоить образ на сервер прямо из komodo

Либо, если используется полноценный стек, то можно создать процедуру, которая будет сначала собирать новый образ, а потом обновлять стек

Ну и если не заморачиваться с репозиториями, то можно написать Dockerfile прямо в UI

Какие минусы нашел

  • Не умеет работать с тегами, либо указывается ветка, либо хэш
  • Не умеет динамические версии, только инкремент
  • Multiarch сборка хоть и возможна, но не очевидна

Завел issue для полноценной интеграции с git, но думаю, что можно реализовать подобный функционал при помощи Actions

По multiarch вот инструкция

В prebuild добавляем следующий код

docker buildx inspect multiarch || docker buildx create --name multiarch --driver docker-container --use
docker buildx inspect --bootstrap

В Extra Args добавляем

  • --platform=linux/amd64,linux/arm64/v8 - указываем список архитектур для сборки
  • --builder=multiarch - указываем сборщик, который создали на PreBuild шаге
2 лайка

Ящерица прям понравилась. Теперь потихоньку перетаскиваю на неё (через forgejo) с портейнера.

По поводу сравнения с портейнером.

Портейнер - это по сути гуи оболочка для докерных команд. Разделы меню соответствуют докерным сущностям - сети, разделы, образа и т.п. Komodo же - это больше инструмент gitops (аналог ArgoCD или Flux для кубера).

Ящерица умеет сама обновлять образы если в композ файле не указана версия образа (ну или latest). НО!!! Такой подход в корне неправильный - обновления должны накатываться осознанно. И решение об обновлении должен принимать оператор, а не бездушные роботы (например небезызвестный immich, в котором через минорный выпуск идут breakable changes). Для этого и вводится обновление через renovatebot. Для этого в композ файлах указывается конкретная версия образа. renovate периодически проверяет есть ли обновления для этого образа и если есть обновление, то создаёт pull request в репе-хранилище композ файлов в котором описывает что меняется и при наличии github токена вытягивает ченджлог. От человека требуется просто ознакомиться с изменениями и нажать на кнопку для принятия PR или отклонения. Далее ящерица увидит что изменился композ файл стека, перекачает новый образ и перезапустит необходимый сервис. Таким образом мы имеем централизованное место в котором описываются изменения и кнопки для принятия решения об обновлении.

По поводу хранения секретов.

Я уже выше упоминал про gitops подход. Основной принцип - в гите лежит текущее состояние инфраструктуры. Соответственно, там же должны лежать и секреты. Но хранение секретов в гите в открытом виде, ну такое себе. Поэтому использую SOPS+age (SOPS - Secrets OPerationS GitHub - getsops/sops: Simple and flexible tool for managing secrets, age - GitHub - FiloSottile/age: A simple, modern and secure encryption tool (and Go library) with small explicit keys, no config options, and UNIX-style composability.).
Структура репо примерно такая:

.
├── .forgejo
│   └── workflows
│       └── dclint.yml
├── .gitignore
├── komodo.toml
├── renovate.json
├── .sops.yaml
└── stacks
    ├── dev
    │   └── qtg-pxy.dev.home.arpa
    │       ├── docker-compose.yml
    │       └── stack.env
    └── infra
        └── srv1.home.arpa
            ├── artifacts-store
            │   └── docker-compose.yml
            ├── postgres
            │   ├── docker-compose.yml
            │   └── secrets.env
            ├── qtg-pxy
            │   ├── docker-compose.yml
            │   └── secrets.env
            └── smtpbridge
                ├── docker-compose.yml
                └── secrets.env

Пример содержимого cat stacks/infra/srv1.home.arpa/postgres/secrets.env (особо длинные строки обрезал):

POSTGRES_USER=ENC[AES256_GCM,data:3kyUnkM=,iv:5FyiRPvQu5RFhnQc=,tag:0+3qbq1EkKIjQQ6NTV1gCQ==,type:str]
POSTGRES_PASSWORD=ENC[AES256_GCM,data:ZUIHqg==,iv:M89fr5U=,tag:fdARn3ZyMiWO3ofmGVDnBw==,type:str]
PGADMIN_DEFAULT_EMAIL=ENC[AES256_GCM,data:F6bzi1,iv:hS7aGOOTInjK7Jw=,tag:VyqxB5hgbL5tJ6bqEB6vcQ==,type:str]
PGADMIN_DEFAULT_PASSWORD=ENC[AES256_GCM,data:MXa10N8=,iv:tqr9FLoU9YEp7p5GSODn8H2fIDE/4xi8UTAt4Xmo6UU=,tag:6cvMy/4OAtioGG85P7Upfg==,type:str]
PGADMIN_OAUTH2_CLIENT_ID=ENC[AES256_GCM,data:HcD+lzIhDIW8Uw5y7w==,iv:Fr0czuaDBYnXgvQf8oyqSll4rsjQAgG8kRPyMV5Vdqc=,tag:SwAMkrxYywg/bLUkFP+hEQ==,type:str]
PGADMIN_OAUTH2_CLIENT_SECRET=ENC[AES256_GCM,data:IXqlMOLOc+lkneytI/NMO0ypiuu8PVfWZKRuyX6cSiY=,iv:OVikFQrnyItsqlS0S5PNXvlHDJz9j2mp8skTxyqu1+I=,tag:OuWafgRcNLYQOJz4q2n6Iw==,type:str]
sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3.....IiqhlIzlJS+TpTklfZhikcVhGNQ==\n-----END AGE ENCRYPTED FILE-----\n
sops_age__list_0__map_recipient=age111123333
sops_lastmodified=2025-10-20T20:58:43Z
sops_mac=ENC[AES256_GCM,data:ZJv4Jluw+UkMI=,tag:7mJzTp3A/8YpoWAgA8FeeA==,type:str]
sops_unencrypted_suffix=_unencrypted
sops_version=3.11.0

Через age генерируем пару - открытый и закрытый ключ. В .sops.yaml указываем например так:

creation_rules:
  # Все secrets.env в stacks/infra/** будут шифроваться ключом infra
  - path_regex: ^stacks/infra/.*/.*/secrets\.env$
    age:
      - age1111113 # INFRA_PUBLIC_KEY
    kms: []
    gcp_kms: []
    azure_keyvault: []
    hc_vault: []

То есть все файлы secrets.env в директории infra шифровать открытым ключом таким-то. В komodo сервере через конфиг добавляем секрет - закрытый ключ:

[secrets]
SOPS_AGE_INFRA_KEY = "AGE-SECRET-KEY-super-secret-key"

Далее в komodo в конфигурации стека в Pre Deploy добавляем:

SOPS_AGE_KEY=[[SOPS_AGE_INFRA_KEY]] sops -d secrets.env > .env

а в Post deploy

rm .env

Что происходит: перед запуском содержимое secrets.env расшифровывается в .env, данный файл автоматически подхватывает docker compose, запускается стек и файл удаляется.
НО!!! Есть нюанс - на periphery агента, где будут запускаться стеки с secrets.env необходимо дополнительно доставить sops и age

Тут согласен полностью, но я описываю все варианты работы с инструментом. У меня единичные сервисы типа wiremock обновляются автоматически, но большинство через ревью, особенно такие как immich где надо внимательно читать ченжлоги.

Это не gitops, а IaC подход все же. В целом, данный подход работы с секретами неплох, но имеет несколько недостатков, которые для дома особо не имеют значения, а вот в энтерпрайзе это может быть проблемой

  1. Настройкой секретов занимается оператор, обычно сервис описывает один человек, а секреты готовит другой, поэтому секреты выносятся в отдельное хранилище.
  2. Сложно переиспользовать секреты, например, креды от cloudflare приходится прописывать в каждом сервисе отдельно
  3. Как работать с различными окружениями? в целом, можно, но сложно
  4. Тут больше не управление секретами с точки зрения доступа к ним, а а шифрование, чтобы в гите не лежали в открытом виде. В энтерпрайзе же задача несколько шире
  5. Ну и с автообновлением секретов тоже сложности, у меня сейчас LE сертификаты лежат в волте и подтягиваются актуальые версии оттуда

Но для работы с инфрой “в одно рыло” SOPS+age будет более, чем достаточно

@KRom Оффтопну. А Semaphore будешь описывать?

У меня пока по komodo не все сценарии расписаны, а semaphore пока активно работаю с Ansible, хочу еще terragrunt/tofu настроить и может быть Python скрипты и уже потом, подумать над тем, чтобы расписать его тут.

1 лайк