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"
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.
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.
And Code.eval_file("some/file.exs")
does the job just fine, really.
# mix.exs
def deps do
[{:flasked, "~> 0.4"}]
end
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
# If you want to control it yourself or
# need to repeatedly call it in your application:
Flasked.bottle_it
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}
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
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"]]]
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.
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
).
{: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.
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.