Mix config evolutions

I am opening this thread to discuss the global problems and solutions around the current mix config implementation.

As seen in multiple github issues and in multiple places around this forum, the current configuration behaviour via mix config.exs has it’s issues.

The most common ones being:

  • Confusion as config being compile time, including reading the environment
  • {system, "MYENV"} being somewhat present but considered bad practice by @josevalim
  • Difficulties around secret handling
  • No standard way of defining config requirements/schema for libraries

I’d like this thread to be used to acknowledge that this is an issue we need to solve, find at what level and how it should be (in mix, with a DSL…).

4 Likes

I think the question we want answered first is whether the general advise should be to run your stuff in production as mix run (where Mix configurations work just fine) or as Erlang distributions (where some ugliness crops up that probably could be documented better). I invested a ton of time to have clean multi-environment distributions using some scripting on top of Distillery, but although the thing works, I’m less than happy with it and maybe the answer is just "don’t bother, use mix run"…

1 Like

Hello, this is how we organize configuration of our Elixir/Phoenix app:

  1. Software/deps:

    • ansible: provisioning / app install and initial config.
    • distillery: releases
    • edeliver: deploy
  2. All default app config (convention over configuration approach) is in mix.exs, in env tuple of OTP application:

  def application do
    [mod: {AppName, []},
    env: env(),
    applications: [.....]]
  end

  defp env() do
    [
      some_config: 1,
      another_conf: "value"
    ]
  end
  1. Config overrides are only in config/dev.exs and config/test.exs. File config/prod.exs only contains configuration of external apps.

  2. Ansible (jinja2) templates for /etc/appname/sys.config and
    /etc/appname/vm.args on production servers. Both files are owned by root:appname and mode=640. Then symlink these files from /opt/appname/sys.config and /opt/appname/vm.args during application installation process. Note: since all properties have a default value in the “env” tuple of OTP application, you only need to include in sys.config properties that change their value in production.

  3. Ansible group_vars and/or host_vars with specific configuration values (encrypted with ansible-vault and shared in VCS). These values includes database password, API keys, etc.

  4. Never use module attributes with external configuration, like:

@value Application.get_env(:appname, :value, 1)

Instead, use this:

@default_value 1
...
def ... do
  value = Application.get_env(:appname, :value, @default_value)
end
  1. This approach does not require config/prod.secret.exs.

Pending: consider to use conform.

I would like to hear about experiences and approaches about configuration in other real world apps.

2 Likes

Can we put that in boldface and blink? In fact, it should trigger a compiler warning or linter error.

3 Likes

I think we need some kind of standard solution that addresses the following:

  • Multiple environments
  • Runtime configuration
  • Discoverable (documentation and schema)
  • Supporting different delivery scenario (mix run, distillery…)

My opinion is that it should be handled at the lowest level possible to have a common configuration mechanism for all elixir apps.

2 Likes

This is a good discussion! Can you please elaborate those two points:

I want to make sure I understand all concerns before giving a reply. :slight_smile:

1 Like

Secret handling

It has been proven too many times that it is really easy to leak secrets for an app. So people came up with tons of different ways of handling it. Like adding a config.secrets.exs that should not be committed to git or env var (things like direnv help). I think we should integrate this in our thinking of a configuration system. We need to be able to inject external secrets (like with env var) and we need to avoid running bad “default/empty” values, like having “change me” as session secret. This would be covered by the next point.

Configuration requirements and schema

The configurations requirements would be the minimal configuration that should be done for an app or library.
For example, if I add a redis adapter library to my app and do no configuration, I should get a message like:
Configuration error: the key redis.hostname has no value.

This should come for free after defining a schema, like:

schema(:redis) do
    key :hostname, :string, required: true
end

The idea of the schema is to do correct type conversion (if I put a port number in an env var, I want an int in my app, not a string), handle missing/empty values.

The general idea is to avoid missing and even dangerous configuration and help debug configuration errors, I know it’s erlang “policy” to crash is something is wrong, but sometimes it can be really time consuming to pinpoint a missing configuration in production because there is some typo in an env var or that you forgot to commit some change. I also encountered super hard to pinpoint errors because some config was “FOO=false” and it was not converted to a boolean, evaluating to true… you get the idea.

It would also avoid replicating configuration validation code and type conversion across all possible libraries. The idea is that a library (or an app) could “trust” the configuration it is given.

3 Likes

I believe a lot of the confusion comes from being unclear on what should be configured via config/config.exs. Some of this is due to the ecosystem being new and clearer guidelines will come as we go.

For example, no configuration library that will solve the compile-time issues we have today. The solution is for libraries to rely less on compile-time configuration and document when it happens. We have also tried to make {:system, env} work but, at this point, it is clear that runtime configuration should be moved to runtime. It doesn’t work in Elixir nor did it work on Erlang. Phoenix v1.3 and Ecto v2.1 are pushing to this new direction.

Hopefully drawing a line on what works with Mix config will allow others to work on a unified approach for configuration that could support multiple sources (system env, database, json files, etc). Looking at what other communities do to tackle this can be helpful. However I would also be careful with putting all responsibilities on the config system. For example, a schema for configuration could be useful, but libraries should also be validating whatever they get from external sources.

2 Likes

I’ve said it elsewhere, but I want to repeat again, that I believe that libs should in most cases not be prescriptive about configuration. This tweet mostly mirrors my way of thinking:

I wouldn’t be as harsh and say that there are no such scenarios, but I do feel that in most typical cases libs should just take their options at runtime, either through function parameters, or through module callbacks. I believe that this will simplify many deployment/config challenges

Let’s see how runtime configuration would address your original concerns:

Confusion as config being compile time, including reading the environment

If a library takes options at runtime, then you need to pass the value at runtime, so there’s no confusion.

{system, “MYENV”} being somewhat present but considered bad practice by @josevalim

If a library takes options at runtime, then we don’t need {:system, ...} or any similar improvisation.

Difficulties around secret handling

If a library takes options at runtime, it’s up to developer to read the secret at runtime from an arbitrary safe place.

No standard way of defining config requirements/schema for libraries

If a library takes options at runtime, then requirements can be listed as mandatory parameters, while schema can be specified with typespecs.

Therefore I believe that the best solution is to educate lib authors to seriously consider whether their libraries really need to be configured through config.exs.

5 Likes

I’m fine with libs not being configured via config.exs, but then, how do you configure a library that is an application being under the supervisor? Such app would get start_link automatically and it might require configuration at this point.

1 Like

I believe in most cases the same ideas hold.

Preferably, an OTP app would start only the minimum part of its subtree, usually some “singleton” (aka locally registered) processes, while everything else should be started on demand, and receive options at the latest possible moment.

A good examples of this are phoenix and ecto. Both require us to provide options through app env (which I dislike). However, they will only read those options when we start the endpoint or the repo (which I like). Thus, for the most part both apps could in fact accept options at runtime, although they are OTP apps with a supervision tree.

There will likely always be some cases where configuration is indeed required at the startup. The best example I can think of is logger. However, I’m pretty certain that in the vast majority of cases libs can defer taking their options to the latest possible moment.

1 Like

A big issue I have with configs in projects is that they tend to be very hardcoded, such as they use a build-time only config.exs setup that does not support environment variables, or they only check environment variables when it is going to be hosted on a system that cannot set them, etc… They are highly inflexible…

1 Like

I am all for pushing lib authors towards function and argument for configuration. But I think this should be official best practice somehow documented. As you said, edge cases will be handled by lib authors as required. If @josevalim agrees and wants to help standardize this.

This leaves us with app configuration, some centralized config repository an app uses to configure it’s dependencies and itself.

2 Likes

That’d be ideal - together with lazy application starting. However, such a system maybe throws out the baby with the bathwater? There’s tons of stuff out there I’ll just use and if they need sys.config, so be it.

FWIW, I settled on Distillery and a single build (I call :deploy) - everything uses the REPLACE_OS_VARS functionality and thus is configured through injection of environment variables. It works, it’s “12fa-compliant” (so will easily integrate in a range of deployment tooling), and is relatively clean and easy (scant details in http://evrl.com/elixir/deployment/2017/04/04/packaging-elixir.html)

To be clear, I wasn’t suggesting avoiding libraries just because they’re taking options through app env :slight_smile: At my work, we certainly use many such libs, and then do some trickery to vary the settings at runtime.

However, my understanding is that this thread is an attempt to summarize config issues and discuss potential solutions:

Given that direction of the discussion, I believe that many (though admittedly not all) issues would be resolved if libraries accepted options as parameters. So I think that rather than trying to invent some complex schemes in e.g. mix, a better approach would be to educate library authors to avoid requiring app env where it’s not needed. Therefore, I agree with this conclusion:

The goal of this thread is not to throw anything out of deprecate config.exs, but to list issues and find solutions.

The first thing that seems to come out is that we should encourage lib author to take configuration via arguments when possible. If this is accepted by @josevalim, a bit of documentation and “word spreading” would be enough for this one.

On the other hand, the app configuration (conform, mix config…) needs to be addressed in some way.

Having libraries that are inflexible in their configuration because they have an opinion on what the config.exs looks like can indeed really be a deal killer. I ran into this recently in an Elixir project and it was one of those “oh, no…” moments . … and as people have noted already, different projects take different approaches, meaning the ability to learn A Way™ and repeatedly apply that to Elixir projects is not really so possible right now. Having a clearly stated, documented, and followed by the “core”/“popular” libraries/tools would be absolutely fantastic. 1000 upvotes for that suggestion.

That said, it would really be nice imho to have a standardized schema mechanic as OP suggested. Libraries may not necessarily use that schema directly, but it could be used by application developers to easily map to/from storage solutions (be they POT files in the file system, records in a key/value store, rows in a tennantized SQL database, …) and give some sense as to what the options are (allowed values, etc) in a way that could be turned into automated tests at runtime.

It would also allow some level of decoupling between the actual MFA calls to configure a library and the configuration, which is ultimately data, not an API, and the configuration management.

With some sort of schema mechanism in place, then application-side (or node orchestration level) tools could evolve independently of library devel (targetting different use cases / environments, even) using the schemas as their generic integration point rather than “random” MFAs in libraries. This would be far more realistic a target imho than semi-de-facto-standardized-as-best-practice-ideology. It would even provide a(n optional) path for library developers to have “best practice” configuration functions generated at compile-time for them.

If these schemas were similar to ecto schemas, allowing for knowledge transfer / consistency / even some potential interoperability, that would be icing on the cake to me …

tl;dr from the perspective of an application developer (and knowing the pain points our ops team faces), I would love to see a configuration schema DSL for library developers to provide for application developers and deployment management.

Wasn’t expecting this. However, having libraries that configure themselves through an application environment is a given. I wonder if we, as a community, have enough weight to add another given (configure through functions and options - which in itself is an excellent idea). Or should we make the given a nice, first-class citizen? (at least nicer than it is now, where probably the biggest single issue is application startup order where your “library” applications are started before you have a chance to setup their environments)

Yes, exactly. Rely less on compile-time configs and more on arguments. The application environment is global configuration and therefore it can be prone to conflicts.

Maybe I was not clear on my first reply but that’s exactly the direction Ecto and Phoenix are moving. We will still use the application environment by default for convenience but we are moving as much as we can to runtime and consequently also allow those to be given as options.

3 Likes

I think if we have a constructive discussion we should be able to steer in a direction. This particular issue seems important enough for an “official” statement. The library ecosystem is what makes a language great. If we can have some standard it will help the language and its users.

1 Like