Introducing Weave for Elixir

Categories: Open Source
Tags: Elixir

What is Weave?

Weave is a library for projects written in Elixir that need Just-in-Time (JIT) configuration. This is particularly important if you’re producing an artifact (Example: Docker Image) that will be deployed to multiple environments; where configuration is different for each.

Side-note: If you’ve not before, you should read the 12-Factor Manifesto

Why Weave?

Elixir already provides a means to access environment variables, this is true. However, if you put System.get_env/1 inside your Mix configuration files, you’ll not get what you expected.

Elixir runs on BEAM, the Erlang virtual-machine. This means your code is compiled to byte-code when you build it. BEAM has no idea what Elixir is and it certainly has no idea what Mix is. So what happens to your configuration files? They’re translated, just as your code is compiled. Lets take a look at what I mean.

Steps:

  1. mix new example_app
  2. Add distillery to build a release
  3. Add System.get_env/1 to config.exs
  4. mix release
  5. cat _build/dev/rel/example_app/releases/0.0.1/sys.config
# config.exs
use Mix.Config
config :example, config: System.get_env("MY_CONFIG_KEY")
$ cat _build/dev/rel/example_app/releases/0.0.1/sys.config
# sys.config
[{sasl,[{errlog_type,error}]},
 {example,[{config,nil}]},

Our configuration is handled during compilation, and not when the code is running in your environment. This is only a problem with Mix configuration, not your applications code. This is because Elixir compiles your application’s configuration for it to run as an OTP application on BEAM.

So, we need a way to defer configuration of our application until runtime; cue Weave.

Installing

Just like any other dependency in Elixir, we add :weave to our deps() call:

{:weave, "~> 1.0.0"}

Handler

Weave requires that you provide a handler that knows what your runtime configuration should look like in an OTP/Elixri context. For that, you simply provide functions that pattern match against the configuration key and return a tuple with the application, key and value to set.

Example:

defmodule My.Handler do
	def apply_configuration("MY_KEY", value) do
		{:my_app, :my_key, value}
	end
end

Loaders

Weave comes with two loaders: File and Environment. Both were built for a very specific need.

File

This loader was written to allow us to load secrets. This is a common pattern, recently adopted by Docker Swarm, and previous to that: Kubernetes.

In-order to use this loader, we must first configure it:

config :weave, handler: My.Handler, file_directory: "/run/secrets" # Docker Swarm secrets directory

Environment

This loader was written for non-secret configuration, such as your database host, port, etc. This loader requires that you specify a prefix, so that we don’t try to handle every environment variable on your host.

config :weave, handler: My.Handler, environment_prefix: "MY_APP_"

This will allow you to pass environment variables, such as MY_APP_DATABASE_HOST=mysql.com to your application and you can provide a handler, such as:

def apply_configuration("DATABASE_HOST", host) do
	{:my_app, :database_host, host}
end

Loading Configuration

Once you’ve configured everything else, all that is left is to actually load the configuration at runtime. You’ll need to add the loaders to your def start() function, which is defined in your application callback module.

Weave.Loaders.File.load_configuration()
Weave.Loaders.Environment.load_configuration()

You can use one, or both; just keep in mind that this should happen before you start your supervisor tree.

What’s Next?

I would like to provide a nicer API for loading, perhaps allowing the loaders to be conifgured and a single Weave.load call within start.

I’m also hoping to add some “auto-wiring”, to save boilerplate on writing lots of apply_configuration/2 functions. At the moment I’m thinking that the value of our environment variables / contents of our secret files could simply be:

{:some_app, some_key: value}

Pattern matching against this value would allow us to provide a catch-all handler, that is hit before manual handlers.

I’m still pondering for now, but if you have an thoughts - join the conversation on our GitHub Issues.

I hope this library helps you move your Elixir application to your next environment,

Thanks, David

comments powered by Disqus