Stackable is a drop-in replacement for Django's settings settings.py
that is ideally suited for 12factor apps:
- environment-specific configuration settings
- modular configuration settings
- object oriented with subclassing
- configuration settings are stackable
- secret data handling (keys, passwords)
Using this approache gives us several benefits from the traditional Django settings
,
in particular the complexity becomes manageable.
StackableSettings uses Python standard classes and multi-inheritance to compose settings. Example:
# config.py
EnvSettings_GLOBAL(object):
# standard apps
INSTALLED_APPS=(
...
)
SOME_SETTING='foo'
Config_SomeApp(object):
APP_CONFIG='baz'
EnvSettings_Local(EnvSettings_GLOBAL):
# additional settings for local testing
SOME_SETTING='bar'
APP_CONFIG='testing'
# load order is in reverse, that is first EnvSettings_GLOBAL,
# then Config_SomeApp. In other words, Config_SomeApp overrides
# any variable set by EnvSettings_GLOBAL.
EnvSettings_production(Config_SomeApp,
EnvSettings_GLOBAL):
SOME_SETTING='prod'
# settings.py
StackableSettings.load()
Running manage.py will load config.py (or a config module) and import the settings classes. Settings are loaded by loading a class and all its superclasses, applying all capitalized variables from the class to the settings.py's local() scope, which is the same as having capitalized variables in settings.py directly. Note that only CAPITALIZED variables are loaded.
The settings class to load is given by the DJANGO_CONFIGURATION env variable, and it defaults to EnvSettings_Local. You can find it in the app's ./config module. In ./deploy/common there are several configuration classes that can be used across deployable apps.
In the above example, SOME_SETTING will have the value 'bar' if DJANGO_CONFIGURATION is either not set or has the value 'EnvSettings_local'. It will have the value 'prod' if DJANGO_CONFIGURATION was set to 'EnvSettings_production'.
The way we load settings is by going through the list of classes in the class specified in $DJANGO_CONFIGURATION. So you have EnvSettings_collect that has a bunch of classes listet in it. What the settings loader does is take all the UPPER_CASE_VARIABLES and add it each to list of settings, whereby existing settings are overwritten by those loaded later. So if you have
class SomeEnv(object):
FOO_BAZ = ('xyz',)
class MyEnv(SomeEnv)
FOO_BAZ = ('abc',)
and load MyEnv, you'll end up with FOO_BAZ==tuple('abc'). Something like FOO_BAZ += 'abc' won't work as it would simply replace what was there before.
That's why we need to use a patch:
class MyEnv(SomeOther)
_addtl_foo_baz = ('abc',)
__patches__ = StackableSettings.patch(
StackableSettings.patch_list('FOO_BAZ', _addtl_foo_baz),
)
Which results in FOO_BAZ == tuple('xyz', 'abc'). There are several patch methods, see StackableSettings.
In order to not expose production keys to everyone on the team, StackableSettings
implements a simple keychain using environment variables or files. With this approach,
the settings classes only contain place holder values, which get replaced
at run-time by the real keys and secrets. The keys to be applied are retrieved
according to the DJANGO_CONFIGURATION
env variable so that each environment can
use different keys.
Keys are either stored in an encrypted environment variable or an encrypted file.
A source file by default lives in ~/.stackable/keys.yml
and is organized by
environment name. They keys.yml
file looks something like this:
# yml
EnvSettings_Local:
SOME_SETTING: SECRET
EnvSettings_Production:
SOME_SETTING: OTHER_SECRET
...
To create the encrypted environment variable:
$ python -m stackable.encryptkeys
Creating variables for stackable from /home/patrick/.stackable/keys.yml
Set the environment for stackable as follows:
---
export DJANGO_CONFIGURATION=EnvSettings_Local DJANGO_CONFIGURATION_TEST=EnvSettings_Local ENV_APIKEY_DECRYPT=ce77e82d-b0a2-4007-9380-d7fddd4bd420 EnvSettings_Local_KEYS=VydQzauHmTnqladba+oU16w=
To enable encrypted keys in your EnvSettings class:
from stackable.contrib.conf_api import Config_ApiKeys
class EnvSettings_XYZ(Config_ApiKeys,
...):
pass
Config_ApiKeys
triggers stackable to read encrypted keys from the environment.
See its definition for more details on available options.
TODO
- implement a Django command for this