Running distributed Erlang & Elixir applications on Docker

2018-03-28 by Oleg Tarasenko

The docker platform has been around for a couple of years already, so let’s briefly review which benefits it provides, and why you should consider using it:

An easy means to boot your production services on the desktop

I still remember how complex it was to bring up MySql on OS X in the past. But it would be even worse if you consider a scenario where your production environment is running some specific version of MySQL, especially built on non OS X.

How would you bring that up? Luckily this problem just goes away when you’re using docker, as you can bring up any version of program as easily as executing the command docker run mysql:5.3 .

The ability to work against a realistic testing environment, without having to implement service mocks

It was always an issue. If you wanted to test your software properly, you had to bring up (or mock) a lot of related services. And it always created issues, as a mock never really matches the characteristics of a complete running service.

And with docker you can just plug any software that your application relies upon, and have it right there as a part of your testing environment.

Creating realistic cloud infrastructure, using containers

Finally, docker containers can be spun up by tools like Apache Mesos forming full featured clouds, deployable in the effortless and clear manner, and incredibly stable!

Interested? Lets see which benefits the technology can give you when it comes to the Erlang and Elixir worlds.

Testing distributed erlang applications with docker

Let’s consider a case when we need to create and test a distributed erlang application, running on the same machine, and let’s say we want to have it running, but it relies upon some extra services.

This kind of setup can be described in the following docker-compose file:

version: "3" services: c1: image: erlang:latest # We're using the following command to bring up erlang shell # for the example purposes, but in the other case the command # will describe a running container command: erl -noshell -name app@host1.com -setcookie cookie container_name: host1.com networks: - net1 c2: image: erlang:17.5 command: erl -noshell -name app@host2.com -setcookie cookie container_name: host2.com networks: - net1 ejabberd: image: rroemhild/ejabberd ports: - 5222:5222 - 5269:5269 - 5280:5280 container_name: host3.com environment: - ERLANG_NODE=ejabberd@host3.com - XMPP_DOMAIN=test.io - ERLANG_COOKIE=cookie - EJABBERD_ADMINS=admin@test.io - EJABBERD_USERS=admin@test.io:admin - EJABBERD_SKIP_MODULES_UPDATE=true networks: - net1 # We're using custom network setup, as it ships with a DNS # system which allows containers to communicate by hostnames. networks: net1: driver: bridge

Now let’s boot this setup and take a look at how we can facilitate communication between these containers:

Bring up every container described in the file: docker-compose up -d Connect to one of the hosts: docker exec -it host2.com erl -name test@host2.com -setcookie cookie -remsh app@host2.com Verify that host connectivity is functioning correctly:

(app@host2.com)1> net_adm:ping('app@host2.com'). pong (app@host2.com)2> net_adm:ping('app@host1.com'). pong (app@host2.com)4> net_adm:ping('ejabberd@host3.com'). pong (app@host2.com)5> nodes(). ['test@host2.com','app@host1.com','ejabberd@host3.com']

In this simple example we’ve brought up several containers and connected them using the standard erlang distribution. This is possible because all of the containers are running in the same docker network, and can access one anothers epmd daemons…

But how can we connect docker containers running on different hosts?

Connecting beams running on separate machines

Finally if you want to go to production, and to create a real cloud, running multiple hosting machines, the setup above will not work, as containers will not know how to connect to each other.

Indeed, hosts will not be aware about each others docker networks (in reality some work to interconnect docker containers using docker swarm or amazon ec2 containers service is being done, but we could not bring it up reliably running), and we would have to organise a more complex connection mechanism.

Actually the idea of creating alternative erlang distribution protocol came out of this amazing article, Erlang (and Elixir) distribution without epmd, we just decided to make few steps forward to create a pluggable application, which would just “Find them all, chain them all and… make sure all of them are always connected.”

The epmdless approach

As it’s described by Magnus Henoch, in his blog post, the epmd is not really needed in the dockerised setup, as docker environment is already forcing to expose particular ports for external access. And because of that there could be only one port used by erlang distribution (as other ports just will not be exposed).

On the other hand it’s quite easy to store mapping between hosts and ports inside the Erlang application. And now we already have the application for that: EpmdLess (credits go to Dmitry Mazurin for building it’s initial version).

Lets now see how it works:

Clone out Erlang Distribution in Docker SandBox Applucation: git clone https://github.com/oltarasenko/erlang_distribution_in_docker.git Checkout to epmdless_example branch: git checkout epmdless_example Build docker image for the application: cd erlang_distribution_in_docker && docker-compose build Bring up the containers described in docker-compose.yml: docker-compose up Let’s check that epmd is not really running on the nodes: docker-compose exec app1 lsof -i -n -P COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME beam.smp 1 root 13u IPv4 251627 0t0 TCP *:17012 (LISTEN) Login to one of the containers: docker-compose exec app3 _build/default/rel/sample_app/bin/sample_app console Connect to another node using epmdless: > epmdless_dist:add_node('app1@host1.com', 17012). % Add node into epmdless database > net_adm:ping('app1@host1.com'). % Do ping > nodes(). % Node is discovered

Here is the short video showing steps above executed:

Great, as you can see we’re able to connect nodes this way. Hovewer it looks like it would be quite hard to discover nodes this way, as we would need to initiate epmdless_dist:add_node and net_adm:ping calls for every node. That’s why we also want to suggest a convenient way (at least we hope you will like it) which allows you to also enable automatic node discovery for docker platform…

Nodes discovery on a distributed cluster

In order to be able to organise node discovery process we’ve released the Erlang Node Discovery application which automatically establishes connections between different nodes, as it’s specified in the application configuration file.

Besides this, it provides a simple API that allows you to extend the node discovery mechanism and to consume data from any other node/port database (local file, redis, database, etc).

{ erlang_node_discovery, [ % epmdless_dist as our database {db_callback, epmdless_dist}, {hosts, ["host1.com", "host2.com", "host3.com"]}, % query following appnames/ports {node_ports, [ {'app1', 17012}, {'app2', 17013}, {'app3', 17014} ]} ]}

This application will now spawn a process per possible pair of {appname@hostname, port} and will try to organise a fully connected cluster (and will do the reconnection).

In our case (as defined in configuration file above) it will spawn processes for the following possible nodes:

app1@host1.com:17012 app2@host1.com:17013 app3@host1.com:17014 app1@host2.com:17012 app2@host2.com:17013 app3@host2.com:17014 app1@host3.com:17012 app2@host3.com:17013 app3@host3.com:17014

Let’s give it a try:

Clone out Erlang Distribution in Docker SandBox Applucation: git clone https://github.com/oltarasenko/erlang_distribution_in_docker.git Checkout to master branch (needed in case you’ve already cloned everything on previous step): git checkout master Build docker image for the application: cd erlang_distribution_in_docker && docker-compose build Bring up the containers described in docker-compose.yml: docker-compose up

And again, here is a video summarising the steps above:

Ok, but what about Elixir?

Actually there is no problem re-using the same erlang libs to run a connected elixir cluster. The only trick is to show elixir where to find erlang libs :). This time I’ve added a Makefile to demonstrate it.

start: @ERL_LIBS=_build/dev/lib/ iex --name ${NODE_NAME} \ --erl "+K true" \ --erl "-config config/sys.config" \ --erl "-proto_dist epmdless_proto" \ --erl "-start_epmd false" \ --erl "-epmd_module epmdless_client" \ -S mix

Let’s now review the Elixir example:

git clone https://github.com/oltarasenko/epmdless.git docker-compose build docker-compose up

That’s all. Here is the short video demonstration!

We thought you might also be interested in: