Setting Up an Erlang Cluster on EC2

Generating a working release to deploy to EC2 required a fair bit of trial and error. This post assumes that you have a set of working OTP applications that you would like to deploy on multiple machines. Let’s call these applications example_app1 and example_app2.

Project Structure

I like to organize my files and folders as follows:

1
2
3
4
5
6
7
8
|- apps
|  |- example_app1
|  |  |- ...
|  |- example_app2
|  |  |- ...
|- rel
|  |- ...
|- rebar.config

All applications go in the apps folder. Release specific files, configuration, and the bundled release itself go in the rel folder. The rebar.config at the top level looks like the following:

1
2
3
4
5
6
7
8
{deps_dir, ["deps"]}.
{deps, []}.
{sub_dirs, [
            "apps/example_app1",
            "apps/example_app2",
            "rel"
           ]
          }.

With this config file, we can run rebar compile at the root level of our project and it will automatically compile every application.

Generating the release

The release will bundle together your applications along with any dependencies necessary to run them on a similar system. It will also bundle in the Erlang RunTime System (ERTS) so that the release executables can be run without Erlang installed on the target machine. Make sure, however, that the build machine and the target machine are of similar (if not identical) architectures.

First, you’ll want to use rebar to generate a set of files, among which is the all important reltool.config file. You can do this by running in the rel directory, rebar create-node nodeid=example, substituting nodeid for your project name or whatever you want. Then modify it to look like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
{sys, [
       {lib_dirs, ["../deps/", "../apps"]},
       {erts, [{mod_cond, derived}, {app_file, strip}]},
       {app_file, strip},
       {rel, "example, "1",
        [
         kernel,
         stdlib,
         example_app1,
         example_app2
        ]},
       {rel, "start_clean", "",
        [
         kernel,
         stdlib
        ]},
       {boot_rel, "example"},
       {profile, embedded},
       {incl_cond, derived},
       {mod_cond, derived},
       {excl_archive_filters, [".*"]}, %% Do not archive built libs
       {excl_sys_filters, ["^bin/.*", "^erts.*/bin/(dialyzer|typer)",
                           "^erts.*/(doc|info|include|lib|man|src)"]},
       {excl_app_filters, ["\.gitignore"]},
       {app, hipe, [{incl_cond, exclude}]},
       {app, example_app1, [{mod_cond, app}, {incl_cond, include}]},
       {app, example_app2, [{mod_cond, app}, {incl_cond, include}]}
      ]}.

{target_dir, "example"}.

{overlay, [
           {mkdir, "log"},
           {copy, "files/erl", "\{\{erts_vsn\}\}/bin/erl"},
           {copy, "files/nodetool", "\{\{erts_vsn\}\}/bin/nodetool"},
           {copy, "files/example, "bin/example},
           {copy, "files/example.cmd", "bin/example.cmd"},
           {copy, "files/start_erl.cmd", "bin/start_erl.cmd"},
           {copy, "files/install_upgrade.escript", "bin/install_upgrade.escript"},
           {copy, "files/sys.config", "releases/\{\{rel_vsn\}\}/sys.config"},
           {copy, "files/vm.args", "releases/\{\{rel_vsn\}\}/vm.args"}
          ]}.

As you can see, we’ve added a line that looks like

1
{app, example_app1, [{mod_cond, app}, {incl_cond, include}]}

for every application we want included and we’ve also included the app name in the sys –> rel option. These applications will all be executed on start.

Configure App Config

By default the node name is set to example@127.0.0.1 but we probably want to strip the attached ip address. To change this edit the -name flag passed in rel/files/vm.args.

1
-name example

Any application specific configuration should happen in rel/files/sys.config. This file also gets copied into the release bundle (you can see all the file and directory manipulation in the overlay option of the reltool.config file). For now, make the sys.config file look like

1
2
3
4
5
6
[
 {kernel, [
           {inet_dist_listen_min, 9100},
           {inet_dist_listen_max, 9200}
          ]}
]

These two kernel options will restrict the port range for inter-node communication. This is important if you plan on deploying on instances that will be part of a security group or behind a firewall. You can put other application configuration variables in this file as well.

Generate the release and deploy.

Now, generate the release by running

1
rebar compile generate

Note that you may need to run rebar get-deps first if your applications have any dependencies. This will bundle your applications, ERTS, dependencies, configuration files, and arguments all into rel/example. Nifty!

You can test it out by running ./rel/example/bin/example start and a subsequent ps aux | grep example should show all the processes now running attached to your project.

To deploy to EC2, you need to make sure that all instances in the cluster have the following ports open:

1
2
9100-9200
4369

Port 4369 is used by the Erlang Port Mapper Daemon (epmd). Finally, bundle up your release and run it on all your instances. To connect your instances:

1
net_adm:ping('example@*insert ip here*').

You should get a pong as a response if you did everything correctly.

Further down the road

To get this example configuration working, many things were done manually. Steps that can (and should) be automated are the generation, distribution, and deployment of each release. In addition, we can automate adding nodes to a cluster by including a ping as part of our bundled application or by making a custom boot script (or by running a separate script after the deploy).

I struggled a lot to get a working configuration as the use of rebar and reltool is not obvious (at least not to me). Hope this helps somebody.

Comments