/flasked

Injecting ENV vars into application configuration at runtime (12factor app for Elixir)

Primary LanguageElixirMIT LicenseMIT

Flasked

flasked - something (an elixir) bottled into a flask

Flasked injects application environment configuration at runtime based on given ENV variables and a mapping.

This is pretty useful for applications following the 12 factor app principle stated by Heroku.

For the specific details see http://12factor.net/config.

Also in container environments like Docker this comes in quite handy when you're literally forced to use ENV vars for configuration of your specific container at startup time.

# provide ENV var
MY_FLASKED_APP_FOO=this_is_foo iex -S mix
# use the ENV var value in the app
iex(1)> Application.get_env(:my_flasked_app, :foo)
"this_is_foo"

Rationale

OTP apps/releases are normally preconfigured and do not provide the tooling for runtime configuration out of the box. At least it is quite difficult and involves a lot of customization of every app or release.

The Phoenix framework only provides to set the PORT via ENV variable, the rest still has to be hard-coded.

Fiddling with the release configuration to enable more variables to be set at runtime is quite cumbersome and not well documented.

On the other hand, especially with containerization in mind, most of the environment specific configuration should be done at runtime, meaning you should not hard-code any environment/stage/role based configuration.

I agree with separation of code and configuration, because configs are state, and the application artifacts should be stateless.

There are similar projects like Dotenv, Envy and ExConf, but they don't go far enough for what I wanted. Most of them tackle only a very low-level need or are useful in development only.

Why no YAML or JSON for the mapping?

I'll just quote José Valim here:

Please avoid YAML in Elixir projects. It is unnecessarily complex, both in implementation and usage. Elixir already provides config files.

https://twitter.com/josevalim/status/626131275150065665

And Code.eval_file("some/file.exs") does the job just fine, really.

Installation

# mix.exs
def deps do
  [{:flasked, "~> 0.4"}]
end

Usage

Add flasked to applications

Make sure Flasked is in the applications list, preferably the very first one. (If it is the first, you can even reconfigure the logger before startup.)

def application do
  [
    applications: [:flasked],
    # ...
  ]
end

Standalone one-shot usage

# If you want to control it yourself or
# need to repeatedly call it in your application:
Flasked.bottle_it

Add mapping

Furthermore you need to set up the mapping file:

# priv/flasked_env.exs
%{
  my_flasked_app: %{
    foo: {:flasked, :MY_FLASKED_APP_FOO},
    bar: {:flasked, :MY_FLASKED_APP_BAR, :integer},
    baz: [something: [nested: {:flasked, :MY_FLASKED_APP_BAZ, :dict}]]
  }
}

So the placeholder has to be always a tuple in the following form:

{:flasked, :MY_FLASKED_APP_ENV_VAR_NAME} # defaults to string type
{:flasked, :MY_FLASKED_APP_ENV_VAR_NAME, :a_valid_type_atom}

Update config

Extend/modify your config/config.exs:

config :flasked,
  otp_app: :my_flasked_app,
  map_file: "priv/flasked_env.exs" # must match with real file relative to the app's root directory

Run

Test and run your application:

MY_FLASKED_APP_FOO=some_foo_val \
MY_FLASKED_APP_BAR=42 \
MY_FLASKED_APP_BAZ=key:val,info=values_will_be_strings \
iex -S mix

In the console:

Application.get_env(:my_flasked_app, :foo)
#=> "some_foo_val"
Application.get_env(:my_flasked_app, :bar)
#=> 42
Application.get_env(:my_flasked_app, :baz)
#=> [something: [nested: [key: "val", info: "values_will_be_strings"]]]

The mapping file

This file is just a normal Elixir script file with a map as its content:

# priv/flasked_env.exs
%{
  my_flasked_app: %{
    key: {:flasked, :MY_FLASKED_APP_KEY},
    another: {:flasked, :MY_FLASKED_APP_ANOTHER, :boolean},
    port: {:flasked, :MY_FLASKED_APP_PORT, :integer, 4000},
  },

  another_app: [can: "be a dict, too"],

  logger: %{
    furthermore: "they do not need to have ENV placeholders",
    but: "you can offload everything from config/*.exs here, if you really like to"
  }
}

The application will fail to start if a placeholder was specified but the ENV var could not be found, unless you specify a default value.

About defaults and development

Use defaults rarely and wisely. Be more explicit, even in development mode.

To avoid manually providing every single env var for development, see wrapper_example/ for a Makefile based setup. (Just copy env.mk and Makefile into your project, adjust values and run make console).

ENV Var placeholders

{:flasked, :MY_FLASKED_APP_KEY} # shortcut for string type without a default as fallback
{:flasked, :MY_FLASKED_APP_KEY, :boolean} # value type specified, no default given
{:flasked, :MY_FLASKED_APP_KEY, :integer, 1234} # value type and default specified

So the tuple must always have :flasked as first element, an atom matching the ENV var you want to read, and optionally a type and a default. If you want to give a default you always need to specify the type, even for strings.

Supported types

MY_FLASKED_APP_VAR=a_string_val
  {:flasked, :MY_FLASKED_APP_VAR}
  => "a_string_val"

MY_FLASKED_APP_VAR=true
  {:flasked, :MY_FLASKED_APP_VAR, :boolean}
  => true
  - valid values: TRUE, true, FALSE, false
  - any other value will always default to `false`

MY_FLASKED_APP_VAR=9
  {:flasked, :MY_FLASKED_APP_VAR, :integer}
  => 9

MY_FLASKED_APP_VAR=3.1415
  {:flasked, :MY_FLASKED_APP_VAR, :float}
  => 3.1415

MY_FLASKED_APP_VAR=list,of,strings
  {:flasked, :MY_FLASKED_APP_VAR, :list}
  => ["list", "of", "strings"]

MY_FLASKED_APP_VAR=list,of,atoms
  {:flasked, :MY_FLASKED_APP_VAR, :list_of_atoms}
  => [:list, :of, :atoms]

MY_FLASKED_APP_VAR=1,2,3
  {:flasked, :MY_FLASKED_APP_VAR, :list_of_integers}
  => [1, 2, 3]

MY_FLASKED_APP_VAR=4.44,5.555,6.789
  {:flasked, :MY_FLASKED_APP_VAR, :list_of_floats}
  => [4.44, 5.555, 6.789]

MY_FLASKED_APP_VAR=this:is,a:dictionary
  {:flasked, :MY_FLASKED_APP_VAR, :dict}
  => [this: "is", a: "dictionary"]

MY_FLASKED_APP_VAR=one:1,two:2,three:3
  {:flasked, :MY_FLASKED_APP_VAR, :dict_of_integers}
  => [one: 1, two: 2, three: 3]

More sophisticated types could be supported. You're welcome to contribute.

Enjoy!