Cluster RabbitMQ+HAProxy con Docker

Nell'ottica di "tutte le volte che mi invento qualcosa di interessante ora me lo segno", una rapida spiegazione di come costruire un cluster RabbitMQ (per MQTT) sotto Docker.

La struttura della cartella di lavoro è la seguente:

rabbitmq
 |--> etc
   |-> enabled_plugins
   |-> rabbitmq.conf
   |-> definitions.json

haproxy
 |--> haproxy.cfg
 
docker-compose.yml

Questo è il docker-compose, che si occupa di avviare le tre istanze di rabbitmq e l'istanza di haproxy:

version: "3.8"
services:

  haproxy:
    image: "haproxy:alpine"
    hostname: "haproxy"
    volumes:
      - "./haproxy/:/usr/local/etc/haproxy:ro"
    depends_on:
      - "rabbit1"
      - "rabbit2"
      - "rabbit3"
    ports: 
      # interfaccia web HAProxy
      - "6010:6010"
      # porta AMQP per cluster rabbitmq
      - "1883:1883"

  rabbit1:
    image: "rabbitmq:3.8.9-management-alpine"
    hostname: "rabbit1"
    volumes:
      - "./rabbitmq/etc/:/etc/rabbitmq/"
    ports:
      # interfaccia web
      - "6001:15672"
    environment:
      - "RABBITMQ_ERLANG_COOKIE=supersecretcookiestringsecret"
        
  rabbit2:
    image: "rabbitmq:3.8.9-management-alpine"
    hostname: "rabbit2"
    volumes:
      - "./rabbitmq/etc/:/etc/rabbitmq/"
    ports:
      # interfaccia web
      - "6002:15672"
    depends_on:
      - "rabbit1"
    environment:
      - "RABBITMQ_ERLANG_COOKIE=supersecretcookiestringsecret"
        
  rabbit3:
    image: "rabbitmq:3.8.9-management-alpine"
    hostname: "rabbit3"
    volumes:
      - "./rabbitmq/etc/:/etc/rabbitmq/"
    ports:
      # interfaccia web
      - "6003:15672"
    depends_on:
      - "rabbit1"
    environment:
      - "RABBITMQ_ERLANG_COOKIE=supersecretcookiestringsecret"

Nota che in questo assetto vengono aperte le seguenti porte:

  • 1883: MQTT
  • 6010: interfaccia web per amministrazione HAProxy
  • 6001/2/3: interfacce web per l'amministrazione dei nodi rabbitmq

Questi sono i file di configurazione di RabbitMQ (in questa configurazione abbiamo attivato l'interfaccia web di configurazione e il plugin MQTT). La riga relativa al caricamento del file delle definizioni può essere omessa al primo avvio; successivamente, dopo aver configurato il cluster mediante API o interfaccia web, si può esportare la configurazione stessa dalla pagina web e usare il file per avere il cluster già configurato per gli avvii successivi.

rabbitmq.conf

# Caricamento iniziale della struttura del server rabbit (se non disponibile
# si può omettere).
load_definitions=/etc/rabbitmq/definitions.json

cluster_formation.node_type = disc
cluster_formation.classic_config.nodes.1 = rabbit@rabbit1
cluster_formation.classic_config.nodes.2 = rabbit@rabbit2
cluster_formation.classic_config.nodes.3 = rabbit@rabbit3

enabled_plugins

[rabbitmq_management, rabbitmq_mqtt].

E questo è il file di configurazione di HAProxy (con attiva l'interfaccia web di gestione):

defaults
	timeout connect 5s
	timeout client 100s
	timeout server 100s

listen rabbitmq-servers-mqtt
	bind :1883
	mode tcp
	balance roundrobin
	timeout client 3h
	timeout server 3h
	server rabbit1 rabbit1:1883 check inter 5s rise 2 fall 3
	server rabbit2 rabbit2:1883 check inter 5s rise 2 fall 3
	server rabbit3 rabbit3:1883 check inter 5s rise 2 fall 3

frontend stats
    bind :6010
    mode http
    stats enable
    stats uri /
    stats refresh 10s
    stats admin if TRUE

Nota: al primo avvio il cluster RabbitMQ ha solo l'utente guest/guest, che può accedere solo da interfaccia locale (quindi da dentro la macchina). Per poter accedere all'interfaccia web è necessario creare un nuovo utente mediante riga di comando.

Per individuare il nome (container id) del container docker relativo al primo RabbitMQ:

docker ps

Per aprire una shell dentro il container:

docker exec -ti CONTAINERID /bin/bash

Per creare un nuovo utente e dargli accesso all'interfaccia web:

rabbitmqctl add_user NOMEUTENTE PASSWORD
rabbitmqctl set_user_tags NOMEUTENTE administrator

Come spiegato prima, usando l'interfaccia web si può ora esportare un file di configurazione, che potrà essere caricato nei successivi riavvii del cluster mediante la direttiva load_definitions del rabbitmq.conf.