Bruno Ripa
4 Jun 2019
•
4 min read
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.
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:
{% gist 3737b82a9a3addc88f23c84e747c7e23 %}
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).
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.
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.
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
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!