-
-
Notifications
You must be signed in to change notification settings - Fork 25
Problems with Environment Variables
Because environment variables can only be strings, even something as simple as a flag in your code means that you have to jump through conversion gymnastics every time it's used.
if ENV['MY_NEW_FEATURE_ENABLED'] == 'true'
# My feature stuff
end
Alternatively, we could check for truthiness:
if ENV['MY_NEW_FEATURE_ENABLED']
# My feature stuff
end
and that works great if it's true, but what if we want to set it to false
?
Since, in this case, the only way we can set an environment variable to false
is if it's not set, then how do we know:
- it was intentionally left blank to demonstrate
false
- it was intentionally left blank because it didn't need to be set
- it was unintentionally left blank due to an oversight
So either a) we're doing conversions on every check, or b) we have ambiguity on what an unset environment variable really means.
Chamber gives you YAML data types in your settings:
- Strings
- Integers
- Floats
- Booleans
- Arrays
- Hashes
- Dates
- Null
- Regexes
- Symbols
plus environment variable coercions for when you absolutely have to use an environment variable to override your YAML settings.
So the above example turns into:
if Chamber.dig!('my_feature', 'enabled')
# My feature stuff
end
Even when working with a small-sized application, being able to organize your settings is important. Take a look at the number of settings that I had in one of my applications after just a couple weeks. We've only just begun working on this app and already it's hard to look at.
Not only that, but keeping multiple versions of that list (one for each environment and/or deploy and/or host) may be "scalable" (as 12-factor states it), but it's not very manageable.
Chamber lets you organize your settings in multiple ways:
In this (the simplest) case, you can nest your settings as deep as you'd like so that different sections are easy to scan.
# settings.yml
smtp:
username: "my_username"
password: "my_password"
As your app grows, you may find the need to set settings differently based on things like environment or host. Chamber lets you easily do that by nesting your settings under the namespace in which it applies.
# settings.yml
development:
smtp:
username: my_dev_username
password: my_dev_password
test:
smtp:
username: my_test_username
password: my_test_password
staging:
smtp:
username: my_staging_username
password: my_staging_password
If you'd like to completely separate out a namespace into other files, you can do that by appending the namespace to your filename. This allows you to not only manage them separately, but also gitignore settings for an entire namespace.
# settings.yml
development:
smtp:
username: my_dev_username
password: my_dev_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_prod_username
password: my_prod_password
Lastly, Chamber lets you split out arbitrary settings into multiple files. Generally as your app grows, what you tend toward is a different YAML file per type of setting. For example, if our settings are getting rather lengthy, we can split out all of the SMTP settings into their own SMTP YAML files:
# settings/smtp.yml
development:
smtp:
username: my_dev_username
password: my_dev_password
test:
smtp:
username: my_test_username
password: my_test_password
staging:
smtp:
username: my_staging_username
password: my_staging_password
# settings/smtp-production.yml
smtp:
username: my_prod_username
password: my_prod_password
If only using environment variables, sharing common settings information between environments can be a pain.
When creating a new environment (or onboarding a new developer) you have to duplicate all settings from every environment. This is the "combinatorial explosion" that was mentioned in the 12-factor manifesto.
# .env-development
EBAY_AFFILIATE_ACCOUNT_ID="my affiliate account id"
EBAY_AFFILIATE_CAMPAIGN_ID="my affiliate campaign id"
EBAY_AFFILIATE_PARTNER_CODE="9"
EBAY_AFFILIATE_USER_ID="my affiliate user id"
EBAY_API_URL="https://api.sandbox.ebay.com/ws/api.dll"
EBAY_CATEGORY_CALLS_PER_HOUR="60"
EBAY_CERT_ID="my dev cert id"
EBAY_DEV_ID="my dev dev id"
EBAY_ENVIRONMENT="sandbox"
EBAY_LIMIT_API_CALLS="true"
EBAY_SITE_ID="0"
# .env-production
EBAY_AFFILIATE_ACCOUNT_ID="my affiliate account id"
EBAY_AFFILIATE_CAMPAIGN_ID="my affiliate campaign id"
EBAY_AFFILIATE_PARTNER_CODE="9"
EBAY_AFFILIATE_USER_ID="my affiliate user id"
EBAY_API_URL="https://api.ebay.com/ws/api.dll"
EBAY_CATEGORY_CALLS_PER_HOUR="720"
EBAY_CERT_ID="my prod cert id"
EBAY_DEV_ID="my prod dev id"
EBAY_ENVIRONMENT="production"
EBAY_LIMIT_API_CALLS="false"
EBAY_SITE_ID="0"
When using environment variables to store settings, you have some options which are different:
# .env-development
EBAY_API_URL="https://api.sandbox.ebay.com/ws/api.dll"
EBAY_CATEGORY_CALLS_PER_HOUR="60"
EBAY_CERT_ID="my dev cert id"
EBAY_DEV_ID="my dev dev id"
EBAY_ENVIRONMENT="sandbox"
EBAY_LIMIT_API_CALLS="true"
# .env-production
EBAY_API_URL="https://api.ebay.com/ws/api.dll"
EBAY_CATEGORY_CALLS_PER_HOUR="720"
EBAY_CERT_ID="my prod cert id"
EBAY_DEV_ID="my prod dev id"
EBAY_ENVIRONMENT="production"
EBAY_LIMIT_API_CALLS="false"
And you have other options, which are the same:
# .env-development
EBAY_AFFILIATE_ACCOUNT_ID="my affiliate account id"
EBAY_AFFILIATE_CAMPAIGN_ID="my affiliate campaign id"
EBAY_AFFILIATE_PARTNER_CODE="9"
EBAY_AFFILIATE_USER_ID="my affiliate user id"
EBAY_SITE_ID="0"
# .env-production
EBAY_AFFILIATE_ACCOUNT_ID="my affiliate account id"
EBAY_AFFILIATE_CAMPAIGN_ID="my affiliate campaign id"
EBAY_AFFILIATE_PARTNER_CODE="9"
EBAY_AFFILIATE_USER_ID="my affiliate user id"
EBAY_SITE_ID="0"
So if some of the options are the same, why aren't we just hard-coding them?
The EBAY_AFFILIATE_ACCOUNT_ID
is the same between development
and
production
, but it's still something I want to be secret.
And even if it's not secret, it doesn't mean that I'm not (or won't in the future) use it in more than one place.
And even if I'm only using it in one spot, I don't want to have multiple places that I have to look for settings for a given thing.
I don't want to have to look in (for example) a Rails initializer as well as my YAML files.
Because Chamber uses YAML, instead of this:
development:
smtp:
server: "smtp.sendgrid.net"
port: 587
tls: true
authentication: "login"
delivery_method: "smtp"
username: "my_dev_username"
password: "my_dev_password"
test:
smtp:
server: "smtp.sendgrid.net"
port: 587
tls: true
authentication: "login"
delivery_method: "smtp"
username: "my_test_username"
password: "my_test_password"
production:
smtp:
server: "smtp.sendgrid.net"
port: 587
tls: true
authentication: "login"
delivery_method: "smtp"
username: "my_prod_username"
password: "my_prod_password"
We would use YAML's anchors and references to do something like this:
defaults:
smtp: &defaults
server: "smtp.sendgrid.net"
port: 587
tls: true
authentication: "login"
delivery_method: "smtp"
development:
smtp:
<<: *defaults
username: "my_dev_username"
password: "my_dev_password"
test:
smtp:
<<: *defaults
delivery_method: "test"
username: "my_test_username"
password: "my_test_password"
production:
smtp:
<<: *defaults
username: "my_prod_username"
password: "my_prod_password"
Here, by utilizing YAML's anchors and references to use portions of settings in
multiple places. We're also able to set a default for delivery_method
and
just override it in the one environment where we need something different.
Notice how much easier it is to see the things that are similar (and different) between each environment, than if we were looking at simply a list of environment variables.
# .env-development
SMTP_SERVER="smtp.sendgrid.net"
SMTP_PORT="587"
SMTP_TLS="true"
SMTP_AUTHENTICATION="login"
SMTP_DELIVERY_METHOD="smtp"
SMTP_USERNAME="my_dev_username"
SMTP_PASSWORD="my_dev_password"
# .env-test
SMTP_SERVER="smtp.sendgrid.net"
SMTP_PORT="587"
SMTP_TLS="true"
SMTP_AUTHENTICATION="login"
SMTP_DELIVERY_METHOD="test"
SMTP_USERNAME="my_test_username"
SMTP_PASSWORD="my_test_password"
# .env-production
SMTP_SERVER="smtp.sendgrid.net"
SMTP_PORT="587"
SMTP_TLS="true"
SMTP_AUTHENTICATION="login"
SMTP_DELIVERY_METHOD="smtp"
SMTP_USERNAME="my_prod_username"
SMTP_PASSWORD="my_prod_password"
Whether it's from Jane to Joe, or Sam to Victor, or Gloria to Gayle, making sure settings stay in sync between the development team can be a pain. The more developers you have, the more people need to have the settings synced between them... talk about a "combinatorial explosion".
And lastly, what about the environment-specific files like .env-production
and
.env-staging
? If a developer makes a change to those, how do they get synced
to the rest of the team?
It's a hard problem. Generally one of three things happens:
Some teams are moving "too fast" or are small enough that they just deal with this pain.
Some teams set up a Dropbox or Google Drive shared folder and then symlink the settings files from that share into their project.
But neither of these solutions is designed with developers in mind. As well as the fact that if you're not into checking in your settings, how is this any different?
Some teams will choose to create a completely separate git repository. This repository will contain only the settings for their app. Then, like with Dropbox, files will be symlinked into the project directory.
This is a better solution, but is still a pain. And again, for those who don't ever want to check sensitive settings into the repository, even this won't work.
Chamber uses Public Key Cryptography to allow every setting to be committable to the repository. Some settings, which are not sensitive, can be committed as plain text. Other settings, which are, can be encrypted with a project-wide private key.
But what about decrypting? Do you have to decrypt every time you need to use a setting?
Absolutely not. Here are the steps:
Running:
chamber init
will generate a public/private key pair for your project (this only need to be done once).
Any settings you'd like to be encrypted need to have a _secure_
prefix in
front of their names.
defaults:
smtp: &defaults
server: "smtp.sendgrid.net"
port: 587
tls: true
authentication: "login"
delivery_method: "smtp"
# ...Snip...
production:
smtp:
<<: *defaults
_secure_username: "my_prod_username"
_secure_password: "my_prod_password"
Running chamber secure
will use your private key,
encrypt all your secure settings
and update your file to look something like this:
defaults:
smtp: &defaults
server: "smtp.sendgrid.net"
port: 587
tls: true
authentication: "login"
delivery_method: "smtp"
# ...Snip...
production:
smtp:
<<: *defaults
_secure_username: wVOqkGbZWfzgLINJlXtjg8H9k3q/SxbNQtgYqEy5uLp7FRbsM79MeIsoStEiv/ZqbpXZQCVmtvFAhlCAT77l1HpGTBFWd9WFIjhO0t0GoI+ewYTug21kOJI18I4zzZUPnsnm9Ml5vjRPleHsI745HhJ8lGHc3bHcGlIit34/KT6vmKdqksbMne6kUCHxt472S92OIhrkx6cY8uHj6Cs8Q3tb3JcfsVgjNAaBzpxaUw8MA8OEEk8vr6YUkgGT3k55GXzM58atwSvGxHTzbCnNJsfqY0Dqj4uUQo07OaIAYXbJ7jn43brhQEq30EoniZ2RFXjgHbQCUmsM+Rskh/l8LA==
_secure_password: reHGp1YKoAW7nD64szmKbDo2hLi69G4Upi9DlcYYNJ8BsztzooDFQfUfOWcWFw//3wQ2DbC3ff8ZE2M3Ym8W/K4dFxdC+YftNB5hQpwjlW+d2SPqL081MMyb4/wWJNefIQ2sN7Xj0Y7MQcgzJ9ctQv6qB93GbgzbdI3e/cKl2r4ON+JfvdTQ4KA9Ik2FRhpJuQk26OAr+2jr+4yTpNSAf+2y24TodRSO2TiPHoy6LJWEfAjpvvdl26R+uK7oqtrXjdlss1WqPfrmLyXpODicxhiO1LWOyESfD7V3ta1TS9PtcACbw1vI7zyofjDJwGZtAeoKoXRgj9qJfL7CcLR+SA==
But when you want to access your settings, all you have to
do is reference the key (without the _secure_
prefix) and it'll decrypt it for
you automatically.
Chamber.dig!('smtp.username')
# => "my_prod_username"
Even if you'd managed to solve the above problem without Chamber, you still have
another issue. When you change your development
settings and (for example)
added a setting. If this setting is required in production
, it's quite easy
for a developer to forget to set it.
This can cause production
to go down.
Chamber provides a way to be able to set one environment variable in each environment one time and each time the app is deployed, all the settings that existed on the developer's machine will exist on the environment. No syncing required.
But even with the above tools, you still need to remember to run it and that
really only needs to happen if a YAML file changes. And even then, you're
always running the same comparisons (eg comparing development
to test
to
production
).
Chamber provides a convenient solution in the form of a git pre-commit hook.
This hook will run chamber compare
against a configurable set of namespaces
any time a settings file is changed. If the settings don't match, the commit
will abort.
Additionally, since Chamber 3.0, any attempts to access to non-existent key will result in a runtime error making any missed misconfigurations easy to spot.
Because your settings aren't supposed to be checked in, the standard practice is
to have something like a .env-sample
which is checked in. This file
typically has development
settings as they are considered not to be sensitive.
Nevermind the fact that this is not a correct assumption, it still poses another
problem.
Even if you put .env-sample
in your repo, nothing forces your developers to
remember that when that file changes, they need to update their local copies.
And on the flipside, there's nothing to make a developer remember that when they
add something locally, they need to remember to add it to .env-sample
.
So let's say we have a fresh checkout. At which point we do:
cp .env-sample .env
# .env-sample
OTHER_EXISTING_SETTINGS="here"
COMBINATION_TO_MY_LUGGAGE="12345"
# .env
OTHER_EXISTING_SETTINGS="here"
COMBINATION_TO_MY_LUGGAGE="12345"
Now assuming that no changes need to be made to this file (another typically incorrect assumption) to get it to run locally, we now have two identical files.
But at some point we need to change a value locally. That's reasonable:
# .env-sample
OTHER_EXISTING_SETTINGS="here"
COMBINATION_TO_MY_LUGGAGE="12345"
# .env
- OTHER_EXISTING_SETTINGS="here"
+ OTHER_EXISTING_SETTINGS="somewhere"
COMBINATION_TO_MY_LUGGAGE="12345"
The next thing that happens is that another developer comes along and adds a variable which is required for a feature they're working on.
# .env-sample
OTHER_EXISTING_SETTINGS="here"
COMBINATION_TO_MY_LUGGAGE="12345"
+ NEW_FEATURE_SETTING="so sweet"
# .env
OTHER_EXISTING_SETTINGS="somewhere"
COMBINATION_TO_MY_LUGGAGE="12345"
Now, on our next pull, we need to notice that the .env-sample
file changed and
manually merge in the changes.
It'd be great if we could just say:
Give me everything in the shared
development
settings and then override specific settings with my local values.
Chamber's namespaces allow you to have host-based settings which are based on your hostname. Chamber will load everything for your environment, then, if it sees settings for your host, it will override the environment settings with the host settings.
When you make changes to your settings, those are as important as changes to
your code. It seems odd to me that we make a big deal about writing good commit
messages so that we can communicate effectively with the rest of the development
team (or ourselves in 6 months) yet the reason we had to change the SMTP host
name in production
is unimportant enough that it doesn't need to be tracked.
Since any developer can commit secured settings (because the public key is committed to the repository) and because those secured settings can be committed to the repository, then good commit message can be written regarding the why and how of the change.
Copyright ©2023
- Release News
- Gem Comparison
- 12-Factor App Rebuttal
- Environment Variable Problems
- Installation
- Basics
- Defining Settings
- Accessing Settings
- Verifying Settings
- Namespaces
- Environment Variables
- Integrations
- Encryption
- Advanced Usage
- Command Line Reference