Chamber is the auto-encrypting, extremely organizable, Heroku-loving, CLI-having, non-extra-repo-needing, non-Rails-specific-ing, CI-serving configuration management library.
We reviewed some other gems, and while each fit a specific need and each did some things well, none of them met all of the criteria that we felt we (and assumed others) needed.
Our Ten Commandments of Configuration Management
- Thou shalt be configurable, but use conventions so that configuration isn't necessary
- Thou shalt seemlessly work with Heroku or other deployment platforms, where custom settings must be stored in environment variables
- Thou shalt seemlessly work with Travis CI and other cloud CI platforms
- Thou shalt not force users to use arcane long_variable_names_just_to_keep_their_settings_organized
- Thou shalt not require users keep a separate repo or cloud share sync just to keep their secure settings updated
- Thou shalt not be bound to a single framework like Rails (it should be usable in plain Ruby projects)
- Thou shalt have an easy-to-use CLI for scripting
- Thou shalt easily integrate with Capistrano for deployments
- Thou shalt be well documented with full test coverage
- Thou shalt not have to worry about accidentally committing secure settings
Add this line to your application's Gemfile:
gem 'chamber'
And then execute:
$ bundle
Or install it yourself as:
$ gem install chamber
Once the gem is installed, you'll want to add it to your project. To do this, type:
chamber init
This creates a public/private keypair for you to use with your project. The
private key will be called .chamber.pem
. The public key will be called
.chamber.pem.pub
.
.chamber.pem
will be added to your gitignore file so that it is not
accidentally checked in. Keep this file safe since anyone who has it will be
able to decrypt any settings that Chamber encrypts for you.
Lastly, it will create a sample settings.yml
file for you which you should
modify as needed.
By default Chamber only needs a base path to look for settings files. From that path it will search for:
- The file
<basepath>/settings.yml
- A set of files ending in
.yml
in the<basepath>/settings
directory
Chamber.load basepath: '/path/to/my/application'
You do not have to do anything. Chamber will auto-configure itself to point to
the config
directory.
The YAML data will be loaded and you will have access to the settings
through the Chamber
class.
Example:
Given a settings.yml
file containing:
smtp:
server: "example.com"
username: "my_user"
password: "my_pass"
can be accessed as follows:
Chamber[:smtp][:server]
# => example.com
or via object notation syntax:
Chamber.env.smtp.server
# => example.com
Certain settings you will want to keep from prying eyes. Unlike other configuration management libraries, Chamber doesn't require you to keep those files separate. You can check everything into your repo.
Why is keeping your secure files separate a pain? Because you must keep those files in sync between all of your team members who are deploying the app. Either you have to use a separate private repo, or you have to use something like a Dropbox share. In either case, you'd then symlink the files from their locations into your application. What. A. Pain.
Chamber uses public/private encryption keys to seemlessly store any of your configuration values as encrypted text. The only file that needs to be synced once between developers is the private key. And even that file would only be needed by the users deploying the application. If you're deploying via CI, Github, etc, then technically no developer needs it.
After running chamber init
as described above, the hard work is done. From
here on out, Chamber makes working with secure settings almost an afterthought.
When you create your configuration YAML file (or add a new setting to an existing one), you can format your secure keys like so:
# settings.yml
_secure_my_secure_key_name: 'my secure value'
When Chamber sees this convention (_secure_
followed by the key name), it will
automatically look to either encrypt or decrypt the value using the
public/private keys you generated above into something like:
# settings.yml
_secure_my_secure_key_name: 8239f293r9283r9823r92hf9823hf9uehfksdhviwuehf923uhrehf9238
However you would still be able to access the value like so (assuming you had the private key in the application's root):
Chamber.env.my_secure_key_name
# => 'my secure value'
If deploying to a system which has all of your environment variables already set (eg Heroku), you're not going to use all of the values stored in the YAML files. Instead, you're going to want to pull certain values from environment variables.
Example:
Given a settings.yml
file containing:
smtp:
server: "example.com"
username: "my_user"
password: "my_pass"
If an environment variable is already set like so:
export SMTP_SERVER="myotherserverisapentium.com"
Then, when you ask Chamber to give you the SMTP server:
Chamber[:smtp][:server]
# => "myotherserverisapentium.com"
It will return not what is in the YAML file, but what is in the environment variable.
If you're deploying to Heroku, they won't let you upload custom config files. If you do not have your config files all stored in your repo, or some of your settings are encrypted, it becomes more difficult to gain access to that information on Heroku.
To solve this problem, Heroku allows you to set environment variables in your
application. Unfortunately this has the nasty side effect of being a pain to
deal with. For one, you have to deal with environment variables with unweildy
names (eg MY_THIRD_PARTY_SERVICE_DEV_API_KEY
). For another, it makes the
organization of those variables difficult.
Fortunately, Chamber allows you to organize your environment variables in separate files and access them easily using hash or object notation, however at the same time, it provides a convenient way to push all of those sensitive configuration settings up to Heroku as environment variables.
When Chamber accesses those same hash/object notated config values, it will first look to see if an associated environment variable exists. If it does, it will use that in place of any values inside of the config files as described above.
To update Heroku with all the proper environment variables so that your app works as expected, run the following from the root of your app:
chamber heroku push
And all of your settings will be converted to environment variable versions and set on your Heroku app.
Note: For the full set of options, see The chamber Command Line App below.
When deploying to Travis CI, it has similar environment variable requirements as Heroku, however Travis allows the encryption of environment variables before they are stored in the .travis.yml file. This allows for that file to be checked into git without worrying about prying eyes figuring out your secret information.
To execute this, simply run:
chamber travis secure
This will add secure
entries into your .travis.yml
file. Each one will
contain one environment variable.
Warning: Each time you execute this command it will delete all secure
entries under 'env.global' in your .travis.yml
file.
Note: For the full set of options, see The chamber Command Line App below.
Using convention over configuration, Chamber handles the 90% case by default, however there may be times at which you would like to explicitly specify which settings files are loaded. In these cases, Chamber has you covered:
Chamber.load files: [
'/path/to/my/application/chamber/credentials.yml',
'/path/to/my/application/application*.yml',
'/path/to/my/application/chamber/*.yml',
]
In this case, Chamber will load only the credentials.yml
file without ever
looking for a namespaced file. Then it will load application.yml
and any
associated namespaced files. Finally it will load all *.yml files in the
chamber
directory except credentials.yml
because it has previously been
loaded.
When using object notation, all settings have ?
and _
predicate methods
defined on them. They work like so:
Chamber.env.my_setting # => nil
Chamber.env.my_setting? # => false
Chamber.env.my_other_setting # => false
Chamber.env.my_other_setting? # => false
Chamber.env.another_setting # => 'my value'
Chamber.env.another_setting? # => true
Chamber.env.empty? # => true
Chamber.env.my_setting_group_.my_setting? # => false
The ?
method will return false if a key has been set to false
or nil
. In
order to check if a key has been set at all, use the key?('some_key')
method
instead.
Notice the difference:
Chamber.env.my_setting # => false
Chamber.env.my_setting? # => false
Chamber.env.key?('my_setting') # => true
Chamber.env.key?('my_non_existent_key') # => false
One of the nice things about Chamber is that it runs each settings file through ERB before it tries to parse it as YAML. The main benefit of this is that you can use settings from previous files in ERB for later files.
Example:
# settings.yml
production:
my_secret_key: 123456789
<%# settings/some_service-production.yml %>
my_service_url: http://my_username:<%= Chamber[:my_secret_key] %>@my-url.com
Because by default Chamber processes settings*.yml
settings files before
anything in the settings
subdirectory, this works.
But it's all ERB so you can do as much crazy ERB stuff in your settings files as you'd like:
<%# settings.yml %>
<% %w{development test production}.each do |environment| %>
<%= environment %>:
hostname_with_subdomain: <%= environment %>.example.com:3000
<% end %>
Would result in the following settings being set:
development:
hostname_with_subdomain: development.example.com:3000
test:
hostname_with_subdomain: test.example.com:3000
production:
hostname_with_subdomain: production.example.com:3000
If, when running your app, you would like to have certain files loaded only under specific circumstances, you can use Chamber's namespaces.
Example:
Chamber.load( :basepath => Rails.root.join('config'),
:namespaces => {
:environment => ::Rails.env } )
For this class, it will not only try and load the file config/settings.yml
,
it will also try and load the file config/settings-<environment>.yml
where <environment>
is whatever Rails environment you happen to be running.
If having a file per namespace value isn't your thing, you can inline your
namespaces. Taking the example from above, rather than having settings.yml
,
settings-development.yml
, settings-test.yml
, settings-staging.yml
and
settings-production.yml
, you could do something like this:
# settings.yml
development:
smtp:
username: my_development_username
password: my_development_password`
test:
smtp:
username: my_test_username
password: my_test_password`
staging:
smtp:
username: my_staging_username
password: my_staging_password`
production:
smtp:
username: my_production_username
password: my_production_password`
You can even mix and match.
# settings.yml
development:
smtp:
username: my_development_username
password: my_development_password`
test:
smtp:
username: my_test_username
password: my_test_password`
staging:
smtp:
username: my_staging_username
password: my_staging_password`
# settings-production.yml
smtp:
username: my_production_username
password: my_production_password`
The above will yield the same results, but allows you to keep the production values in a separate file which can be secured separately. Although I would recommend keeping everything together and just encrpyting your sensitive info
If you would like to have items shared among namespaces, you can easily use YAML's built-in merge functionality to do that for you:
# settings.yml
default: &default
smtp:
headers:
X-MYAPP-NAME: My Application Name
X-MYAPP-STUFF: Other Stuff
development:
<<: *default
smtp:
username: my_development_username
password: my_development_password`
test:
<<: *default
smtp:
username: my_test_username
password: my_test_password`
staging:
<<: *default
smtp:
username: my_staging_username
password: my_staging_password`
Multiple namespaces can be defined by passing multiple items to the loader:
Chamber.load( :basepath => Rails.root.join('config'),
:namespaces => {
:environment => ::Rails.env,
:hostname => ENV['HOST'] } )
When accessed within the test
environment on a system named tumbleweed
, it
will load the following files in the following order:
settings.yml
settings-test.yml
settings-tumbleweed.yml
If a file does not exist, it is skipped.
Similarly named settings in later files can override settings defined in earlier files.
If settings.yml
contains a value:
smtp:
server: "generalserver.com"
And then settings-test.yml
contains this:
smtp:
server: "testserver.com"
The when you access the value with Chamber[:smtp][:server]
you will receive
testserver.com
.
Chamber makes it dead simple to output your environment settings in a variety of formats.
The simplest is:
Chamber.to_s
# => MY_SETTING="my value" MY_OTHER_SETTING="my other value"
But you can pass other options to customize the string:
pair_separator
value_surrounder
name_value_separator
Chamber.to_s pair_separator: "\n",
value_surrounder: "'",
name_value_separator: ': '
# => MY_SETTING: 'my value'
# => MY_OTHER_SETTING: 'my other value'
Chamber provides a flexible binary that you can use to make working with your configurations easier. Let's take a look:
Each of the commands described below takes a few common options.
-
--preset
(or-p
): Allows you to quickly set the basepath, files and/or namespaces for a given situation (eg working with a Rails app).Example:
--preset=rails
-
--rootpath
(or-r
): Allows you to quickly set the rootpath of the application. By default this is the directory that thechamber
executable is run from.Example:
--rootpath=/path/to/my/application
-
--basepath
(or-b
): Sets the base path that Chamber will use to look for its common settings files.Example:
--basepath=/path/to/my/application
-
--files
(or-f
): Allows you to specifically set the file patterns that Chamber should look at in determining where to load settings information from.Example:
--files=/path/to/my/application/secret.yml /path/to/my/application/settings/*.yml
-
--namespaces
(or-n
): The namespace values which will be considered for loading settings files.Example:
--namespaces=development tumbleweed
-
--encryption-key
: The path to the key which will be used for encryption. This is optional unless you need to secure any settings.Additionally you may pass in the actual contents of the key for this option.
Example:
--keypair=/path/to/my/app/my_project_rsa.pub
-
--decryption-key
: The path to the key which will be used for decryption. This is optional unless you need to decrypt any settings.Additionally you may pass in the actual contents of the key for this option.
Example:
--keypair=/path/to/my/app/my_project_rsa
Note: --basepath
and --files
are mutually exclusive. --files
will
always take precedence.
Note: Only select commands support the following options. Use chamber help SUBCOMMAND
to verify if a particular command does.
-
--dry-run
(or-d
): The command will not actually execute, but will show you a summary of what would have happened.Example:
--dry-run
-
--only-secured
(or-o
): This is the default. Because most systems have no issues reading from the config files you have stored in your repo, there is no need to process all of your settings. So by default, Chamber will only convert, push, etc those settings which have been gitignored or those which have been encrpyted.To process everything, use the
--skip-secure-only
flag.Example:
--secure-only
,--skip-secure-only
Gives users an easy way of looking at all of the settings that Chamber knows about for a given context. It will be output as a hash of hashes by default.
-
--as-env
: Instead of outputting the settings as a hash of hashes, convert the settings into environment variable-compatible versions.Example:
--as-env
Example: chamber show --as-env
Very useful for troubleshooting, this will output all of the files that Chamber considers relevant based on the given options passed.
Additionally, the order is significant. Chamber will load settings from the top down so any duplicate items in subsequent entries will override items from previous ones.
Example: chamber files
Will verify that any items which are marked as secure (eg _secure_my_setting
)
have secure values. If it appears that one does not, the user will be prompted
as to whether or not they would like to encrpyt it.
This command differs from other tasks in that it will process all files that match Chamber's conventions and not just those which match the passed in namespaces.
Example: chamber secure
Will display a diff of the settings for one set of namespaces vs the settings for a second set of namespaces.
This is extremely handy if, for example, you would like to see whether the settings you're using for development match up with the settings you're using for production, or if you're setting all of the same settings for any two environments.
-
--keys-only
: This is the default. When performing a comparison, only the keys will be considered since values between namespaces will often (and should often) be different.Example:
--keys-only
,--no-keys-only
-
--first
: This is an array of the first set of namespace settings that you would like to compare from. You can list one or more.Example:
--first=development
,--first=development my_host_name
-
--second
: This is an array of the second set of namespace settings that you would like to compare against that specified by--first
. You can list one or more.Example:
--second=staging
,--second=staging my_host_name
Example: chamber compare --first=development --second=staging
Init can be used to initialize a new application/project with everything that Chamber needs in order to run properly. This includes:
- Creating a public/private keypair
- Setting the proper permissions on the the newly created keypair
- Adding the private key to the gitignore file
- Creating a template
settings.yml
file
Example: chamber init
As we described above, working with Heroku environment variables is tedious at best. Chamber gives you a few ways to help with that.
-
--app
(or-a
): Heroku application name for which you would like to affect its environment variables.Example:
--app=my-heroku-app-name
As we described above, this command will take your current settings and push them to Heroku as environment variables that Chamber will be able to understand.
Example: chamber heroku push --namespaces=production --app=my-heroku-app
Note: To see exactly how Chamber sees your settings as environment variables, see the chamber settings show command above.
Will display the list of environment variables that you have set on your Heroku instance.
This is similar to just executing heroku config --shell
except that you can
specify the following option:
-
--into
: The file which the pulled settings will be copied into. This file will be overridden.Note: Eventually this will be parsed into YAML that Chamber can load straight away, but for now, it's basically just redirecting the output.
Example:
--into=/path/to/my/app/settings/heroku.config
Example: chamber heroku pull --app=my-heroku-app --into=/path/to/my/app/heroku.config
Will use git's diff function to display the difference between what Chamber
knows about locally and what Heroku currently has set. This is very handy for
knowing what changes may be made if chamber heroku push
is executed.
Example: chamber heroku diff --namespaces=production --app=my-heroku-app
Will remove any environment variables from Heroku that Chamber knows about. This is useful for clearing out Chamber-related settings without touching Heroku addon-specific items.
Example: chamber heroku clear --namespaces=production --app=my-heroku-app
Travis CI allows you to use the public key on your Travis repo to encrypt items as environment variables which you would like for Travis to be able to have access to, but which you wouldn't necessarily want to be in plain text inside of your repo.
This command takes the settings that Chamber knows about, encrypts them, and puts them inside of your .travis.yml at which point they can be safely committed.
Warning: This will delete all of your previous 'secure' entries under 'env.global' in your .travis.yml file.
Example: chamber travis secure --namespaces=continuous_integration
One of the things that is a huge pain when dealing with environment variables is that they can only be strings. Unfortunately this is kind of a problem for settings which you would like to use to set whether a specific item is enabled or disabled. Because this:
# settings.yml
my_feature:
enabled: false
if Chamber.env.my_feature.enabled?
# Do stuff with my feature
end
Now because environment variables are always strings, false
becomes 'false'
.
And because, as far as Ruby is concerned, any String
is true
, enabled?
would return true
. Now, you could completely omit the enabled
key, however
this causes issues if you would like to audit your settings (say for each
environment) to make sure they are all the same. Some will have the enabled
setting and some will not, which will give you false positives.
You could work around it by doing this:
if Chamber.env.my_feature.enabled == 'true'
# Do stuff with my feature
end
but that looks awful and isn't very idomatic.
To solve this problem, Chamber reviews all of your settings values and, if they are any of the following exact strings (case insensitive):
- 'false'
- 'f'
- 'no'
- 'true'
- 't'
- 'yes'
The value will be converted to the proper Boolean value. In which case the
above Chamber.env.my_feature.enabled?
will work as expected and your
environment audit will pass.
In any case that you need to set configuration options or do advanced post processing on your YAML data, you'll want to create your own object for accessing it. Don't worry, Chamber will take you 98% of the way there.
Just include it like so:
class Settings
extend Chamber
end
Now, rather than using Chamber[:application_host]
to access your
environment, you can simply use Settings[:application_host]
.
We recommend starting with a single settings.yml
file. Once this file begins
to become too unwieldy, you can begin to extract common options (let's say SMTP
settings) into another file (perhaps settings/smtp.yml
).
Let's walk through how you might use Chamber to configure your SMTP settings:
# config/settings.yml
stuff:
not: "Not Related to SMTP"
# config/settings/smtp.yml
default: &shared
smtp:
headers:
X-MYAPP-NAME: My Application Name
X-MYAPP-STUFF: Other Stuff
development:
<<: *shared
smtp:
username: my_dev_user
password: my_dev_password
staging:
<<: *shared
smtp:
_secure_username: my_staging_user
_secure_password: my_staging_password
production:
<<: *shared
smtp:
_secure_username: my_production_user
_secure_password: my_production_password
Now, assuming you're running in staging, you can access both username
and
headers
off of smtp
like so:
Chamber[:smtp][:headers]
# => { X-MYAPP-NAME: 'My Application Name', X-MYAPP-STUFF: 'Other Stuff' }
Chamber[:smtp][:username]
# => my_staging_username
Chamber[:smtp][:password]
# => my_staging_password
Special thanks to all those gem authors above @binarylogic, @bendyworks, @laserlemon and @bkeepers. They gave us the inspiration to write this gem and we would have made a lot more mistakes without them paving the way. Thanks all!
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request