Config as YAML file yet still being 12-factor compliant...
...by serializing the file as one environment variable.
- Can be read without loading Rails
- Variable namespacing using nested hash
- Follows 12-factor rule 3 - store config in the environment
- Customizable due to loosely coupled PORO parts
Insert into Gemfile:
gem 'settei'
# gem 'dig_rb' # for Ruby < 2.3
And then execute:
$ bundle
For Rails, execute this rake task for out-of-the-box setup:
$ rake settei:install:rails
This task does the following things:
- create
config/setting.rb
for setting upSetting
- require the above in
config/application.rb
- create YAML files
config/environments/default.yml
andconfig/environments/production.yml
- make git ignore YAML files above
- append script to
deploy.rb
so config is passed via env var to production
If config/environments/default.yml
contains the following:
the_answer_to_life_the_universe_and_everything: 42
google:
api: foo
Then you can access those like this:
Setting.dig(:the_answer_to_life_the_universe_and_everything)
Setting.dig(:google, :api)
#dig
is used to access its values. It's convenient because it does not err if nested hash is absent.
#dig_and_wrap
will return a Settei::Base
if the return value is a hash.
For other available methods, check here.
If you have development.yml
or test.yml
, it will be loaded instead of default.yml
.
If you use Capistrano or Mina, the deploy.rb
is modified so deploy process will serialize production.yml
into one long string, and pass it to remote server as a single environment variable. There it is de-serialized and loaded, and the rest works the same way.
If you use Heroku, use rake settei:heroku:config:set app=[app_name]
to upload your config. You need heroku-cli and authenticate first.
Most config gems have not been updated for ages, and do not meet my needs:
I want to be 12-factor compliant, but I also hate using environment variables. See the following example: naming is hard and names tend to be very long. Passing more env vars also becomes more impractical.
BOARD_PAGINATION_PER_PAGE=5
BOARD_PAGINATION_MAX_PAGE=10
BOARD_REPLY_OMIT_CONDITION_N_RECENT_ONLY=5
BOARD_REPLY_OMIT_CONDITION_AVOID_ONLY_N_HIDDEN=2
In comparison YAML allows nested hashes, so we can manage them using namespaces.
board:
pagination:
per_page: 5
max_page: 10
reply_omit_condition:
n_recent_only: 5
avoid_only_n_hidden: 2
Can I have the benefit of env var (12-factor) and the benefit of YAML (ease of variable management) at the same time?
Yes, if settings are stored in YAML files, but during deploy, transfer the whole YAML file as one env var.
I feel it is simpler and more effective.
Rails secrets.yml
and credentials.yml.enc
are needlessly complex, and now we are able to ignore them:
Do away with Rails 4.1 secret.yml with something like this:
# secret_token.rb
Foo::Application.config.secret_token = Setting.dig(:rails, :secret_token)
Foo::Application.config.secret_key_base = Setting.dig(:rails, :secret_key_base)
Similarly with Rails 5.2's credentials:
# application.rb
config.secret_token = Setting.dig(:rails, :secret_token)
config.secret_key_base = Setting.dig(:rails, :secret_key_base)
Maybe we can get rid of database.yml
one day too.
The default setup is probably good enough for 90% of the users. However if you have advanced requirements, you can easily customize.
One can start by editing the generated setting.rb
file. The three parts are Settei::Base
, loader and deploy script:
Setting
is an instance of Settei::Base
, the accessor of the configurations. It is initialized by a hash.
You can change Setting
to other constants or a global variable.
You can also extend Settei::Base
, or replace it with other classes such as SettingsLogic.new(hash)
, or you can just use the hash without it.
Loaders are responsible for returning the configuration as hash, used to initialize Settei::Base
.
Settei::Loaders::SimpleLoader
is one type of loader. It loads from YAML or environment variable.
When initializing it, you can set:
dir
: the full path to directory containing YAML filesenv_name
: the environment variable name; defaults to APP_CONFIG
loader = Settei::Loaders::SimpleLoader.new(dir: 'path/to/dir')
To load data, call load(Rails.env)
. In development environment, it tries to load development.yml
if it exists, else it loads default.yml
.
Once data is loaded, we can obtain it in hash form by calling as_hash
.
The deploy script also relies on loader's ability to serialize the whole hash into one string, suitable for deploying as environment variable. The methods as_env_assignment
and as_env_value
are provided for this purpose, e.g.:
loader.load.as_hash # loads default.yml and returns a hash
loader.load(:production).as_env_value # loads production.yml and returns "XYZ"
loader.load(:test).as_env_assignment # loads test.yml and returns "APP_CONG=XYZ"
But no one is stopping you from writing your own loader. For example you might want the loader to encrypt/decrypt ENV value, or you may want to load from .env file.
For more detailed doc of SimpleLoader
, check here.
If you have more complex deploy requirements, just edit/revert the changes on deploy.rb
.
Settei is designed to be simple so you can integrate it into any frameworks easily. The steps are mainly:
- Designate a folder for storing YAML files.
- Create a
setting.rb
file, in whichSettei::Base
is initialized (seetemplates/setting.rb
). - Require it when framework starts.
- Load production.yml, pass its serialized form as environment variable to production (see
templates/_capistrano.rb
ortemplates/_mina.rb
).
Q: Would serialized configuration be too big for environment variable?
A: The upper limit is pretty big.
The slogan "YAML config yet still 12-factor compliant" is not entirely correct. Why not load from TOML or .env? If there is a need we can accommodate for that.
PRs are welcomed. Some ideas are:
- generators for other frameworks
- loader or its plugins
- plugin for
Settei::Base
- explore deep merge hash so development.yml can combine with default.yml
- make loader configurable so it is easy to add and mix functionality
- rake task for heroku setup