How to config environment variables with Elixir and Exrm

It’s very common (and highly recommended) that application keeps its configuration values separated from its version control. A way of doing this is by using ENV vars (environment variables). They’re being used for improvements mostly on maintainability. The 12-factor app manifesto explains it on its Configuration section:

The twelve-factor app stores config in environment variables (often shortened to env vars or env). Env vars are easy to change between deploys without changing any code; unlike config files, there is little chance of them being checked into the code repo accidentally; and unlike custom config files, or other config mechanisms such as Java System Properties, they are a language- and OS-agnostic standard.

In an Elixir project, the config goes in Mix.Config files. Some examples are: config.exs and environment config files (dev.exs, test.exs and prod.exs). These files are generally used by frameworks and libraries, but they have already proven useful for using mocks in our tests.

Let’s take an Ecto config as example:

# config/dev.exs
config :myapp, MyApp.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "myapp_dev",
hostname: "localhost",
pool_size: 10

A well-known approach is using Environment variables to hide and scope these values through different environments. To use it, we just need to have a configured variable and get it in our application. In Elixir we do this easily with System.get_env("ENV_VAR").

We could configure our last example with this approach:

# config/dev.exs
config :myapp, MyApp.Repo,
adapter: Ecto.Adapters.Postgres,
username: System.get_env("DB_USER"),
password: System.get_env("DB_PASSWORD"),
database: System.get_env("DB_NAME"),
hostname: System.get_env("DB_HOST"),
pool_size: 10

This way you won’t expose your database configs and will actually make things more dynamic. In development this is useful because the developers won’t need to make changes on this file, they’ll just need to export these vars.

So far this isn’t much different from what we do in other languages. However, things start to happen differently when we try to generate an Exrm release to deploy our app in production.

ENV vars need to be present during compile time

We all already know that Elixir is a compiled language. And in order to deploy or generate a release we need to compile our application. So everything is compiled, even our config files! Then, there’s an interesting behavior while compiling our config files.

Our System.get_env() calls will be evaluated during the compilation, so the binaries will be generated with the current value of the ENV var. Because of this, we need all of our environment variables to be exported during compiling. When we don’t have them, their value will be nil and we won’t be able to connect to our database, for example. This way, to build a release we’d need all our environment variables where we’re building it (our own machine or a build server).

If we’re working with Phoenix, there is an exception. Phoenix has a special way of configuring an HTTP port with ENV vars that evaluates it during runtime.

config :myapp, MyApp.Endpoint,
http: [port: {:system, "PORT"}],
# ...

It works great and data won’t be fixed in the release, but it’s specific for this Phoenix config. But don’t be sad! There are already some mature discussions around this in the Exrm repo, take a look, you may be able to help!

There’s a way when using Exrm release

I was chatting around Elixir Slack channel when our friend Ranelli mentioned that there was a simple technique that we could use to solve this when we build an Exrm release. Instead of using System.get_env in our configs, we must use "${ENV_VAR}". Then, we just need to run our release with RELX_REPLACE_OS_VARS=true.

RELX_REPLACE_OS_VARS=true rel/myapp/bin/myapp start

This will make our release to use the values represented by these special strings. I’ll explain.

An Exrm release has two important files: sys.config and vm.args. These files are responsible by the data used in production (usually what’s in config.exs and prod.exs) and specific configs that we can make of the Erlang VM respectively.

sys.config

[{sasl,[{errlog_type,error}]},
{logger,
[{console,
[{format,<<"$time $metadata[$level] $message\n">>},
{metadata,[request_id]}]},
{level,info}]},
{myapp,
[{'Elixir.MyApp.Endpoint',
[{root,<<"/Users/igorffs/src/myapp">>},
{render_errors,[{accepts,[<<"html">>,<<"json">>]}]},
{pubsub,
[{name,'Elixir.MyApp.PubSub'},
{adapter,'Elixir.Phoenix.PubSub.PG2'}]},
{http,[{port,<<"${PORT}">>}]},
{url,[{host,<<"localhost">>}]},
{cache_static_manifest,<<"priv/static/manifest.json">>},
{server,true},
{secret_key_base,
<<"${SECRET_KEYBASE}">>}]},
{'Elixir.MyApp.Repo',
[{adapter,'Elixir.Ecto.Adapters.Postgres'},
{username,<<"${DB_USER}">>},
{password,<<"${DB_PASSWORD}">>},
{database,<<"${DB_NAME}">>},
{hostname,<<"localhost">>},
{pool_size,10},
{port,<<"15432">>}]}]},
{phoenix,[{generators,[{migration,true},{binary_id,false}]}]}].

vm.args

## Name of the node
-sname myapp

## Cookie for distributed erlang
-setcookie myapp

## Heartbeat management; auto-restarts VM if it dies or becomes unresponsive
## (Disabled by default..use with caution!)
##-heart

## Enable kernel poll and a few async threads
##+K true
##+A 5

## Increase number of concurrent ports/sockets
##-env ERL_MAX_PORTS 4096

## Tweak GC to run more often
##-env ERL_FULLSWEEP_AFTER 10

Exrm is using a lib called relx under the hood to build its releases. When we exported RELX_REPLACE_OS_VARS=true relx will make a replace of the strings by their correspondent ENV var values in the config files.

{'Elixir.MyApp.Repo',
[{adapter,'Elixir.Ecto.Adapters.Postgres'},
{username,<<"${DB_USER}">>},
{password,<<"${DB_PASSWORD}">>},
{database,<<"${DB_NAME}">>},
{hostname,<<"localhost">>},
{pool_size,10},
{port,<<"15432">>}]}

You’ve noticed where our special strings are in the sys.config, and if you guessed that this process can be done manually, you got it! But this replace really makes things easier for us. Otherwise, we would have to edit every option in the file. It’s very important to mention, if you change those files, you’ll have to reboot your application.

Considerations

This subject is very important if we’re going on production. It concerned us a bit when we’ve noticed that we couldn’t have more dynamic configs. This replacement solution was a relief. Make sure to keep following the discussion I mentioned before, things are probably going to change after it.

Have you already been in trouble dealing with ENV vars? How did you solve it?


What's new in Ecto 2.0 -- Reserve your copy

Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someone
  • Geovane Fedrecheski

    Isn’t it vm.args? 🙂

  • Igor Florian

    Fixed! Thanks <3

  • Bradley

    We use a config gen_server to read in config files and start them first in our supervisor. YMMV

  • Igor Florian

    That seems to be a very interesting approach, I’d really appreciate some examples or to discuss it with you! o/

  • Bradley

    Let me try to put something together. I am always happy discussing code and systems =]

  • Bradley

    Igor, here is a high level view of what we are doing. Please reach out to me if you want to discuss this further.
    We use two approaches:

    The first, we use a gen_server that loads a configuration file upon application start–first child in our Supervisor. To get information from config file just send gen_server call message. We also create a ‘reload’ function that will re-read configuration file and update state.

    The second option is a copy of Mix.Config–slightly modified. It is a module that can be included in our release. This copy behaves the same way as Mix.Config. Once again we use a gen_server here to load configuration from a file or directory and save state. The good thing about second approach is we can use Config syntax in our configs. This gives us much more flexibility.

    Brad

  • Great! It solved my problem connecting a Phoenix application with Postgres via Ecto configuration.

    Thank you!

  • Joshua

    Wish we could hot-config apps, as with the Lua config in Nginx Plus. 🙂

  • Paul Daigle

    At Manheim we built a small app called env_helper (https://hex.pm/packages/env_helper) to handle environment variables and application variables. The motive was mostly to have default values defined for environment variables that might not be set in development.

  • Bradley

    I threw this into a hex package
    https://hex.pm/packages/external_config

    There is an example how we are using it on git repo.

    Brad