josevalim
Proposal: moving towards discoverable config files
One of the major differences between running your application as a release and as a Mix project is the differences in configuration. Mix evaluates the configuration right before the application starts, releases evaluates the configuration when your application is compiled.
This implies in a large mismatch of how those two environments are used. For releases, environment variables (read by System.get_env/1) need to be set when the application is compiled and such information may not be available at this point.
Ideally, we would want a release to evaluate the configurations files in config when the release starts. One approach would be to copy the configuration files as is to the release but that’s hard to achieve in practice for two reasons:
-
A config file may import other config files and often importing those files happen dynamically. For example:
import_config "#{Mix.env()}.exs". The dynamic import makes it hard for release tools to know which configuration files must be copied to a release, especially in cases like umbrella projects, where a developer may load configuration across projects -
Even we copy today’s configuration files to a release, those configuration files rely on
Mix, which is a build tool and therefore it is not available during releases
To solve those issues, we need to make sure we can discover all imports of a configuration file without evaluating its contents. We also need to introduce a new module for configuration that does not depend on Mix.
This is the goal of this proposal.
Application.Config
This proposal is about introducing a module named Application.Config. It will work similarly to the existing Mix.Config, except it belongs to the :elixir application instead of :mix. This allows releases to leverage configuration without depending on Mix.
The user API of Application.Config is quite similar to Mix.Config. There is config/2 and config/3 to define configurations. There still is import_config/1 to import new configuration files with one important difference: the argument to import_config/1 must be a literal string. So interpolation, variables or any other dynamic pattern is no longer allowed.
In order to help with configuration management, we will introduce a project option in your mix.exs, named :config_paths to help manage multiple required and optional configuration files.
In the next section we will provide an example of how configuration files used by projects like Nerves and Phoenix will have to be rewritten and then we will discuss how integration with release tools such as distillery will work.
A common example
Projects like Nerves and Phoenix generate files with built-in multi-environment configuration. Today, this configuration has an entry point config/config.exs file that imports an environment specific configuration at the bottom:
# config/config.exs
use Mix.Config
config :my_app, :some_shared_configuration, ...
import_config "#{Mix.env()}.exs"
And then each config/{dev,test,prod}.exs provides environment specific configuration. For instance:
# config/dev.exs
use Mix.Config
config :my_app, :some_dev_configuration, ...
The issue in the example above is the use of dynamic imports, such as import_config "#{Mix.env()}.exs". We will address this by defining both config/config.exs and config/#{Mix.env()}.exs as configuration entry points in your mix.exs:
# mix.exs
def project do
[
...,
config_paths: ~w(config/config.exs config/#{Mix.env()}.exs),
...
]
end
And now we can define those configuration files without dynamic imports:
# config/config.exs
import Application.Config
config :my_app, :some_shared_configuration, ...
# config/dev.exs
import Application.Config
config :my_app, :some_dev_configuration, ...
In Phoenix, the config/prod.exs case may link to a separate prod.secret.exs file. While we could also refer to this file in the :config_paths configuration in the mix.exs file, because it is only specific to production, it is more straight-forward to continue importing it at the bottom. So a config/prod.exs would look like this:
# config/prod.exs
import Application.Config
config :my_app, :some_prod_configuration, ...
import_config "prod.secret.exs"
By adding :config_paths, we are able to move the dynamic configuration to the mix.exs file and make the order that configuration files are loaded clearer.
A FarmBot example
Nerves projects tend to rely extensively on configuration files. So let’s look into existing open source Nerves projects and see how this proposal will fare. Let’s take a look at FarmBot v6.4.1.
The questions we want to answer are: if we move the FarmBot project to the proposed Application.Config, will they be able to express of all the existing idioms they do today? And, even further, will their configuration files become simpler or more complex?
From looking at its config/config.exs, we can already see a pattern that won’t work in releases: the use of Mix.env and Mix.Project.config.
We can see those variables are used to dynamically import configuration, which Application.Config won’t allow.
Those idioms are perfectly fine with how configurations work in Mix today. But they will no longer with a release built on top of Application.Config.
The solution is to move all of those imports to the :config_paths option in mix.exs. However, note that some of those dynamic imports are optional, so we will also need the ability to explicitly tag them as such:
# farmbot/mix.exs
def project do
[
...,
config_path: ~w(config/config.exs config/#{Mix.env()}.exs) ++
optional_config_paths(@target, Mix.env())
...
]
end
defp optional_config_paths("host", env),
do: [{:optional, "config/host/#{env}.exs"}]
defp optional_config_paths(target, env),
do: [{:optional, "config/target/#{env}.exs"}, {:optional, "config/target/#{target}.exs"}]
We believe this approach is an improvement to the previous one because it allows all environment and target specific handling to remain in the mix.exs file and not scattered around multiple configuration files.
Using it in releases
In the previous sections, we have outlined Application.Config which no longer depends on Mix and has a restricted import_config.
Now that we are able to see all of the configuration files that affect our system, a release tool, such as distillery, should be able to traverse all of those configuration files and merge them into a final config/release.exs that will be part of your release. In fact, Elixir will provide a convenient API that performs such operation, streamlining the release assembling process.
Unresolved topics
There are two important topics that we have not included in this proposal and they will be discussed in a further step.
-
What about umbrella projects? Umbrella projects also rely on configuration and we need to make sure the listed mechanisms also work well with umbrellas.
-
How to avoid common pitfalls? Even though we will migrate to
Application.Config, there is nothing stopping a developer from accessing Mix (and the module defined in themix.exsfile) from their new config files. As we have seen, this may lead to errors when running releases, as releases do not have Mix available. To address this, we may introduce checks when assembling releases that make sureMixis not invoked in configuration files, raising appropriate error messages in case they do.
Summing up
We propose a new Application.Config module and a new :config_paths project option that allows release tools to discover all of the relevant configurations in a system. Release tools can then merge and copy those configuration into releases and execute them as part of the release process, allowing dynamic calls such as System.get_env/1 to work in development and in production transparently, with or without releases.
Trending in Proposals
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #elixirconf-us
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #hex
- #performance










First 10 of 108 Posts
blatyo
Would it also make sense at this point to separate build time configuration from runtime configuration?
The example of build time configuration that comes to mind is:
https://github.com/elixir-plug/mime
Once you’ve built with a particular configuration, it no longer matters what value you’ve set it to. It also acts as a bit of a gotcha about how to pick up a change if you’ve already built once.
Nicd
With this proposal, would Mix.Config still exist for compile time configuration, and in what file would it be in?
What does the
:optionaltag mean? What does it actually do? Sorry if it’s a silly question but I didn’t really get it from the explanation.JEG2
I suspect it means something like: process this file if it exists.
josevalim
Application configuration is a general key-value store, which means that if you do a typo on a configuration key, you may spend some time chasing why a configuration is not working only to realize it was a simple typo. If we add a distinction between runtime and compile-time, we are adding a new “vector” for misplacing configuration and a new source of confusion. Basically, we would be pushing this concern to users without giving them any support when something goes wrong.
Before we split configuration between runtime and compile time, we need to have a more declarative approach to configuration so they are easier to document and easier to check. So it is definitely something we intend to do, but we are not adding this distinction now.
It will continue to work the same. We are not making a distinction between compile time and runtime. You just put them in the same files. Just the top of the file changed from
use Mix.Configtoimport Application.Config.Bingo!
Nicd
Then how would we do actual compile time configs? For example I use this in my
config.exsto set some values when building the release:Anyway I appreciate the work on the config system, definitely an area that can use some improvement and clarification.
josevalim
It will work the same as today: the config file will be read before your code is compiled. It will also be read before your code runs (via mix run) and also before your release starts.
You probably don’t want to run those bits inside a release though. You can control those things with the new config_paths though.
michalmuskala
Would
if_exists: "path"be better thanoptional: "path"in that case?Eiji
I see what you mean, but I think that
optionalfor most will be simpler to remember especially where people are familiar with optional/required naming.net
It strikes me as potentially confusing that the same config might produce different results at compile time and runtime. It seems such a change would benefit from making the execution environment for a config expression explicit and restricted.
Qqwy
Very interesting! I remember you guys discussing the configuration challenge during ElixirConf.EU; I am really happy that you were able to come up with such a clean looking solution! I definitely think that inversion-of-control is the way to go with configuration files.
I had the same question, especially since it was combined with the example where we have multiple function clauses based on the current building target.
I personally think that
if_existsis a lot more explicit in what it checks for thanoptional.Question: What would, using the new
Application.Config, be the way to read out system environment variables during compilation time? Is the idea to ‘just fall back on the currentMix.Configin that case’ or will that one be deprecated at some point?