Kafka

Kafka в режиме KRaft на Docker Swarm: пошаговая настройка кластера на Debian 12

Разворачиваем отказоустойчивый Kafka‑кластер в режиме KRaft поверх Docker Swarm на двух виртуальных машинах с Debian 12, с доступом через Kafka UI и SSH‑туннели.

Михаил Ровнягин

5 октября 2024 г.

Kafka
Docker Swarm
KRaft
Debian 12
DevOps
Kafka в режиме KRaft на Docker Swarm: пошаговая настройка кластера на Debian 12

Задача

В этой статье разберём, как с нуля развернуть минимальный Kafka‑кластер в режиме KRaft поверх Docker Swarm на двух виртуальных машинах с Debian 12. В результате мы получим:

  • два узла hl1.zil и hl2.zil с Docker Swarm;
  • кластер Kafka из двух брокеров/контроллеров в режиме KRaft;
  • UI для управления кластером (Kafka UI);
  • возможность безопасно работать с топиками (создавать, читать, писать) как с серверов, так и с локальной машины через SSH‑туннель.

Статья рассчитана на инженеров, которые уже уверенно работают с Linux и Docker, но только начинают погружаться в Kafka и KRaft.


1. Подготовка виртуальных машин Debian 12

Для примера будем использовать две виртуальные машины с Debian 12:

  • hl1.zil — первый узел кластера Kafka;
  • hl2.zil — второй узел кластера Kafka и менеджер Docker Swarm.

1.1. Базовые параметры VM

Минимальные рекомендуемые ресурсы (на каждую VM):

  • CPU: 2 vCPU
  • RAM: 4 ГБ (лучше 8 ГБ)
  • Диск: от 40 ГБ
  • Сеть: одна сетевая карта в одной подсети (например, 10.0.0.0/24)

Предположим, что IP‑адреса:

  • hl1.zil10.0.0.11
  • hl2.zil10.0.0.12

1.2. Настройка имён хостов и /etc/hosts

После установки Debian 12 на каждом узле зададим корректное доменное имя.

На hl1:

sudo hostnamectl set-hostname hl1.zil

На hl2:

sudo hostnamectl set-hostname hl2.zil

Перелогиньтесь в сессию, чтобы prompt отображал новое имя.

Далее пропишем имена узлов в /etc/hosts на каждом сервере:

sudo nano /etc/hosts

Пример содержимого:

127.0.0.1   localhost
10.0.0.11   hl1.zil hl1
10.0.0.12   hl2.zил hl2

Если у вас есть внутренняя DNS‑зона, можно завести A‑записи для hl1.zil и hl2.zil, но для простоты достаточно /etc/hosts.

1.3. Обновление системы и установка Docker

На обоих узлах обновим пакеты и установим Docker:

sudo apt update && sudo apt upgrade -y

# Утилиты
sudo apt install -y ca-certificates curl gnupg lsb-release

# Добавляем официальный репозиторий Docker
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg \
  | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

echo \
  "deb [arch=$(dpkg --print-architecture) \
  signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/debian \
  $(lsb_release -cs) stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update

# Docker Engine + CLI + плагин docker compose
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# (опционально) Добавим текущего пользователя в группу docker
sudo usermod -aG docker "$USER"

После добавления в группу docker перелогиньтесь или пересоздайте SSH‑сессию.

Проверим, что Docker работает:

docker info

2. Настройка Docker Swarm

Мы будем использовать hl2.zil как менеджер Swarm, а hl1.zil — как воркер.

2.1. Инициализация Swarm на менеджере

На hl2.zil:

sudo docker swarm init --advertise-addr <HL2_IP>

Подставьте реальный IP hl2.zil, например:

sudo docker swarm init --advertise-addr 10.0.0.12

Команда выведет пример строки для подключения воркеров, вида:

docker swarm join --token <SWARM_TOKEN> 10.0.0.12:2377

2.2. Подключение worker‑узла

На hl1.zil (worker) выполним команду с токеном, полученным на шаге выше:

sudo docker swarm join --token <SWARM_TOKEN> <HL2_IP>:2377

Пример:

sudo docker swarm join --token SWMTKN-1-... 10.0.0.12:2377

Проверяем состояние кластера на менеджере hl2.zil:

docker node ls

Вы должны увидеть два нода — один в роли Leader, второй как Worker.


3. Брандмауэр и открытие портов

Для работы Swarm, Kafka и Kafka UI откроем необходимые порты на обоих узлах.

Устанавливаем UFW:

sudo apt install ufw -y

Разрешаем нужные порты:

# Kafka internal port (broker communication)
sudo ufw allow 9092/tcp

# Kafka controller communication (KRaft)
sudo ufw allow 9093/tcp

# Kafka external port (клиентские подключения)
sudo ufw allow 9094/tcp

# Kafka UI
sudo ufw allow 8080/tcp

# Docker Swarm management
sudo ufw allow 2377/tcp

# Node‑to‑node communication (gossip)
sudo ufw allow 7946/tcp

# Overlay network (VXLAN)
sudo ufw allow 4789/udp

# Включаем UFW
sudo ufw enable

Статус:

sudo ufw status

4. Docker‑стек с Kafka в режиме KRaft и Kafka UI

Теперь опишем стек docker-compose.yml, который будет развёрнут в Docker Swarm через docker stack deploy.

Архитектура:

  • два сервиса kafka-1 и kafka-2 на основе confluentinc/cp-kafka:7.6.1;
  • оба узла выполняют роли broker,controller в режиме KRaft;
  • внешний листенер (EXTERNAL://) публикуется на 9094 в режиме host;
  • хосты:
    • kafka-1 закреплён за hl1.zil;
    • kafka-2 закреплён за hl2.zil;
  • Kafka UI (provectuslabs/kafka-ui) развёрнут на менеджере.

4.1. Генерация CLUSTER_ID

Kafka в режиме KRaft использует CLUSTER_ID. Сгенерируем его один раз и используем на обоих брокерах:

docker run --rm confluentinc/cp-kafka:7.6.1 kafka-storage random-uuid

Пример вывода:

2Sy4fmVPRlyy6abZuLghoA

Это значение должно быть одинаковым для всех брокеров кластера. Далее будем использовать его в переменной CLUSTER_ID.

4.2. Файл docker-compose.yml

На менеджере (hl2.zil) создадим файл docker-compose.yml:

nano docker-compose.yml

И поместим в него следующее содержимое:

version: '3.8'

networks:
  kafka-net:
    driver: overlay

volumes:
  kafka-1-data:
  kafka-2-data:

services:
  kafka-1:
    image: confluentinc/cp-kafka:7.6.1
    container_name: kafka-1
    networks:
      - kafka-net
    ports:
      - target: 9094
        published: 9094
        protocol: tcp
        mode: host
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_PROCESS_ROLES: 'broker,controller'
      # docker run --rm confluentinc/cp-kafka:7.6.1 kafka-storage random-uuid
      CLUSTER_ID: '2Sy4fmVPRlyy6abZuLghoA'
      KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka-1:9093,2@kafka-2:9093'
      KAFKA_LISTENERS: 'INTERNAL://:9092,CONTROLLER://:9093,EXTERNAL://:9094'
      KAFKA_ADVERTISED_LISTENERS: 'INTERNAL://kafka-1:9092,EXTERNAL://hl1.zil:9094'
      KAFKA_INTER_BROKER_LISTENER_NAME: 'INTERNAL'
      KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'INTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT'
      KAFKA_LOG_DIRS: '/var/lib/kafka/data'
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 2
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 2
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
      KAFKA_HEAP_OPTS: "-Xms1024m -Xmx1024m"
    volumes:
      - kafka-1-data:/var/lib/kafka/data
    deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints:
          - node.hostname == hl1.zil

  kafka-2:
    image: confluentinc/cp-kafka:7.6.1
    hostname: kafka-2
    container_name: kafka-2
    networks:
      - kafka-net
    ports:
      - target: 9094
        published: 9094
        protocol: tcp
        mode: host
    environment:
      KAFKA_NODE_ID: 2
      KAFKA_PROCESS_ROLES: 'broker,controller'
      CLUSTER_ID: '2Sy4fmVPRlyy6abZuLghoA'
      KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka-1:9093,2@kafka-2:9093'
      KAFKA_LISTENERS: 'INTERNAL://:9092,CONTROLLER://:9093,EXTERNAL://:9094'
      KAFKA_ADVERTISED_LISTENERS: 'INTERNAL://kafka-2:9092,EXTERNAL://hl2.zil:9094'
      KAFKA_INTER_BROKER_LISTENER_NAME: 'INTERNAL'
      KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'INTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT'
      KAFKA_LOG_DIRS: '/var/lib/kafka/data'
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 2
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 2
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
      KAFKA_HEAP_OPTS: "-Xms1024m -Xmx1024m"
    volumes:
      - kafka-2-data:/var/lib/kafka/data
    deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints:
          - node.hostname == hl2.zil

  kafka-ui:
    image: provectuslabs/kafka-ui:latest
    container_name: kafka-ui
    networks:
      - kafka-net
    ports:
      - "8080:8080"
    environment:
      KAFKA_CLUSTERS_0_NAME: 'Swarm KRaft Cluster'
      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: 'kafka-1:9092,kafka-2:9092'
      DYNAMIC_CONFIG_ENABLED: 'true'
    depends_on:
      - kafka-1
      - kafka-2
    deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints:
          - node.role == manager

Обратите внимание, что для обоих брокеров используется один и тот же CLUSTER_ID, а сервисы закреплены за конкретными узлами Swarm с помощью placement.constraints.


5. Запуск и управление Kafka‑кластером в Swarm

На менеджере (hl2.zil) разворачиваем стек:

docker stack deploy -c docker-compose.yml kafka_stack

Проверяем список сервисов стека:

docker stack services kafka_stack

Смотрим логи одного из брокеров (например, kafka-1):

docker service logs kafka_stack_kafka-1 -f

Просматриваем список задач (контейнеров) со всеми нодами:

docker stack ps --no-trunc kafka_stack

При необходимости остановить и удалить стек:

docker stack rm kafka_stack

После успешного запуска вы должны видеть оба брокера в состоянии Running, а Kafka UI — в статусе Running на порту 8080.


6. Доступ к Kafka UI

По умолчанию Kafka UI публикуется на порту 8080 менеджера кластера.

6.1. Доступ с внутренней сети

Если вы находитесь в той же сети, что и hl2.zil, достаточно открыть в браузере:

http://hl2.zil:8080/

6.2. Доступ через SSH‑туннель

Если кластер доступен только через bastion‑хост, можно использовать SSH‑туннель. Например:

ssh -p 2302 -L 8080:hl2.zil:8080 hl@hlssh.zil.digital

После установления туннеля UI будет доступен локально по адресу:

http://localhost:8080/

7. Работа с топиками: сервер‑сервер

Для работы с топиками удобно использовать дистрибутив Apache Kafka, совпадающий по версии с вашим кластером.

7.1. Загрузка и распаковка Kafka CLI

На одном из серверов (например, hl2.zil) скачиваем подходящую версию с официального сайта:

wget https://archive.apache.org/dist/kafka/3.7.1/kafka_2.13-3.7.1.tgz
tar -xzf kafka_2.13-3.7.1.tgz
cd kafka_2.13-3.7.1

7.2. Создание топика

Создадим тестовый топик test-topic с одной партицией и фактором репликации 1 (для продакшена обычно увеличивают оба параметра):

bin/kafka-topics.sh --create \
  --topic test-topic \
  --partitions 1 \
  --replication-factor 1 \
  --bootstrap-server hl2.zil:9094

Проверим параметры топика:

bin/kafka-topics.sh --describe \
  --topic test-topic \
  --bootstrap-server hl2.zil:9094

7.3. Отправка сообщений (producer)

Запустим консольный продюсер:

bin/kafka-console-producer.sh \
  --topic test-topic \
  --bootstrap-server hl2.zil:9094

В открывшейся консоли введите несколько сообщений:

test message 1
test message 2

Завершите ввод Ctrl+D.

7.4. Чтение сообщений (consumer)

Откроем консольного консюмера и прочитаем сообщения с начала топика:

bin/kafka-console-consumer.sh \
  --topic test-topic \
  --bootstrap-server hl2.zil:9094 \
  --from-beginning \
  --group tunnel-test-$(date +%s)

Вы увидите ранее отправленные test message 1 и test message 2.


8. SSH‑туннелирование для удалённой работы с Kafka

Часто Kafka‑кластер развёрнут во внутренней сети, а администратору нужно работать с ним с локальной машины, используя те же имена хостов (hl1.zil, hl2.zil). Решение — дополнительные loopback‑интерфейсы и SSH‑туннели.

8.1. Создание дополнительных loopback‑адресов

Linux

# Создание
sudo ip addr add 127.0.0.2/8 dev lo
sudo ip addr add 127.0.0.3/8 dev lo

# Удаление
sudo ip addr del 127.0.0.2/8 dev lo
sudo ip addr del 127.0.0.3/8 dev lo

macOS

# Создание
sudo ifconfig lo0 alias 127.0.0.2 netmask 255.0.0.0
sudo ifconfig lo0 alias 127.0.0.3 netmask 255.0.0.0

# Удаление
sudo ifconfig lo0 -alias 127.0.0.2
sudo ifconfig lo0 -alias 127.0.0.3

8.2. Настройка /etc/hosts на локальной машине

Добавьте в локальный /etc/hosts следующие строки:

127.0.0.2 hl2.zil
127.0.0.3 hl1.zil

Так мы «привяжем» имена hl1.zil и hl2.zil к локальным loopback‑адресам.

8.3. SSH‑туннель для Kafka и Kafka UI

Теперь настроим SSH‑туннель через bastion‑хост, пробросив порты Kafka и Kafka UI:

ssh -p 2302 \
  -L 8080:hl2.zil:8080 \
  -L 127.0.0.2:9094:hl2.zil:9094 \
  -L 127.0.0.3:9094:hl1.zil:9094 \
  hl@hlssh.zil.digital

После установления туннеля любые обращения к hl1.zil:9094 и hl2.zil:9094 на вашей локальной машине будут прозрачно проксироваться к соответствующим брокерам внутри кластера.

8.4. Чтение сообщений по туннелю

Теперь вы можете использовать локально установленный Kafka CLI (например, тот же kafka_2.13-3.7.1) и подключаться к кластеру по тем же именам хостов:

bin/kafka-console-consumer.sh \
  --topic test-topic \
  --bootstrap-server hl2.zil:9094 \
  --from-beginning \
  --group tunnel-test-$(date +%s)

Сообщения будут читаться через SSH‑туннель, при этом для приложений и инструментов всё выглядит так, будто кластер доступен напрямую.


9. Итоги

В этой статье мы:

  • подготовили две виртуальные машины на Debian 12 и настроили имена хостов hl1.zil и hl2.zil;
  • развернули кластер Docker Swarm с выделением ролей manager/worker;
  • настроили брандмауэр и необходимые порты для Swarm, Kafka и Kafka UI;
  • подняли кластер Kafka в режиме KRaft в виде Docker‑стека с двумя брокерами и Kafka UI;
  • научились управлять топиками и сообщениями через Kafka CLI;
  • настроили SSH‑туннели и дополнительные loopback‑интерфейсы для удобной удалённой работы с кластером.

Такой подход хорошо подходит для лабораторной среды, пилотных инсталляций и внутренних сервисов. При переносе в продакшен имеет смысл добавить аутентификацию и шифрование (SASL, TLS), мониторинг (Prometheus, Grafana) и бэкапы конфигурации.