Quick take — A reusable hashicorp/aws ~> 5.0 Terraform module for Amazon MQ that provisions ActiveMQ or RabbitMQ brokers with Active/Standby Multi-AZ, a private security group, a custom broker configuration, KMS encryption, and CloudWatch logs. New here? Jump to the Quickstart below to deploy it in minutes; read on for how it works and when to reach for it.
Quickstart (copy-paste)
Minimal, runnable configuration — drop this in a .tf file and fill in the "..." placeholders (each required input is commented):
provider "aws" {
region = "us-east-1"
}
module "mq" {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-mq?ref=v1.0.0"
name_prefix = "..." # Short prefix for all resource names (service/app name).
environment = "..." # One of `dev`, `staging`, `prod`; used in naming and tag…
vpc_id = "..." # VPC in which the broker security group is created.
subnet_ids = ["...", "..."] # Private subnet IDs (count depends on `deployment_mode`).
users = ["...", "..."] # Broker users; passwords >= 12 chars, no commas (pass vi…
}
Then terraform init && terraform apply. Every other input has a sensible default — see Inputs below to override behaviour.
What this module is
Amazon MQ is a managed message-broker service that runs the ActiveMQ and RabbitMQ engines for you, so you can keep an existing application that speaks AMQP 0-9-1, MQTT, STOMP, OpenWire, or the JMS wire protocols without re-platforming onto SQS/SNS. It is the right service precisely when you are migrating an on-premises broker or running off-the-shelf software that expects a standards-compliant broker endpoint rather than an AWS-proprietary queue API. The aws_mq_broker resource is the heart of it: it represents one broker (or a Multi-AZ pair / RabbitMQ cluster), its instance size, its engine version, its network placement, its users, and its encryption.
Wrapping it in a reusable module matters because a correct Amazon MQ deployment has several easy-to-get-wrong pieces that rarely vary between teams: it must live in private subnets with publicly_accessible = false behind a security group that opens only the engine’s ports (61617/8162 for ActiveMQ, 5671/443 for RabbitMQ) to the application tier; it should run ACTIVE_STANDBY_MULTI_AZ (ActiveMQ) or CLUSTER_MULTI_AZ (RabbitMQ) so a single AZ failure does not take messaging down; it needs encryption at rest with a KMS key, a managed broker configuration (aws_mq_configuration) that pins broker-level settings instead of relying on defaults, and general/audit log delivery to CloudWatch. This module folds all of that into one versioned unit, so every broker your organisation ships is private, HA, encrypted, and observable by default — instead of someone spinning up a single-instance, publicly-accessible broker that becomes both an outage and an audit finding.
When to use it
- You are migrating an existing ActiveMQ or RabbitMQ workload to AWS and need a drop-in broker endpoint that speaks the same protocols, not a rewrite onto SQS.
- You need JMS, AMQP 0-9-1, MQTT, or STOMP semantics (durable topics, message selectors, transactions, fan-out exchanges) that the native AWS queue services do not provide.
- You want Multi-AZ high availability — ActiveMQ Active/Standby or a 3-node RabbitMQ cluster — provisioned correctly from day one rather than retrofitted after an incident.
- You are deploying the same broker shape repeatedly (per environment or per service) and want one audited definition with environment-specific instance sizes and engine versions.
- You want encryption at rest, private networking, and CloudWatch audit logs enforced by default instead of left to reviewer vigilance.
- You do not need a fully serverless, AWS-native queue (use
aws_sqs_queue) or pub/sub fan-out to AWS targets (useaws_sns_topic) — this module is specifically for protocol-compatible broker workloads.
Module structure
terraform-module-aws-mq/
├── versions.tf # provider + Terraform version pins
├── main.tf # security group, configuration, broker
├── variables.tf # var-driven inputs with validations
└── outputs.tf # id, arn, endpoints, console URL, SG id
versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
main.tf
locals {
name = "${var.name_prefix}-${var.environment}"
is_activemq = var.engine_type == "ActiveMQ"
is_rabbitmq = var.engine_type == "RabbitMQ"
# ActiveMQ exposes OpenWire (SSL) + the web console; RabbitMQ exposes
# AMQPS + the management console. We only ever open the SSL/TLS ports.
broker_ports = local.is_activemq ? [61617, 8162] : [5671, 443]
# A custom configuration is only attached when configuration_data is set.
# RabbitMQ in CLUSTER_MULTI_AZ mode does not support broker configurations,
# so we guard against attaching one there.
manage_configuration = (
var.configuration_data != null &&
!(local.is_rabbitmq && var.deployment_mode == "CLUSTER_MULTI_AZ")
)
tags = merge(
{
Name = local.name
Environment = var.environment
Engine = var.engine_type
ManagedBy = "terraform"
Module = "terraform-module-aws-mq"
},
var.tags,
)
}
# Dedicated security group; ingress is opened only to the CIDRs/SGs you pass in,
# and only on the engine's TLS ports.
resource "aws_security_group" "this" {
name = "${local.name}-mq-sg"
description = "Access to ${local.name} Amazon MQ broker (${var.engine_type})"
vpc_id = var.vpc_id
tags = local.tags
}
resource "aws_security_group_rule" "ingress_cidr" {
for_each = length(var.allowed_cidr_blocks) > 0 ? toset([for p in local.broker_ports : tostring(p)]) : toset([])
type = "ingress"
description = "Broker TLS port ${each.value} from allowed CIDRs"
from_port = tonumber(each.value)
to_port = tonumber(each.value)
protocol = "tcp"
cidr_blocks = var.allowed_cidr_blocks
security_group_id = aws_security_group.this.id
}
resource "aws_security_group_rule" "ingress_sg" {
for_each = {
for pair in setproduct(var.allowed_security_group_ids, local.broker_ports) :
"${pair[0]}-${pair[1]}" => { sg = pair[0], port = pair[1] }
}
type = "ingress"
description = "Broker TLS port ${each.value.port} from app security group"
from_port = each.value.port
to_port = each.value.port
protocol = "tcp"
source_security_group_id = each.value.sg
security_group_id = aws_security_group.this.id
}
resource "aws_security_group_rule" "egress_all" {
type = "egress"
description = "Allow all outbound"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.this.id
}
# Custom broker configuration (XML for ActiveMQ, Cuttlefish for RabbitMQ).
# Pins broker-level behaviour instead of relying on engine defaults.
resource "aws_mq_configuration" "this" {
count = local.manage_configuration ? 1 : 0
name = "${local.name}-config"
description = "Managed configuration for ${local.name}"
engine_type = var.engine_type
engine_version = var.engine_version
data = var.configuration_data
# Apply a new configuration revision in the next maintenance window
# rather than forcing an immediate reboot of the broker.
lifecycle {
create_before_destroy = true
}
tags = local.tags
}
resource "aws_mq_broker" "this" {
broker_name = local.name
engine_type = var.engine_type
engine_version = var.engine_version
host_instance_type = var.host_instance_type
deployment_mode = var.deployment_mode
storage_type = var.storage_type
# Placement: private subnets, never public.
subnet_ids = var.subnet_ids
security_groups = concat([aws_security_group.this.id], var.extra_security_group_ids)
publicly_accessible = var.publicly_accessible
# Patching / change control.
auto_minor_version_upgrade = var.auto_minor_version_upgrade
apply_immediately = var.apply_immediately
authentication_strategy = var.authentication_strategy
# Attach the managed configuration when one is created.
dynamic "configuration" {
for_each = local.manage_configuration ? [1] : []
content {
id = aws_mq_configuration.this[0].id
revision = aws_mq_configuration.this[0].latest_revision
}
}
# Broker users. ActiveMQ supports multiple users (with optional console
# access and group membership); RabbitMQ supports exactly one admin user.
dynamic "user" {
for_each = { for u in var.users : u.username => u }
content {
username = user.value.username
password = user.value.password
console_access = local.is_activemq ? user.value.console_access : null
groups = local.is_activemq ? user.value.groups : null
replication_user = user.value.replication_user
}
}
# Encryption at rest. A customer-managed KMS key is used when supplied;
# otherwise Amazon MQ uses an AWS-owned key.
encryption_options {
kms_key_id = var.kms_key_id
use_aws_owned_key = var.kms_key_id == null
}
# Stream general (and, for ActiveMQ, audit) logs to CloudWatch Logs.
logs {
general = var.logs_general
audit = local.is_activemq ? var.logs_audit : null
}
maintenance_window_start_time {
day_of_week = var.maintenance_day_of_week
time_of_day = var.maintenance_time_of_day
time_zone = var.maintenance_time_zone
}
tags = local.tags
lifecycle {
# Passwords are rotated out-of-band via Secrets Manager; ignore drift so
# a rotation does not force the broker to be replaced on the next plan.
ignore_changes = [user]
}
}
variables.tf
variable "name_prefix" {
description = "Short prefix for all resource names (e.g. the service or app name)."
type = string
validation {
condition = can(regex("^[a-z][a-z0-9-]{1,40}$", var.name_prefix))
error_message = "name_prefix must be lowercase alphanumeric/hyphens, start with a letter, 2-41 chars."
}
}
variable "environment" {
description = "Deployment environment, used in naming and tags."
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment must be one of: dev, staging, prod."
}
}
variable "engine_type" {
description = "Broker engine: 'ActiveMQ' or 'RabbitMQ'."
type = string
default = "ActiveMQ"
validation {
condition = contains(["ActiveMQ", "RabbitMQ"], var.engine_type)
error_message = "engine_type must be 'ActiveMQ' or 'RabbitMQ'."
}
}
variable "engine_version" {
description = "Engine version (e.g. '5.18' for ActiveMQ, '3.13' for RabbitMQ). Must match a version AWS supports."
type = string
default = "5.18"
}
variable "host_instance_type" {
description = "Broker instance class (e.g. mq.t3.micro, mq.m5.large). t3.micro is dev-only."
type = string
default = "mq.m5.large"
validation {
condition = can(regex("^mq\\.", var.host_instance_type))
error_message = "host_instance_type must be an Amazon MQ instance class starting with 'mq.'."
}
}
variable "deployment_mode" {
description = "Topology: SINGLE_INSTANCE, ACTIVE_STANDBY_MULTI_AZ (ActiveMQ), or CLUSTER_MULTI_AZ (RabbitMQ)."
type = string
default = "ACTIVE_STANDBY_MULTI_AZ"
validation {
condition = contains(["SINGLE_INSTANCE", "ACTIVE_STANDBY_MULTI_AZ", "CLUSTER_MULTI_AZ"], var.deployment_mode)
error_message = "deployment_mode must be SINGLE_INSTANCE, ACTIVE_STANDBY_MULTI_AZ, or CLUSTER_MULTI_AZ."
}
}
variable "storage_type" {
description = "Broker storage backend: 'efs' (Multi-AZ durable, ActiveMQ) or 'ebs' (single-AZ, lower latency)."
type = string
default = "efs"
validation {
condition = contains(["efs", "ebs"], var.storage_type)
error_message = "storage_type must be 'efs' or 'ebs'."
}
}
variable "vpc_id" {
description = "VPC in which the broker security group is created."
type = string
}
variable "subnet_ids" {
description = "Private subnet IDs for the broker. Use 1 subnet for SINGLE_INSTANCE, 2 for ACTIVE_STANDBY_MULTI_AZ, and exactly 1 (RabbitMQ single) or per-AZ subnets for CLUSTER_MULTI_AZ."
type = list(string)
validation {
condition = length(var.subnet_ids) >= 1
error_message = "Provide at least one private subnet for the broker."
}
}
variable "publicly_accessible" {
description = "Whether the broker has a public endpoint. Keep false; brokers should be reachable only from within the VPC."
type = bool
default = false
}
variable "authentication_strategy" {
description = "Authentication backend: 'simple' (broker-managed users) or 'ldap'."
type = string
default = "simple"
validation {
condition = contains(["simple", "ldap"], var.authentication_strategy)
error_message = "authentication_strategy must be 'simple' or 'ldap'."
}
}
variable "users" {
description = "Broker users. ActiveMQ supports multiple users (console_access/groups honoured); RabbitMQ requires exactly one. Passwords must be 12-250 chars with no commas. Pass via a secret, never hardcode."
type = list(object({
username = string
password = string
console_access = optional(bool, false)
groups = optional(list(string), [])
replication_user = optional(bool, false)
}))
sensitive = true
validation {
condition = length(var.users) >= 1
error_message = "At least one broker user is required."
}
validation {
condition = alltrue([for u in var.users : length(u.password) >= 12 && !can(regex(",", u.password))])
error_message = "Each user password must be at least 12 characters and contain no commas."
}
}
variable "configuration_data" {
description = "Broker configuration body: ActiveMQ XML (<broker>...) or RabbitMQ Cuttlefish. Null to use engine defaults. Not supported for RabbitMQ CLUSTER_MULTI_AZ."
type = string
default = null
}
variable "kms_key_id" {
description = "Optional customer-managed KMS key ARN for encryption at rest. Null = AWS-owned key."
type = string
default = null
}
variable "allowed_cidr_blocks" {
description = "CIDR blocks permitted to reach the broker's TLS ports."
type = list(string)
default = []
}
variable "allowed_security_group_ids" {
description = "Source security group IDs (app tier) permitted to reach the broker's TLS ports."
type = list(string)
default = []
}
variable "extra_security_group_ids" {
description = "Additional pre-existing SG IDs to attach to the broker."
type = list(string)
default = []
}
variable "logs_general" {
description = "Stream general broker logs to CloudWatch Logs."
type = bool
default = true
}
variable "logs_audit" {
description = "Stream audit logs to CloudWatch Logs. ActiveMQ only; ignored for RabbitMQ."
type = bool
default = true
}
variable "auto_minor_version_upgrade" {
description = "Allow Amazon MQ to apply minor engine upgrades during the maintenance window."
type = bool
default = true
}
variable "apply_immediately" {
description = "Apply modifications (and reboots) immediately instead of during the maintenance window."
type = bool
default = false
}
variable "maintenance_day_of_week" {
description = "Day of the weekly maintenance window (e.g. MONDAY). Required for SINGLE_INSTANCE/ACTIVE_STANDBY ActiveMQ."
type = string
default = "SUNDAY"
validation {
condition = contains(["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"], var.maintenance_day_of_week)
error_message = "maintenance_day_of_week must be an upper-case day name (e.g. SUNDAY)."
}
}
variable "maintenance_time_of_day" {
description = "Start time of the maintenance window, 24h HH:MM (e.g. '03:00')."
type = string
default = "03:00"
validation {
condition = can(regex("^([01][0-9]|2[0-3]):[0-5][0-9]$", var.maintenance_time_of_day))
error_message = "maintenance_time_of_day must be HH:MM in 24-hour format."
}
}
variable "maintenance_time_zone" {
description = "Time zone for the maintenance window (e.g. 'UTC', 'Asia/Kolkata')."
type = string
default = "UTC"
}
variable "tags" {
description = "Additional tags merged onto every resource."
type = map(string)
default = {}
}
outputs.tf
output "broker_id" {
description = "The unique ID of the Amazon MQ broker."
value = aws_mq_broker.this.id
}
output "broker_name" {
description = "The name of the broker."
value = aws_mq_broker.this.broker_name
}
output "arn" {
description = "ARN of the broker."
value = aws_mq_broker.this.arn
}
output "instances" {
description = "Per-instance details (endpoints, console_url, ip_address) for every broker node."
value = aws_mq_broker.this.instances
}
output "primary_endpoints" {
description = "Wire-protocol endpoints (e.g. ssl://...:61617 or amqps://...:5671) of the active broker instance."
value = aws_mq_broker.this.instances[0].endpoints
}
output "console_url" {
description = "URL of the active broker's web console (ActiveMQ) or management UI (RabbitMQ)."
value = aws_mq_broker.this.instances[0].console_url
}
output "security_group_id" {
description = "ID of the security group created for the broker."
value = aws_security_group.this.id
}
output "configuration_id" {
description = "ID of the managed broker configuration, or null when none is created."
value = try(aws_mq_configuration.this[0].id, null)
}
output "configuration_latest_revision" {
description = "Latest revision number of the managed configuration, or null when none is created."
value = try(aws_mq_configuration.this[0].latest_revision, null)
}
How to use it
# Generate and store the broker admin password in Secrets Manager; never hardcode it.
resource "random_password" "mq_admin" {
length = 32
special = false # Amazon MQ passwords disallow commas and some symbols; alphanumeric is safe.
}
resource "aws_secretsmanager_secret" "mq_admin" {
name = "orders/mq-admin-password"
}
resource "aws_secretsmanager_secret_version" "mq_admin" {
secret_id = aws_secretsmanager_secret.mq_admin.id
secret_string = random_password.mq_admin.result
}
module "amazon_mq" {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-mq?ref=v1.0.0"
name_prefix = "orders-broker"
environment = "prod"
engine_type = "ActiveMQ"
engine_version = "5.18"
host_instance_type = "mq.m5.large"
# Active/Standby across two AZs on durable EFS storage.
deployment_mode = "ACTIVE_STANDBY_MULTI_AZ"
storage_type = "efs"
vpc_id = module.network.vpc_id
subnet_ids = slice(module.network.private_subnet_ids, 0, 2) # exactly 2 AZs for Active/Standby
# Lock down access to the app tier only.
allowed_security_group_ids = [module.orders_service.app_security_group_id]
# Single admin user, sourced from Secrets Manager, with web-console access.
users = [{
username = "orders_admin"
password = aws_secretsmanager_secret_version.mq_admin.secret_string
console_access = true
groups = ["admins"]
}]
# Pin broker behaviour: enable JMX-free audit, tune the destination policy.
configuration_data = file("${path.module}/activemq.xml")
# Encrypt at rest with a customer-managed key, ship logs, patch on Sundays.
kms_key_id = aws_kms_key.orders.arn
logs_general = true
logs_audit = true
maintenance_day_of_week = "SUNDAY"
maintenance_time_of_day = "03:00"
maintenance_time_zone = "Asia/Kolkata"
tags = {
Team = "fulfilment"
CostCentre = "ECOM-204"
}
}
# Downstream: hand the OpenWire SSL endpoint to the ECS task definition so the
# order worker can connect to the broker.
resource "aws_ssm_parameter" "mq_endpoint" {
name = "/orders/mq/openwire-ssl-endpoint"
type = "String"
value = module.amazon_mq.primary_endpoints[0]
}
With Terragrunt
Terragrunt keeps this module DRY across environments — define the backend and provider once in a root config, then a thin terragrunt.hcl per environment supplies only the inputs that differ.
1. Root config — live/terragrunt.hcl (inherited by every module):
remote_state {
backend = "s3"
generate = { path = "backend.tf", if_exists = "overwrite" }
config = {
# ...s3 state bucket/container + key per path...
}
}
2. Module config — live/prod/mq/terragrunt.hcl:
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-mq?ref=v1.0.0"
}
inputs = {
name_prefix = "..."
environment = "..."
vpc_id = "..."
subnet_ids = ["...", "..."]
users = ["...", "..."]
}
3. Deploy one environment, or roll out all modules together:
cd live/prod/mq && terragrunt apply # this module
terragrunt run-all apply # every module under live/prod
Why Terragrunt here: the backend and provider live in one place instead of being copy-pasted into every module; inputs is overridden per environment (dev / stage / prod) without forking the module; and run-all orchestrates dependencies across modules. Reach for it once you have more than one environment or more than a handful of modules — for a single stack, the plain Quickstart above is enough.
Inputs
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
name_prefix |
string |
— | Yes | Short prefix for all resource names (service/app name). |
environment |
string |
— | Yes | One of dev, staging, prod; used in naming and tags. |
engine_type |
string |
"ActiveMQ" |
No | Broker engine: ActiveMQ or RabbitMQ. |
engine_version |
string |
"5.18" |
No | Engine version (e.g. 5.18, 3.13); must be AWS-supported. |
host_instance_type |
string |
"mq.m5.large" |
No | Broker instance class (e.g. mq.t3.micro, mq.m5.large). |
deployment_mode |
string |
"ACTIVE_STANDBY_MULTI_AZ" |
No | SINGLE_INSTANCE, ACTIVE_STANDBY_MULTI_AZ, or CLUSTER_MULTI_AZ. |
storage_type |
string |
"efs" |
No | efs (Multi-AZ durable) or ebs (single-AZ, lower latency). |
vpc_id |
string |
— | Yes | VPC in which the broker security group is created. |
subnet_ids |
list(string) |
— | Yes | Private subnet IDs (count depends on deployment_mode). |
publicly_accessible |
bool |
false |
No | Whether the broker has a public endpoint; keep false. |
authentication_strategy |
string |
"simple" |
No | simple (broker users) or ldap. |
users |
list(object) |
— | Yes | Broker users; passwords >= 12 chars, no commas (pass via a secret). |
configuration_data |
string |
null |
No | ActiveMQ XML / RabbitMQ Cuttlefish config body; null = defaults. |
kms_key_id |
string |
null |
No | Customer-managed KMS key ARN for at-rest encryption; null = AWS-owned. |
allowed_cidr_blocks |
list(string) |
[] |
No | CIDRs permitted to reach the broker’s TLS ports. |
allowed_security_group_ids |
list(string) |
[] |
No | Source SG IDs (app tier) permitted to reach the broker. |
extra_security_group_ids |
list(string) |
[] |
No | Additional existing SGs to attach to the broker. |
logs_general |
bool |
true |
No | Stream general broker logs to CloudWatch. |
logs_audit |
bool |
true |
No | Stream audit logs (ActiveMQ only; ignored for RabbitMQ). |
auto_minor_version_upgrade |
bool |
true |
No | Allow minor engine upgrades during maintenance. |
apply_immediately |
bool |
false |
No | Apply changes/reboots immediately vs. during maintenance. |
maintenance_day_of_week |
string |
"SUNDAY" |
No | Maintenance window day (upper-case day name). |
maintenance_time_of_day |
string |
"03:00" |
No | Maintenance window start time (HH:MM, 24h). |
maintenance_time_zone |
string |
"UTC" |
No | Maintenance window time zone (e.g. Asia/Kolkata). |
tags |
map(string) |
{} |
No | Additional tags merged onto every resource. |
Outputs
| Name | Description |
|---|---|
broker_id |
The unique ID of the Amazon MQ broker. |
broker_name |
The name of the broker. |
arn |
ARN of the broker. |
instances |
Per-instance details (endpoints, console_url, ip_address) for every node. |
primary_endpoints |
Wire-protocol endpoints of the active broker instance. |
console_url |
URL of the active broker’s web/management console. |
security_group_id |
ID of the security group created for the broker. |
configuration_id |
ID of the managed configuration, or null when none is created. |
configuration_latest_revision |
Latest revision of the managed configuration, or null. |
Enterprise scenario
A logistics company is lifting a legacy order-routing application off an on-premises ActiveMQ cluster and into AWS, and the application is hard-wired to JMS durable topics and OpenWire — rewriting it onto SQS is off the table for this release. The platform team consumes this module pinned at ref=v1.0.0 to stand up an ACTIVE_STANDBY_MULTI_AZ ActiveMQ 5.18 broker on mq.m5.large across two private subnets with EFS storage, a customer-managed KMS key, and a custom activemq.xml that pins the dead-letter strategy and per-destination memory limits used on-prem. Because the module forces publicly_accessible = false, scopes the security group to the order-service app SG on ports 61617/8162 only, and ships both general and audit logs to CloudWatch, the migrated broker clears the company’s security review on the first pass and survives an AZ failure with an automatic standby promotion — while every other team that later adopts it inherits the same HA, encrypted, audited baseline.
Best practices
- Keep brokers private and tightly scoped. Always run
publicly_accessible = falsein private subnets and let the module open only the engine’s TLS ports (61617/8162 for ActiveMQ, 5671/443 for RabbitMQ) to the application security group — never expose a broker to0.0.0.0/0, since a public broker endpoint with broker-managed users is a credential-stuffing target. - Run Multi-AZ for anything that matters. Use
ACTIVE_STANDBY_MULTI_AZfor ActiveMQ andCLUSTER_MULTI_AZ(3 nodes) for RabbitMQ so a single-AZ failure triggers an automatic standby promotion; reserveSINGLE_INSTANCEfor dev only, and remember that ActiveMQ HA requiresefsdurable storage, notebs. - Source every user password from Secrets Manager and never commit it. Pass passwords through the
usersvariable from arandom_password+aws_secretsmanager_secret_version; the module marksusersas sensitive andignore_changes = [user]so out-of-band rotations do not force a broker replacement on the next plan. - Pin behaviour with a managed configuration and version your engine. Supply
configuration_data(ActiveMQ XML / RabbitMQ Cuttlefish) throughaws_mq_configurationso dead-letter policies, memory limits, and TTLs are codified rather than left to defaults, and bumpengine_versiondeliberately withauto_minor_version_upgradehandling patch releases in your maintenance window. - Right-size and watch cost. Amazon MQ bills per broker-instance-hour plus storage, and Active/Standby and clusters multiply that by the node count — start on the smallest instance that meets throughput, scale up rather than out where the engine allows, and avoid leaving idle
mq.m5-class brokers running in non-prod. - Make brokers observable and name them consistently. Enable
logs_generaland (for ActiveMQ)logs_auditto CloudWatch, alarm on broker CPU, heap/memory usage, andConsumerCount/queue depth so backpressure pages you before producers stall, and drive naming throughname_prefix+environmentso brokers, configurations, and security groups stay predictable across environments.