christhekeele

christhekeele

TL;DR:

I’m planning on building a library, inspired by Vapor, to make configuring Elixir applications more straight-forward; in an approachable but highly flexible way that scales to non-trivial usecases, even supporting no-redeploy modification of runtime configuration values in distributed, hot-code-reloaded systems. Interested?

Summary

I know there are quite a few options out there, and a lot of recent developments in this space the last few years, but I’ve still found building a good runtime configuration story in Elixir for large 12-factor apps to be a bit of a pain.

The problem is partially permutative: over time, an application can grow to want multiple sources of runtime configuration; loaded differently at compile-time, boot-time, or runtime; differently in different build environments and targets; with support for different configuration file types; and with different approaches for overriding values during development and testing. This gets harder and harder to reason about without good developer tooling.

Add in the ability to modify these values at runtime, in distributed systems, that supports hot-code reloading, and the problem becomes nearly intractable. I’d like to tract it.

Backstory

I’ve been porting a personal Elixir solution from project to project over the last 5 years, starting from the excellent Vapor library, pretty much since the day it was released. As my pet approach has evolved, I’m pretty happy with it, but would love to polish it—and I’m tired of copy-pasting my own code again and again.

Some of this approach was stolen from my Ruby on Rails days, where I was equally dissatisfied with the situation in that ecosystem, and developed a similar personal non-open-sourced solution that worked well with Rails’ boot system and Ruby’s dynamacism, around which I built the Inquisitive gem for even more Ruby syntax-sugary ways of interacting with runtime configuration, first deployed in production applications around a decade ago.

Elixir, as a less-dynamic-than-Ruby, compiled language with a (historically driven by erlang release mechanisms) mostly build-time configuration story, has a harder-to-engineer story around runtime configuration. There’s been a lot of improvements to this in the first decade of Elixir, but there are still pain points.

I’m planning on codifying my Elixir approach to this problem in a package anyways for personal convenience, but I’m curious if there’s wider interest in the community, and would like to solicit ideas for a feature roadmap that might gel with what I’m building!

Synopsis

What I’m developing is essentially a declarative way to define your runtime configuration, and integrate it into your project at any point in your application’s development lifecycle.

Vapor’s example shows you how to throw it in to your Application.start/2; but I unerringly find myself wrapping that in complex conditionals and OTP conveniences as the development, deployment, and override configuration-sophistication needs of my projects increases; always re-evolving the implementation towards the same result.

I figured it might be beneficial to encode my approach as a library, and make it easy for folks other than myself to re-use. Here’s what I have in mind:

Core Features

These are aspects of this system I’ve actually built before, and would love to stop re-inventing:

  • Support multiple approaches to defining configuration and sources:

    • A straightforward config.exs-driven approach.
    • An inline-Supervisor-tree-driven approach (including, your main OTP Application supervisor, as the Vapor docs guide you towards).
    • Perhaps a module-driven approach DSL approach
  • Config env and target aware filters in configuration plans:

    To make it easier to describe a complicated permutation of sources for configuration values in different build environments and targets.

    For example: loading from .env.#{Config.config_env()}-type files, but never when deployed to :prod, where the app should rely exclusively on environment variables. Or, looking into a .gitignored .env.local configuration file for ultimate overrides, but never doing so outside MIX_ENV=dev or MIX_TARGET=local situation.

    Specifically, instead of repeating configuration in different config/#{Config.config_env()}.exs files, allowing a single source of truth entry in your main config.exs file, with filters attached (similar to your mix.exs deps() :env and :targets filters). This makes it much easier to reason about where your runtime configuration comes from in your build-time configuration.

    Or, describing a single list of configuration providers in your Application.start/2 callback, instead of incrementally building a list with many providers = if Config.config_env() == desired_env, do: modify_providers_for_this_permutation(providers) calls

  • A handful of trivial out-of-the-box mappers for common config coercions:

    Ex: modeling string-only env vars as booleans, ints, or floats at runtime.

  • A validation system for ensuring values are within required parameters:

    Ex: ensuring that your database pool size is always greater than 1 in production.

  • Very specific error messages when required configuration values are missing or cannot parse:

    Including a lineage of all config sources that attempted to provide a value.

  • A Mix task for ensuring configuration is loaded appropriately for other mix tasks:

    Necessary when your config loading is done externally to your Application.start call.

    For example, if your Ecto Repo uses the init/2 callback to configure itself dynamically at runtime, mix ecto... will not work without a little help in accessing config not loaded in your main application callback, if it is provided by libraries such as these.

  • A Mix task for easy introspection of the current configuration given the current env/target, including lineage of overrides from different configuration sources.

  • Logger output at Application startup about configuration values:

    To make it trivial to understand in your logs the way in which your application was configured at launch, including override lineage.

  • Secret-awareness to prevent sensitive things from being logged or displayed in Mix tasks.

Aspirational Features

Features for this system I’ve never implemented before, but believe I can build it to support, with enough motivation:

  • An extensible system of declaring configuration value parser Vapor “mapping” functions:

    • Working around restrictions in referencing anonymous functions in a config.exs, to support all usage modes.

      Generally by the time I find this need, I’ve moved configuration over into my Application.start/2 callback where they are already available, but an out-of-the-box solution that supports config.exs configuration as a first-class citizen must accommodate this.

  • Test helpers, to make overwriting runtime config during a test (and other mocking of configuration values) trivial without fully losing parallelization of said tests:

    Regardless of configuration value providence, like an environment variable that is hard to modify mid-test-suite, this would let you play nicely with the virtuous properties of ExUnit.

  • Per-process caching of commonly fetched config values in the process dictionary for hot paths and tight loops (since the initial library plan currently throws everything into :ets for retrieval at runtime each time it is referenced, and would only grow less performant with distributed-friendly alternative implementations of the configuration backend).

  • Swappable backends over :ets to extend the configuration value storage mechanism to more distributed-friendly environments, once I am convinced we can optimize this scenario for hot paths and updates. For example, any Ecto-supported adapter, or persistent_term for scenarios where configuration is rarely intended to be changed.

  • Sane support for changing runtime configuration values at runtime, even with distributed backends, so that it is viable to do so via a remote connection to a production system:

    • With a Pub-Sub system for configuration value consumers to be notified when this occurs.

    • And supervision tree helpers subscribed to that to make restarting when certain values change trivial, ex:

       [
         {Library.Configuration.Dependency, values: [:SECRET_KEY_BASE, :SECRET_SALT]},
         MyApp.Endpoint
       ] |> Supervisor.start_link(strategy: :rest_for_one, name: MyApp.WebSupervisor)
      

      or even

      [
        {
          Library.Configuration.Watcher,
          values: [:SECRET_KEY_BASE, :SECRET_SALT]},
          children: [MyApp.Endpoint] ,
          name: MyApp.WebSupervisor
        },
        Other.Things
      ] |> MyApp.Supervisor.start_link(strategy: :one_for_one)
      

      letting you literally connect to a running production application and rotate your secret keys, at runtime, with zero downtime outside of your Supervisors restarting things.

      Or more generally, modifying any configuration in a production system at runtime that you’ve decided to make runtime configuration, with OTP supervision tree resiliency guarantees about the consequences.

Call for feedback, criticism, and ideas

Does any of this excite you, or feel like it might solve a pain point in the projects you work on? Let me know!

Or, do you maintain a complicated and large 12-factor app, and this still seems over-engineered and unrealistically overblown—would you loathe working in a system configured this way?

Finally, this is all conceived from my own personal experience, needs, and observing those of others here on this forum. Do you have any other insights from your experience you think would be instructive during the initial development of such a library?

Thanks for reading! Let me know your thoughts!

Showing Posts 38 to 29

christhekeele

christhekeele OP

Right, I allow the same config/source specifications to be loaded at either compile time, boot time, or runtime as appropriate, so I am doing most of my stuff to support that at compile time.

fireproofsocks

fireproofsocks

cool – hope it’s helpful! I haven’t tried, but I think you could use the env!/3 function when dealing with compile-time config in config/config.exs to read system ENVs and take advantage of the type-casting. There’s no need to read/source .env files if you’re dealing with system ENVs that are already present. And yeah, the parser is pretty self-contained, so feel free to copy it. I’d considered publishing it as a stand-alone package, but I opted to avoid the deps. I hadn’t thought about it, but I don’t think there’s anything that would prevent one from using dotenvy to deal with compile-time config… even though the inspiration for it came from dealing with runtime config. :thinking:

christhekeele

christhekeele OP

I hadn’t found dotenvy before! I really like what you’re doing with it.

  • I think most of what I’m trying to build would subsume usage of that project, to support more sophisticated use-cases. I think what you’ve built is a very good middle ground though, including for many of my own projects! I’ll be linking to it in an “Alternatives” section for my project, at the very least, and will have to try it out on some of my things soon!
  • Sadly, I’m trying to accomplish a lot of things at compile-time, within config/config.exs, to get a unified story for non-dotenv sources and metaprogram certain config key access guarantees, so I don’t think I can leverage its source-loading tooling.
  • I do really like your dotenv file parser, though—it’s very well-specified and deliberate in a way that suits what I’m working on well. I may investigate swapping out the library I use to parse dotenvs in favor of its parser, if I can!
fireproofsocks

fireproofsocks

I’ll mention dotenvy here because it is relevant to the 12-factor app ideas and it attempts to address some of the pain points that can crop up with Elixir configuration. See also the related article.

christhekeele

christhekeele OP

I’ve been going back and forth between RuntimeConfig, and Plasma — in homage to Vapor, as another cool state of matter.

axelson

axelson

Scenic Core Team

Hi @christhekeele! I wanted to drop by and say that I’ve been eagerly following your progress on this library. I do think that this is touching a need for some applications and I’m curious to see what you come up with.

Once you’ve released a semi-stable version I’ll look into adding it to some of my side projects.

My only feedback at this time is that naming the application/library :runtime might confuse some users into thinking that a line like:

config :runtime, values: [
  SIGNING_SALT: :string,
  DATABASE_URL: :uri,
  DATABASE_POOL: {:integer, min: 1, max: 100}
]

Is something built directly into Elixir instead of a separate library. Whereas giving the application a more unique name wouldn’t cause that same sort of confusion.

christhekeele

christhekeele OP

This is true, what I’m trying to achieve is a third layer: true modifiable-at-runtime, distributed, post-compile post-boot configuration. That needs complicate things. I would like to subsume the complexity of the other two existing options along the way to offer a unified experience, though.

christhekeele

christhekeele OP

Trying to build something that supports validated modification of configuration values at runtime, well after boot-time, makes it clear how there are really 3 different contexts: configuration that is required at compile-time, configuration that is read at runtime, and the special case of runtime configuration that should be required for the application to boot successfully.

To support strongly-typed, validatable configuration values, that can be modified post-boot at runtime, I’ve been wanting to separate the boot-time special-case into its own discrete concept, so that I can perform validations of said configuration within the Runtime application’s boot cycle, to fail fast instead of waiting for said values to be read at a later point in runtime and only then be discovered to be invalid.

For example, I’ve had Elixir, Ruby, and Python applications where something like the equivalent of a ERROR_REPORTING_SYSTEM_CALLBACK_URL was mis-configured as an invalid url in certain environments. The applications booted happily and ran for days until trying to report an error, and only ate dirt when trying to use the mis-configured value at runtime (with the exception of the Elixir application, which recovered thanks to supervisors, but delayed discovery of the issue for weeks). Boot-time validation of that runtime configuration value would have saved me many times. When the error reporting url is mis-configured, you’re deeply screwed.

This is the sort of very specific issue I’m trying to provide tooling address, but I agree the juice is not always worth the squeeze for simpler applications. I do like the idea of providing a solution to the config/when-the-hell-am-i-loaded-or-modifiable.exs confusion along the way for even simple applications and newcomers by allowing consolidating into config/config.exs alone along the way, though.

christhekeele

christhekeele OP

I agree with this. I followed the discussion on the core mailing list and other forums while it was being developed, and I do think I have an intuition — they were trying to improve the compile-time-only issues of the time, to support boot-time configuration from ENV vars. Supporting runtime-modifiable configuration was way out of scope, and does belong in its own library. What upsets me is that trying develop such a library is immediately confusing by the name of runtime.exs instead of boot-time.exs, I wish I’d agitated more about the decision back then…

christhekeele

christhekeele OP

(I am not willing to describe to you how atrocious the Runtime.CompileTime module looks yet. :wink:)

Where Next? Top

Trending in RFCs Top

manuel-rubio
There was some time when I started thinking about giving a boost to Lambdapad, the initiative from @garretsmith in Erlang that I loved wa...
New
Agostinho1965
Hey everyone — I’m putting together a practical, code-first book on building production-ready business applications with Phoenix LiveView...
New
andreasronge
You set up environments, each with its own tools, its own data and its own limits, and programs get evaluated in them. The same program r...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews