Auto scaling dockerized RabbitMQ on Elastic Beanstalk

The guide I wish I had

June 7, 2018
Contact Us
Weekly Shorts are topics we discuss in our weekly remote meeting related to recent work we have done with our customers
Auto scaling dockerized   RabbitMQ on Elastic Beanstalk

TL;DR — It works. Github repo.

As a DevOps consultant, I often see companies working with outdated architectures and long forgotten “best practices” that do not apply today. 

This is the story of one of those companies. They had a static cluster of RabbitMQ servers deployed up by an old Terraform code. 

Where is the state for this Terraform code? Probably in the hard drive of some old discarded mac of a developer who worked there 3 years ago, collecting dust somewhere in the office storage room. 

So basically, just 4 AWS EC2s alone in the wilderness (one of which was their designated master node) talking to each other with no alerts or monitoring.

This RabbitMQ set up worked pretty well for almost 3 years. the “bad craftsmanship” went unnoticed as it was “only” the staging environment. To be honest, the chances of something going wrong were slim.

After 3 years the luck ran out and the cluster began malfunctioning, something that I would describe as hiccups. ֿ

Before I continue to explain these hiccups, you need to know that this company has grown very fast in the last year or so. The business side was doing great, and lots and lots of traffic came through their servers. With it, came a lot of money, new developers, a drastic change to micro-services architecture and lots of new features. Pretty great for the company, not so good for the poor, unscalable and unmanaged RabbitMQ cluster. The hiccups started as an unexplained behavior of the entire system, as HTTP requests just dropped randomly when the RabbitMQ was overloaded. Sometimes it would happen at the beginning of the process, sometimes in the middle, and sometimes everything was fine.

It took them some time figuring out that the Rabbit cluster was the issue and it was one of the new developers who figured it out. The cluster was just too small for their current operation and needed 1 or 2 extra nodes. This was my starting point which was not that bad. After some debate on how we should approach the issue, I decided to start from scratch and re-deploy the cluster with auto-scaling, self healing, proper logs pipeline and monitoring. I chose Elastic Beanstalk for this venture, as about half of the company’s code is already on Beanstalk. we have ready made YAMLs for deployment and a pretty cool CI/CD pipeline which has been working great for a long time. Oh yeah, and Docker — when thinking of this cluster, I wouldn’t even consider not using Docker. it was apparent to me that I had to Dockerize Rabbit. I noticed there is an official docker image created by the RabbitMQ people. Easy.

Well; not that easy. 

The official RabbitMQ docker image is very good. It covers a lot of options with docker environment variables. I used this config file (find the most important part in Bold):

#gist:<link rel="stylesheet" href="https://assets-cdn.github.com/assets/gist-embed-87673c31a5b37b5e6556b63e1081ebbc.css"><div id=\"gist90026210\" class=\"gist\">\n <div class=\"gist-file\">\n <div class=\"gist-data\">\n <div class=\"js-gist-file-update-container js-task-list-container file-box\">\n <div id=\"file-rabbitmq1\" class=\"file\">\n \n\n <div itemprop=\"text\" class=\"blob-wrapper data type-text\">\n <table class=\"highlight tab-size js-file-line-container\" data-tab-size=\"8\">\n <tr>\n <td id=\"file-rabbitmq1-L1\" class=\"blob-num js-line-number\" data-line-number=\"1\"><\/td>\n <td id=\"file-rabbitmq1-LC1\" class=\"blob-code blob-code-inner js-file-line\">management.load_definitions = /etc/rabbitmq/definitions.json<\/td>\n <\/tr>\n <tr>\n <td id=\"file-rabbitmq1-L2\" class=\"blob-num js-line-number\" data-line-number=\"2\"><\/td>\n <td id=\"file-rabbitmq1-LC2\" class=\"blob-code blob-code-inner js-file-line\">loopback_users.guest = false<\/td>\n <\/tr>\n <tr>\n <td id=\"file-rabbitmq1-L3\" class=\"blob-num js-line-number\" data-line-number=\"3\"><\/td>\n <td id=\"file-rabbitmq1-LC3\" class=\"blob-code blob-code-inner js-file-line\">listeners.tcp.default = 5672<\/td>\n <\/tr>\n <tr>\n <td id=\"file-rabbitmq1-L4\" class=\"blob-num js-line-number\" data-line-number=\"4\"><\/td>\n <td id=\"file-rabbitmq1-LC4\" class=\"blob-code blob-code-inner js-file-line\">hipe_compile = false<\/td>\n <\/tr>\n <tr>\n <td id=\"file-rabbitmq1-L5\" class=\"blob-num js-line-number\" data-line-number=\"5\"><\/td>\n <td id=\"file-rabbitmq1-LC5\" class=\"blob-code blob-code-inner js-file-line\">management.listener.port = 15672<\/td>\n <\/tr>\n <tr>\n <td id=\"file-rabbitmq1-L6\" class=\"blob-num js-line-number\" data-line-number=\"6\"><\/td>\n <td id=\"file-rabbitmq1-LC6\" class=\"blob-code blob-code-inner js-file-line\">cluster_formation.peer_discovery_backend = rabbit_peer_discovery_aws<\/td>\n <\/tr>\n <tr>\n <td id=\"file-rabbitmq1-L7\" class=\"blob-num js-line-number\" data-line-number=\"7\"><\/td>\n <td id=\"file-rabbitmq1-LC7\" class=\"blob-code blob-code-inner js-file-line\">cluster_formation.aws.use_autoscaling_group = true<\/td>\n <\/tr>\n<\/table>\n\n\n <\/div>\n\n <\/div>\n<\/div>\n\n <\/div>\n <div class=\"gist-meta\">\n <a href=\"https://gist.github.com/seanroisentul/864cfaed503f881dc160014e96c7461a/raw/ba1a35f034da1f3ff65e9852e663aac3d493757f/RabbitMQ1\" style=\"float:right\">view raw<\/a>\n <a href=\"https://gist.github.com/seanroisentul/864cfaed503f881dc160014e96c7461a#file-rabbitmq1\">RabbitMQ1<\/a>\n hosted with ❤ by <a href=\"https://github.com\">GitHub<\/a>\n <\/div>\n <\/div>\n<\/div>\n

RabbitMQ, as of version 3.7.0, comes with lots of built in plugins, one of which is rabbit_peer_discovery_aws. It’s a backend discovery plugin for AWS. Basically, the plugin, if enabled and configured correctly, searches through EC2s in your account in a specified region for signs of other RabbitMQ nodes. You can configure it to look for certain tags, such as:

#gist:<link rel="stylesheet" href="https://assets-cdn.github.com/assets/gist-embed-87673c31a5b37b5e6556b63e1081ebbc.css"><div id=\"gist90028798\" class=\"gist\">\n <div class=\"gist-file\">\n <div class=\"gist-data\">\n <div class=\"js-gist-file-update-container js-task-list-container file-box\">\n <div id=\"file-rabbitmq2\" class=\"file\">\n \n\n <div itemprop=\"text\" class=\"blob-wrapper data type-text\">\n <table class=\"highlight tab-size js-file-line-container\" data-tab-size=\"8\">\n <tr>\n <td id=\"file-rabbitmq2-L1\" class=\"blob-num js-line-number\" data-line-number=\"1\"><\/td>\n <td id=\"file-rabbitmq2-LC1\" class=\"blob-code blob-code-inner js-file-line\">cluster_formation.aws.instance_tags.region = us-east-1<\/td>\n <\/tr>\n <tr>\n <td id=\"file-rabbitmq2-L2\" class=\"blob-num js-line-number\" data-line-number=\"2\"><\/td>\n <td id=\"file-rabbitmq2-LC2\" class=\"blob-code blob-code-inner js-file-line\">cluster_formation.aws.instance_tags.service = rabbitmq<\/td>\n <\/tr>\n <tr>\n <td id=\"file-rabbitmq2-L3\" class=\"blob-num js-line-number\" data-line-number=\"3\"><\/td>\n <td id=\"file-rabbitmq2-LC3\" class=\"blob-code blob-code-inner js-file-line\">cluster_formation.aws.instance_tags.environment = staging<\/td>\n <\/tr>\n<\/table>\n\n\n <\/div>\n\n <\/div>\n<\/div>\n\n <\/div>\n <div class=\"gist-meta\">\n <a href=\"https://gist.github.com/seanroisentul/743ace753644a0b4a024a6fe739b6096/raw/7dbae6f2571325d1cf0e402b73f880bb1eaa35e2/RabbitMQ2\" style=\"float:right\">view raw<\/a>\n <a href=\"https://gist.github.com/seanroisentul/743ace753644a0b4a024a6fe739b6096#file-rabbitmq2\">RabbitMQ2<\/a>\n hosted with ❤ by <a href=\"https://github.com\">GitHub<\/a>\n <\/div>\n <\/div>\n<\/div>\n

In my case, I used cluster_formation.aws.use_autoscaling_group = true which means the node looks for its own EC2 autoscaling group and looks for other nodes in that group. A much simpler solution in my opinion, which fits my needs. 

Here is a quick breakdown of what is supposed to happen — Amazon Elastic Beanstalk node starts up with a RabbitMQ docker image in it; the Rabbit looks in it’s own EC2 autoscaling group for other rabbits; if it’s the only rabbit  (first), it creates a RabbitMQ cluster and proclaims itself a leader (master). Another node spins up (from autoscaling rules). It looks for other rabbits and finds the master rabbit. It then registers with that master rabbit using the hostname from within the docker instance. Meaning that the slave rabbit tells the master rabbit how to find it and uses its hostname. 

At that point, there is a problem; as the docker inner hostname is just a random number given by the docker daemon, and means nothing in regarding discovery. This is a big problem, which took me too long to figure out. Possible solutions I thought of:

  1. Get a functional hostname (aws private ip) into the container as an environment variable at runtime (the only time I actually know the ip address) and then override the rabbits own hostname with the one I gave it from outside the container. This meant messing around with the Elastic Beanstalk startup scripts to make them give the host IP to the docker. Working with those Beanstalk scripts was so frustrating I went to option 2.
  2. Change the way RabbitMQ calculates the hostname and replace it with the much appreciated API from AWS EC2:

#gist:<link rel="stylesheet" href="https://assets-cdn.github.com/assets/gist-embed-87673c31a5b37b5e6556b63e1081ebbc.css"><div id=\"gist90028806\" class=\"gist\">\n <div class=\"gist-file\">\n <div class=\"gist-data\">\n <div class=\"js-gist-file-update-container js-task-list-container file-box\">\n <div id=\"file-rabbitmq3\" class=\"file\">\n \n\n <div itemprop=\"text\" class=\"blob-wrapper data type-text\">\n <table class=\"highlight tab-size js-file-line-container\" data-tab-size=\"8\">\n <tr>\n <td id=\"file-rabbitmq3-L1\" class=\"blob-num js-line-number\" data-line-number=\"1\"><\/td>\n <td id=\"file-rabbitmq3-LC1\" class=\"blob-code blob-code-inner js-file-line\">curl -s 169.254.169.254\\/latest\\/meta-data\\/local-hostname<\/td>\n <\/tr>\n<\/table>\n\n\n <\/div>\n\n <\/div>\n<\/div>\n\n <\/div>\n <div class=\"gist-meta\">\n <a href=\"https://gist.github.com/seanroisentul/036dfd70d20344b354b67b25105eb591/raw/794e8533d16174c8af2cbc82b2ab71efc04e6b7e/RabbitMQ3\" style=\"float:right\">view raw<\/a>\n <a href=\"https://gist.github.com/seanroisentul/036dfd70d20344b354b67b25105eb591#file-rabbitmq3\">RabbitMQ3<\/a>\n hosted with ❤ by <a href=\"https://github.com\">GitHub<\/a>\n <\/div>\n <\/div>\n<\/div>\n

This API call made from inside an EC2 instance gives back the local hostname of the instance. Very, very handy in my situation. All I need to do now is dive into the rabbit code and find where to stick this API call.

Luckily, the people who wrote RabbitMQ made my job much easier. In the configuration docs it states that the hostname environment variable is calculated with this simple command:

#gist:<link rel="stylesheet" href="https://assets-cdn.github.com/assets/gist-embed-87673c31a5b37b5e6556b63e1081ebbc.css"><div id=\"gist90028815\" class=\"gist\">\n <div class=\"gist-file\">\n <div class=\"gist-data\">\n <div class=\"js-gist-file-update-container js-task-list-container file-box\">\n <div id=\"file-rabbitmq4\" class=\"file\">\n \n\n <div itemprop=\"text\" class=\"blob-wrapper data type-text\">\n <table class=\"highlight tab-size js-file-line-container\" data-tab-size=\"8\">\n <tr>\n <td id=\"file-rabbitmq4-L1\" class=\"blob-num js-line-number\" data-line-number=\"1\"><\/td>\n <td id=\"file-rabbitmq4-LC1\" class=\"blob-code blob-code-inner js-file-line\">env hostname #for linux/unix machines<\/td>\n <\/tr>\n<\/table>\n\n\n <\/div>\n\n <\/div>\n<\/div>\n\n <\/div>\n <div class=\"gist-meta\">\n <a href=\"https://gist.github.com/seanroisentul/340618aad65ec24c880e279d6b4e533b/raw/72acbb461499c7fdba02e4b56a0888de7eda4ad1/RabbitMQ4\" style=\"float:right\">view raw<\/a>\n <a href=\"https://gist.github.com/seanroisentul/340618aad65ec24c880e279d6b4e533b#file-rabbitmq4\">RabbitMQ4<\/a>\n hosted with ❤ by <a href=\"https://github.com\">GitHub<\/a>\n <\/div>\n <\/div>\n<\/div>\n

Also, there is a configuration file named rabbitmq-env.conf where you can define environment variables. Amazing. My rabbitmq-env.conf looks like this:

#gist:<link rel="stylesheet" href="https://assets-cdn.github.com/assets/gist-embed-87673c31a5b37b5e6556b63e1081ebbc.css"><div id=\"gist90028824\" class=\"gist\">\n <div class=\"gist-file\">\n <div class=\"gist-data\">\n <div class=\"js-gist-file-update-container js-task-list-container file-box\">\n <div id=\"file-rabbitmq5\" class=\"file\">\n \n\n <div itemprop=\"text\" class=\"blob-wrapper data type-text\">\n <table class=\"highlight tab-size js-file-line-container\" data-tab-size=\"8\">\n <tr>\n <td id=\"file-rabbitmq5-L1\" class=\"blob-num js-line-number\" data-line-number=\"1\"><\/td>\n <td id=\"file-rabbitmq5-LC1\" class=\"blob-code blob-code-inner js-file-line\">HOSTNAME=`curl -s 169.254.169.254\\/latest\\/meta-data\\/local-hostname`<\/td>\n <\/tr>\n<\/table>\n\n\n <\/div>\n\n <\/div>\n<\/div>\n\n <\/div>\n <div class=\"gist-meta\">\n <a href=\"https://gist.github.com/seanroisentul/c2b70c53bf06d453b5715d2c1f04074f/raw/2cce82c02c70bbe25013465aec5689ff4002c0c3/RabbitMQ5\" style=\"float:right\">view raw<\/a>\n <a href=\"https://gist.github.com/seanroisentul/c2b70c53bf06d453b5715d2c1f04074f#file-rabbitmq5\">RabbitMQ5<\/a>\n hosted with ❤ by <a href=\"https://github.com\">GitHub<\/a>\n <\/div>\n <\/div>\n<\/div>\n

That was the final piece of the puzzle! With everything put together, I had a functioning docker image ready for deployment. 

Github repo for the whole project

At the end, the company could focus on developing things that matter, and not worry about the scale or health of their RabbitMQ cluster.

Auto scaling dockerized   RabbitMQ on Elastic Beanstalk
Yair Leshem
Operations Engineer
After a successful online marketing venture, Yair decided to focus on what he loves most - handling technical and analytical challenges as a software operations engineer. Yair has 7 years experience with handling all technical challenges, starting with infrastructure, DB, resolve unique challenges and code. He is great at solving problems and has an eye for designing, a sweet tooth and lots of love and compassion for others. When not on the job, he takes joy in snowboarding and organizing his theme camp in the Israeli Burning Man (Midburn). He is passionate about learning how complex systems work and figuring out how to make them work better.