Elixir release with Distillery

May 25, 2019

Create an Elixir release with Distillery

After you have spent time to develop your Elixir application, you have the challenge of deploying it. I call it a challenge because, without taking into account complex scenarios (scalability, reliability and so on), you will be pretty soon bumping into some unexpected behaviours; this happens because of the difference between build time and run time environment.

Build time vs run time

During development you run your application by launching

$ mix phx.server

and probably you have a set of exported environment variables that are used by your application, in config|dev|..|.exs file, like this:

config :app,
    payments_key: System.get_env("PAYMENTS_KEY")

which could be the key of some remote payment service your app is using. As long as you are using mix phx.server you are de facto running a mix application, and this solution will work.

If you, instead, create a production build and run it, properly exporting environment variables would not just work.

This happens because the System.get_env(..) instruction is evaluated at compile time, and so, providing the values after such step would cause an empty value to be found instead of the expected one.

Imagine we have a controller that loads the values of a given application key:

served at /api/keys; by adding the following configuration in config.exs (so in the base file, extended by [dev|test|prod].exs files) and running the app you’ll see:

$ PAYMENT_KEY=pk mix phx.server
Compiling 11 files (.ex)
Generated app app
[info] Running ElixirTestOneWeb.Endpoint with cowboy 2.6.3 at 0.0.0.0:8080 (http)
[info] Access ElixirTestOneWeb.Endpoint at http://localhost:8080

$ curl http://localhost:8080/api/keys | jq
% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                Dload  Upload   Total   Spent    Left  Speed
100    29  100    29    0     0    475      0 --:--:-- --:--:-- --:--:--   483
{
    "keys": {
        "payment_key": "pk"
    }
}

And it will work for all the other envs (try running MIX_ENV=prod PAYMENT_KEY=pk mix phx.server and verify).

Release creation

Let’s try what happens if we create a release build:

# Needed to generate the release configuration file
$ MIX_ENV=prod mix release.init

An example config file has been placed in rel/config.exs, review it,
make edits as needed/desired, and then run `mix release` to build the release

$ MIX_ENV=prod mix release --env=prod 
==> Assembling release..
==> Building release app:0.1.0 using environment prod
==> Including ERTS 10.3.1 from /usr/local/Cellar/erlang/21.3.2/lib/erlang/erts-10.3.1
==> Packaging release..
Release successfully built!
To start the release you have built, you can use one of the following tasks:

    # start a shell, like 'iex -S mix'
    > _build/prod/rel/app/bin/app console

    # start in the foreground, like 'mix run --no-halt'
    > _build/prod/rel/app/bin/app foreground

    # start in the background, must be stopped with the 'stop' command
    > _build/prod/rel/app/bin/app start

If you started a release elsewhere, and wish to connect to it:

    # connects a local shell to the running node
    > _build/prod/rel/app/bin/app remote_console

    # connects directly to the running node's console
    > _build/prod/rel/app/bin/app attach

For a complete listing of commands and their use:

    > _build/prod/rel/app/bin/app help

and run it as suggested by the resulting output:

$ PAYMENT_KEY=pk HOSTNAME=localhost PORT=8080 _build/prod/rel/app/bin/app foreground

Trying to hit the endpoint for the values will generate the following result:

$ curl http://localhost:8080/api/keys | jq  
% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                               Dload  Upload   Total   Spent    Left  Speed
100    29  100    29    0     0   4443      0 --:--:-- --:--:-- --:--:--  4833
{
    "keys": {
        "payment_key": null
    }
}

This happens because res = Application.get_env(:app, :config_keys) is trying to access a value that has been evaluated at build time, while we provided (correctly) at run time.

Distillery config providers

The proper way to handle this is by using Distillery Config Providers: long story short, it’s a way to inject configuration that will be evaluated at run time, so that System.get_env commands will be correctly valued.

It’s a very easy solution, because all it takes is to prepare a proper configuration file to be used during the release build.

The first step is to create the release file with MIX_ENV=prod mix release.init. This will create a file config.exs that contains information about environments and releases (info). In order to inject the proper configuration provider, you need to add, in the prod environment, the following config lines:

set(
    config_providers: [
        {Mix.Releases.Config.Providers.Elixir, ["${RELEASE_ROOT_DIR}/config/runtime.exs"]}
    ]
)

set(
    overlays: [
        {:copy, "config/runtime.exs", "config/runtime.exs"}
    ]
)

This tells the system to make two things: to copy config/prod.exs as ${RELEASE_ROOT_DIR}/config/runtime.exs and use it as configuration file.

So, if we now build the app, everything will work fine:

$ ~/dev/elixir/gcp_article/app(master*) » MIX_ENV=prod mix release --env=prod
==> Assembling release..
==> Building release app:0.1.0 using environment prod
==> Including ERTS 10.3.1 from /usr/local/Cellar/erlang/21.3.2/lib/erlang/erts-10.3.1
==> Packaging release..
Release successfully built!
To start the release you have built, you can use one of the following tasks:

    # start a shell, like 'iex -S mix'
    > _build/prod/rel/app/bin/app console

    # start in the foreground, like 'mix run --no-halt'
    > _build/prod/rel/app/bin/app foreground

    # start in the background, must be stopped with the 'stop' command
    > _build/prod/rel/app/bin/app start

If you started a release elsewhere, and wish to connect to it:

    # connects a local shell to the running node
    > _build/prod/rel/app/bin/app remote_console

    # connects directly to the running node's console
    > _build/prod/rel/app/bin/app attach

For a complete listing of commands and their use:

    > _build/prod/rel/app/bin/app help

$ ~/dev/elixir/gcp_article/app(master*) » PAYMENT_KEY=pk HOSTNAME=localhost PORT=8080 _build/prod/rel/app/bin/app foreground
13:36:53.482 [info] Running ElixirTestOneWeb.Endpoint with cowboy 2.6.3 at 0.0.0.0:8080 (http)
13:36:53.482 [info] Access ElixirTestOneWeb.Endpoint at http://8080:8080

$ » curl http://localhost:8080/api/keys | jq
% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                  Dload  Upload   Total   Spent    Left  Speed
100    29  100    29    0     0   4583      0 --:--:-- --:--:-- --:--:--  4833
{
    "keys": {
        "payment_key": "pk"
    }
}

At this point, you can orchestrate your deployment making proper configuration provisioning.

The future

Version 1.9 of Mix.Config will contain the release command, so to import the release features now offered by distillery and much more. Read about it here.

Note: the code for this article is published here.

Photo by John Barkiple on Unsplash