Dealing with runtime environment variables in Elixir

Why do I need runtime environment variables?

Because it is super cool! I just need to build my app Docker image in my CI/CD and then I can run it in any deployment environments.. Staging, QA, Production… This is the freedom of 21st Century!

It would be only 1 paragraph if we are talking about using environment variables in Ruby. In Ruby, you can use environment variables anywhere in your app and it just works. But it will be different story in Elixir.

I have been headache around this issue since I’ve been working with Elixir and it is still counting.

“closed-up photo of gray bird” by James Toose on Unsplash

Firstly, Elixir compiles your app into BEAM byte code before you can run the app. So that, your environment variables will be evaluated at that time where it could be your local machine or CI/CD pipeline. Not your Staging or Production, Seriously!

The first time I’ve been into this issue. This issue is so simple to me to deal with.

Just compiled when deploying in the specific environment. If you are about to deploy on Staging, just compile on staging.

This approach it makes me very confident that all my environment variables will be set up right as expected… But wait… With small Phoenix application it could take time for compiling your app and app’s dependencies stuffs for stuffs 2–3 minutes. That means if you application crashes for any reasons, you will need 2–3 minutes compiling at boot and restart the app again. Don’t even think for medium size or big size application which could take 5–10 minutes when booting. If you can keep your Elixir application very tiny and 1–2 minutes downtime is not an issue then this is the sweet spot. Sadly, I can’t live with it….


“blue-and-white bottle lot” by Annie Spratt on Unsplash

Distillery is a guy!

Distillery is tool to make the deployment simpler. It not only helps for making compile-time environment variables and run time environment variables in your release. It also does many cool stuffs which I don’t really use them much like packaging your app with Erlang runtime ERTS together, managing Erlang magic cookies for cluster nodes, Hot updates, etc.

In this article, I am telling you how to setup Distillery. So Please follow Announcing Distillery 2.0. It is straightforward. Distillery version 2 is recommended there are lot of improvements since version 1.

So yes, everybody tell you that the deployment is simpler. However, we shouldn’t underestimate Elixir. Distillery is not silver bullet that magically turn all environment variables in your application to be runtime..

Why?

It depends how you write Elixir code. In some places, it will be forced to be evaluated at compile time. Distillery couldn’t help you. For example:

Module Attributes. Becareful Module Attributes [a good article ]

or sometimes you might have trouble like this in Phoenix Router

Those are some common use cases of the environment variables that get evaluated at compile time. The pain is there is no explicit way to tell us which environment variables will be compile or runtime.


How to prevent and make sure that all your environment variables is not being evaluated at compile time?

I haven’t found the best way yet. But Good news! I have built an automated tool as an add-on of Credo (a popular Elixir Static Code Analysis tool) for screening some common patterns that could be happening like the above examples. It is not 100% guarantee yet but at least it will give me and my team some safety net and feel less worries about compile environment variables. Thanks to Credo that it is super easy to be extended.

And The HERO is CredoEnvvar !

How to use CredoEnvvar (In summary)

  • Make sure that you already setup credo in your mix.exs deps
  • Add credo_envvar to your mix.exs deps
  • Run `mix deps.get`
  • Add CredoEnvvar module into .credo.exs like this:
  • Run `mix credo`

Some useful tricks

  • Use environment variables in the simplest way as we can.
  • Avoid default config setup in config/config.exs if your environment variables get compiled, you might see your app breaking during compile time. It is a lot better than it breaks at runtime.
  • Move environment variables inside def or defp. In most of the cases, those environment variables will be evaluated at runtime.

For example: