Docs / Amazon MQ

Amazon MQ

LocalEmu Amazon MQ implements all 24 operations and pairs them with a real RabbitMQ broker running in Docker on the host. The control plane (broker records, users, configurations, configuration revisions, tags, engine catalog) is served by the moto-ext metadata backend. The behavior plane (the AMQP/MQTT/STOMP/management protocols that your application actually speaks) is the official rabbitmq:3.13-management image, started by LocalEmu's broker manager and exposed on ephemeral host ports. The two planes are kept in sync: DescribeBroker returns endpoints rewritten to the real container ports, and DeleteBroker stops and removes the container.

Operation-level coverage: see the MQ coverage matrix.

Quick start

Terminal
# Real RabbitMQ container needs MQ_DOCKER_BACKEND=1 (off by default).
$ MQ_DOCKER_BACKEND=1 localemu start

$ awsemu mq create-broker --broker-name orders \
    --engine-type RABBITMQ --engine-version 3.13 \
    --host-instance-type mq.t3.micro \
    --deployment-mode SINGLE_INSTANCE \
    --publicly-accessible \
    --users '[{"Username":"admin","Password":"supersecret123","Groups":["admin"]}]' \
    --query '{Id:BrokerId,Arn:BrokerArn}'
{
  "Id":  "b-1234abcd-5678-...",
  "Arn": "arn:aws:mq:us-east-1:000000000000:broker:orders:b-1234abcd-..."
}

$ awsemu mq describe-broker --broker-id b-1234abcd-... \
    --query '{State:BrokerState,Eps:BrokerInstances[0].Endpoints,Console:BrokerInstances[0].ConsoleURL}'
{
  "State":   "RUNNING",
  "Eps":     ["amqp://127.0.0.1:54321","amqps://127.0.0.1:54322","mqtt+ssl://127.0.0.1:54323","stomp+ssl://127.0.0.1:54324"],
  "Console": "https://127.0.0.1:54325/"
}

The Docker backend is opt-in via MQ_DOCKER_BACKEND=1. Without it, CreateBroker records the broker in moto only and you will not have a live AMQP endpoint to connect to. With it on, LocalEmu pulls rabbitmq:3.13-management if missing, allocates host ports for each protocol, boots the container, and waits for the AMQP port to accept TCP connections before returning.

Real AMQP round-trip with pika

Terminal
$ python3 - <<'PY'
import pika

params = pika.ConnectionParameters(
    host="127.0.0.1", port=54321,
    credentials=pika.PlainCredentials("admin", "supersecret123"),
)
conn = pika.BlockingConnection(params)
ch = conn.channel()
ch.queue_declare(queue="hello")
ch.basic_publish(exchange="", routing_key="hello", body=b"world")

method, props, body = ch.basic_get(queue="hello", auto_ack=True)
print("Got:", body.decode())
conn.close()
PY
Got: world

Architecture

Code lives at services/mq/. The provider class MqProvider at provider.py:54 is registered as mq:default in plux.ini:86 via services/providers.py:798. Five operations are LocalEmu-custom:

Everything else (users, configurations and revisions, tags, engine catalog, UpdateBroker, Promote) rides on moto unchanged through MotoFallbackDispatcher.

BrokerManager + RabbitMQ driver

The Docker layer lives at services/mq/docker/. BrokerManager at broker_manager.py:89 is a thread-safe singleton holding _brokers: dict[broker_id, BrokerInstance]. The RabbitMQ driver at rabbitmq.py:

Features supported

FeatureNotes
Broker lifecycleCreateBroker (Docker pull + boot + readiness), DeleteBroker (stop + remove), RebootBroker (restart + re-readiness), DescribeBroker, ListBrokers.
Protocols exposedAMQP 0-9-1 + AMQP 1.0, AMQPS (TLS), MQTT, STOMP, and the RabbitMQ management HTTP API.
UsersCreateUser, UpdateUser, DeleteUser, DescribeUser, ListUsers against the moto control plane. First-user credentials are seeded into the running broker; later user CRUD does not propagate (see Limitations).
ConfigurationsCreateConfiguration, UpdateConfiguration, DescribeConfiguration, DescribeConfigurationRevision, ListConfigurations, ListConfigurationRevisions. Stored in moto; revision history works; the broker container is not hot-reloaded (see Limitations).
TagsCreateTags, DeleteTags, ListTags on brokers and configurations.
Engine catalogDescribeBrokerEngineTypes, DescribeBrokerInstanceOptions return the AWS-style catalog from moto.
PromotionPromote succeeds at the moto control plane (used for cross-region replication API parity). No replica container is actually promoted.
UpdatesUpdateBroker updates the moto record; the running container is not modified (see Limitations).
Persistencemoto state restored from disk on PERSISTENCE=1 restart; Docker containers persist via Docker itself; BrokerManager rehydrates from container labels (broker_manager.py:315-341).

Endpoint shape

DescribeBroker rewrites every endpoint to point at the host loopback on the allocated port:

BrokerInstances[0].Endpoints     = [
  "amqp://127.0.0.1:<amqp>",
  "amqps://127.0.0.1:<amqps>",
  "stomp+ssl://127.0.0.1:<stomp>",
  "mqtt+ssl://127.0.0.1:<mqtt>",
]
BrokerInstances[0].IpAddress     = "127.0.0.1"
BrokerInstances[0].ConsoleURL    = "https://127.0.0.1:<mgmt>/"

Reachability:

Terminal
# Inside a Lambda (or any other LocalEmu Docker container), 127.0.0.1 is
# the container, not the host. Use host.docker.internal instead.
$ awsemu lambda create-function --function-name publish-order \
    --runtime python3.12 --handler index.handler \
    --role arn:aws:iam::000000000000:role/lambda-role \
    --environment 'Variables={MQ_HOST=host.docker.internal,MQ_PORT=54321}' \
    --zip-file fileb://publish.zip

$ awsemu lambda invoke --function-name publish-order \
    --payload '{"order_id":42}' /tmp/out.json
{"StatusCode": 200}

$ cat /tmp/out.json
{"published": true, "queue": "orders"}

RabbitMQ management UI

The -management image includes the RabbitMQ HTTP API and web UI on port 15672 (container-side), exposed by LocalEmu on an ephemeral host port. DescribeBroker reports it as ConsoleURL. Authenticate with the first-user credentials from CreateBroker.

Terminal
$ CONSOLE=$(awsemu mq describe-broker --broker-id b-1234abcd-... \
    --query 'BrokerInstances[0].ConsoleURL' --output text)
$ echo $CONSOLE
https://127.0.0.1:54325/

# RabbitMQ management UI runs on the management port baked into the image.
# Log in with the first-user credentials from CreateBroker.
$ curl -sk -u admin:supersecret123 $CONSOLE/api/queues \
    | python3 -c 'import sys,json;[print(q["name"]) for q in json.load(sys.stdin)]'
hello

Persistence

With PERSISTENCE=1, moto's mq_backends roundtrips through the standard state save/load path (provider.py:57-60). Docker containers are preserved by Docker itself across LocalEmu process restarts. On the next API call, BrokerManager rehydrates each BrokerInstance from the container labels stamped at create-time. The same broker-id continues to resolve to the same container, and queue+message state inside the broker is whatever RabbitMQ itself persisted.

Terminal
# With PERSISTENCE=1, the broker metadata reloads from disk on restart.
# The Docker container itself is preserved by Docker, and BrokerManager
# rehydrates the BrokerInstance lazily on the next API call.
$ MQ_DOCKER_BACKEND=1 PERSISTENCE=1 localemu start
$ awsemu mq create-broker --broker-name orders \
    --engine-type RABBITMQ --engine-version 3.13 \
    --host-instance-type mq.t3.micro --deployment-mode SINGLE_INSTANCE \
    --publicly-accessible \
    --users '[{"Username":"admin","Password":"hunter2hunter2"}]' >/dev/null

$ localemu restart

$ awsemu mq list-brokers --query 'BrokerSummaries[].{Name:BrokerName,State:BrokerState}'
[{"Name": "orders", "State": "RUNNING"}]

Configuration

VariableDefaultPurpose
MQ_DOCKER_BACKEND(unset)Set to 1 or true to boot a real RabbitMQ container per CreateBroker. When unset, brokers are recorded in moto only and no live AMQP endpoint exists. Read at provider.py:51.

Integration points

ServiceHow it touches MQ
CloudFormationAWS::AmazonMQ::Broker resource provider drives CreateBroker through this provider.
Secrets ManagerCommon pattern: store broker admin password as a secret, fetch via awsemu secretsmanager get-secret-value at deploy time.
CloudTrailAll MQ control-plane calls (CreateBroker, DeleteBroker, ...) appear in the CloudTrail event store and are recorded by the dashboard.

Known limitations