Es folgen ein paar "Erkenntnisse", die ich beim Versuch gemacht habe, ein RabbitMQ-Cluster mit Hilfe eines Overlay-VPNs (nebula) und Nomad über drei unabhängige Availability Zonen aufzubauen.
Natürlich sind RabbitMQ-Cluster auf Nomad keine Neuigkeit:
- RabbitMQ clustering with Consul in Nomad
- Running a Secure RabbitMQ Cluster in Nomad
- Deploying a RabbitMQ Cluster with Three Nodes to Nomad
- Nomad Integrations: RabbitMQ
- RabbitMQ Cluster with Consul and Vault
- etc.
Das Overlay-VPN macht die Sache aber nochmal interessant - doch eins nach dem anderen.
RabbitMQ bietet per Plug-In mehrere "Discovery-Mechanismen" an. Mit Hilfe dieser Plugins (und entsprechender Backendsysteme) können sich damit RabbitMQ-Knoten, die sich zu einem Cluster zusammenfinden sollen, "automatisch" finden.
Da wir für Patroni auf nebula schon einen Consul-Cluster im Overlay-VPN aufgebaut haben, können wir den natürlich auch für RabbitMQ und das "Peer Discovery Using Consul" verwenden.
Das hat auch Andy Dote in "RabbitMQ clustering with Consul in Nomad" schon gemacht. Wir müssen uns also einen eigenen RabbitMQ-Container bauen, in dem wir einerseits das Plugin aktivieren:
FROM rabbitmq:management-alpine
COPY rabbitmq.conf /etc/rabbitmq
RUN rabbitmq-plugins enable --offline rabbitmq_peer_discovery_consul
und andererseits konfigurieren:
root@7384294c-b9f9-657d-f7e5-ce9b11cd3120:/mnt/rabbitmq# cat rabbitmq.conf
cluster_formation.peer_discovery_backend = consul
cluster_formation.consul.svc_addr_auto = true
cluster_formation.consul.scheme = http
cluster_formation.consul.host = 172.17.0.1
cluster_formation.consul.use_longname = false
cluster_formation.consul.svc_addr_use_nodename = true
Doch auch der Consul-Client und der Docker-Daemon müssen passend konfiguriert sein. Wie man oben sieht, konfigurieren wir das RabbitMQ-Plugin so, dass es den Consul-Client an der IP-Adresse der lokalen Docker-Bridge anspricht. Das ist aber nur möglich, wenn der Consul-Client mit
client_addr = "0.0.0.0"
konfiguriert wurde. Standardmäßig steht das auf 127.0.0.1. Aber Andy Dote macht das in seinem Beispiel auch so.
Aber RabbitMQ benötigt nicht nur eine Liste von IP-Adressen und Hostnamen, um ein Cluster bilden zu können: IP-Adressen können nicht verwendet werden und die Hostnamen müssen auch "resolvable" sein.
Selbst wenn in der jeweiligen Availability Zone ein dynamischer DNS-Dienst verfügbar wäre, würde das nichts nützen weil für unser Overlay-VPN keiner verfügbar zu sein scheint. Für nebula gibt es zwar einen "experimental Lighthouse DNS" aber bei defined.net konnte ich darauf noch keinen Hinweis finden (Supportanfrage ist raus).
Für diesen Fall bleibt dann eigentlich nur noch über, den Cluster-Knoten mit den entsprechenden Einträgen in die /etc/hosts zu versorgen. Das geht, in dem man dem Docker-Container den Parameter extra_hosts mitgeben. Z. B. so:
[...]
task "rabbitmq" {
driver = "docker"
config {
image = "<myregistry>/rabbitmq/rabbitmq-consul:2025110302"
hostname = "${attr.unique.hostname}"
ports = [ "ui", "epmd", "clustering", "amqp" ]
extra_hosts = ["nomad-client-prod1-0:100.102.0.23", "nomad-client-prod4-0:100.102.0.22", "nomad-client-triton-1:100.102.0.5"]
}
}
[...]
Damit können sich die Cluster-Knoten dann finden und ein Cluster bilden. Wenn man im Nomad-Job dann auch einen Service-Check definiert hat
[...]
service {
name = "rabbitmq"
port = "amqp"
tags = [ "v1" ]
check {
type = "tcp"
port = "amqp"
interval = "10s"
timeout = "2s"
}
}
[...]
werden die Cluster-Knoten automatisch im Consul registriert und sind natürlich dann auch per Consul-DNS auffindbar:
root@nomad-client-1:~# dig +short rabbitmq.service.consul
100.102.0.5
100.102.0.23
100.102.0.22
Das kann dann eventuell auch für die Clients, die auf den RabbitMQ-Cluster zugreifen wollen, interessant sein.
Ohne DNS-Unterstützung im Overlay-VPN ist eine "dynamische" Konfiguration – die man z. B. mit einem Node-Pool und ggfs. einer Autoscaling-Konfiguration realisieren könnte – für ein RabbitMQ-Cluster nicht sinnvoll.
Mit "statischen" Nomad-Client Hosts lässt sich so ein Cluster aber einfach (s. o.) realisieren.

Der Nomad Job dafür sieht dann ungefähr so aus:
job "rabbitmq-cluster" {
datacenters = [ "prod1", "prod4", "de-gt-2" ]
type = "service"
group "rabbitmq" {
count = 3
constraint {
operator = "distinct_hosts"
value = "true"
}
network {
port "amqp" {
host_network = "overlay"
static = 5672
}
port "ui" {
host_network = "overlay"
static = 15672
}
port "epmd" {
host_network = "overlay"
static = 4369
}
port "clustering" {
host_network = "overlay"
static = 25672
}
}
service {
name = "rabbitmq"
port = "amqp"
tags = [ "v1" ]
check {
type = "tcp"
port = "amqp"
interval = "10s"
timeout = "2s"
}
}
task "rabbitmq" {
driver = "docker"
template {
destination = "secrets/secret.env"
env = true
change_mode = "restart"
data = <<EOH
DOCKER_USER = {{ with nomadVar "nomad/jobs/rabbitmq-cluster" }}{{ .username }}{{ end }}
DOCKER_PASS = {{ with nomadVar "nomad/jobs/rabbitmq-cluster" }}{{ .password }}{{ end }}
EOH
}
env {
RABBITMQ_ERLANG_COOKIE = "secret_cookie"
RABBITMQ_DEFAULT_USER = "test"
RABBITMQ_DEFAULT_PASS = "test"
# RABBITMQ_USE_LONG_NAME = "true"
# CONSUL_HOST = "${attr.unique.network.ip-address}"
}
config {
image = "reg.code667.net/rabbitmq/rabbitmq-consul:2025110302"
hostname = "${attr.unique.hostname}"
ports = [ "ui", "epmd", "clustering", "amqp" ]
extra_hosts = ["nomad-client-prod1-0:100.102.0.23", "nomad-client-prod4-0:100.102.0.22", "nomad-client-triton-1:100.102.0.5"]
}
resources {
cpu = 500 # 500 MHz
memory = 512 # 512MB
}
}
}
}
Das "Gehampel" mit dem template ist notwendig, damit die Credentials für die Registry nicht im Klartext übergeben werden müssen. Das Issue, sie direkt aus Vault herauszuziehen (oder aus Nomad-Variablen) ist leider noch offen.
Der constraint weiter oben ist vermutlich auch nicht von Nöten, da wir eine statische Portzuordnung konfigurieren müssen. Mit dynamischer Portwahl (durch Nomad) habe ich keine Clusterung hinbekommen. Offenbar erwartet rabbitmq diese Ports auch bei seinen Clustermembern.