|
| 1 | +--- |
| 2 | +title: "Understanding Application Configuration in Elixir" |
| 3 | +date: 2024-04-18 22:19:00 +1000 |
| 4 | +tags: Elixir configuration distillery release |
| 5 | +header: |
| 6 | + image: /assets/images/2024-04-19/banff_canada.jpg |
| 7 | + image_description: "Banff National Park" |
| 8 | + teaser: /assets/images/2024-04-19/banff_canada.jpg |
| 9 | + overlay_image: /assets/images/2024-04-19/banff_canada.jpg |
| 10 | + overlay_filter: 0.2 |
| 11 | + caption: > |
| 12 | + Photo by [Luca Bravo](https://unsplash.com/@lucabravo) |
| 13 | + on [Unsplash](https://unsplash.com/photos/banff-national-park-canada-oV4bR3YoR_s) |
| 14 | +excerpt: Application configuration can get interesting in Elixir. Let's dig ... |
| 15 | +--- |
| 16 | + |
| 17 | +Recently I started working on an Elixir project, which was a breath of fresh |
| 18 | +air, especially after two years mainly working with Node. However, it didn't |
| 19 | +take long before I stumbled upon something that puzzled me. |
| 20 | + |
| 21 | +In the production configuration files of this project, we have environment |
| 22 | +variable names interpolated in strings as follows: |
| 23 | + |
| 24 | +```elixir |
| 25 | +# config/prod.exs |
| 26 | +config :my_app, MyApp.Repo, |
| 27 | + hostname: "${DATABASE_HOSTNAME}", |
| 28 | + username: "${DATABASE_USERNAME}", |
| 29 | + ... |
| 30 | +``` |
| 31 | + |
| 32 | +Clearly this approach wouldn't work for the `:dev` environment, where we would |
| 33 | +typically see something like this: |
| 34 | + |
| 35 | +```elixir |
| 36 | +# config/dev.exs |
| 37 | +config :my_app, MyApp.Repo, |
| 38 | + hostname: System.get_env("DATABASE_HOSTNAME"), |
| 39 | + username: System.get_env("DATABASE_USERNAME"), |
| 40 | + ... |
| 41 | +``` |
| 42 | + |
| 43 | +However, I also knew that the interpolation syntax did work for us in |
| 44 | +production. This made me curious: How exactly does this work? It was time for |
| 45 | +some investigation. |
| 46 | + |
| 47 | +## The How |
| 48 | + |
| 49 | +After consulting with our old friend Google and chatting with our new friend |
| 50 | +GPT-4, I learnt that this peculiar method of setting configuration values from |
| 51 | +environment variables is facilitated by Distillery, the tool we use for building |
| 52 | +releases. |
| 53 | + |
| 54 | +For this functionality to be enabled, the environment variable `REPLACE_OS_VARS` |
| 55 | +must be set to `true`. And then Distillery would replace environment variables |
| 56 | +with their actual values upon encountering interpolation syntax, such as |
| 57 | +`"${DATABASE_USERNAME}"`, in the configuration files. |
| 58 | + |
| 59 | +As a matter of fact, when it comes to handling application configuration, |
| 60 | +Distillery supports more than simple variable interpolation. It also supports |
| 61 | +Config Providers, which are custom modules capable of dynamically generating |
| 62 | +configuration at the start of your application. For further details, please |
| 63 | +refer to the relevant section in [Distillery documentation][distillery-doc]. |
| 64 | + |
| 65 | +## The Why |
| 66 | + |
| 67 | +While it's enlightening to learn how it works, I kept wondering: why does |
| 68 | +Distillery offer support for both interpolation and Config Providers? Isn't |
| 69 | +`System.get_env` good enough for this purpose? As it turns out, although |
| 70 | +`System.get_env` is quite effective for the `:dev` environment, it can be |
| 71 | +inadequate for the production environment in many cases. |
| 72 | + |
| 73 | +To grasp this, we need to take a step back and have a quick review of how Elixir |
| 74 | +the language works. Elixir is a compiled language. When you execute `mix |
| 75 | +compile`, the Elixir compiler transforms your project into Erlang bytecode. This |
| 76 | +bytecode then runs on the Erlang Virtual Machine, commonly known as |
| 77 | +Bogdan/Björn's Erlang Abstract Machine (BEAM). |
| 78 | + |
| 79 | +The configuration files and code that are outside of function bodies within a |
| 80 | +module are evaluated at compile-time. Thus, if you use |
| 81 | +`System.get_env("DATABASE_HOSTNAME")` within a configuration file, it's |
| 82 | +evaluated during the compilation process. This means the value of the |
| 83 | +environment variable `DATABASE_HOSTNAME` is then captured and embedded in the |
| 84 | +application as a static value. |
| 85 | + |
| 86 | +For production configurations, the goal is generally to dynamically capture |
| 87 | +runtime environment variables, rather than to embedding those from the build |
| 88 | +server as static values. Apparently using `System.get_env` wouldn't achieve this |
| 89 | +purpose. |
| 90 | + |
| 91 | +That is precisely why Distillery supports both the interpolation syntax and |
| 92 | +Config Providers, enabling dynamic configuration value setting from the runtime |
| 93 | +environment of the application. |
| 94 | + |
| 95 | +It's noteworthy that with version 1.9, Elixir introduced built-in support for |
| 96 | +[releases][mix-release] as well, which includes a novel approach to runtime |
| 97 | +configuration. This was achieved through the introduction of a new configuration |
| 98 | +file `config/releases.exs`, which is executed every time a release starts. Then |
| 99 | +in Elixir 1.11, another configuration file `config/runtime.exs` was introduced, |
| 100 | +which is essentially a superior version of `config/releases.exs`. For more |
| 101 | +details on these developments, please consult to the [relevant |
| 102 | +documentation][elixir-1-11-change]. |
| 103 | + |
| 104 | +## Summary |
| 105 | + |
| 106 | +In this post, I shared my initial confusion about the interpolation syntax |
| 107 | +`"${DATABASE_HOSTNAME}"` in our production configuration files, followed by an |
| 108 | +exploration into understanding how it works and why is it necessary for runtime |
| 109 | +configuration. As always, my aim is to provide a reference for my further self |
| 110 | +and perhaps assist others along their journey. |
| 111 | + |
| 112 | +[distillery-doc]: https://hexdocs.pm/distillery/config/runtime.html |
| 113 | +[elixir-1-11-change]: https://hexdocs.pm/elixir/1.11.0/changelog.html#config-runtime-exs-and-mix-app-config |
| 114 | +[mix-release]: https://hexdocs.pm/mix/Mix.Tasks.Release.html |
0 commit comments