New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Require environment variables to be defined at startup #17175
Conversation
@aantix nice! I had this itch too and wrote https://github.com/eval/envied#envied- |
@schneems Finding a way to ensure that third party environment variables are set before deployment has caused us issues in the past with Heroku. Any comments on this PR? |
I think it should raise after checking all variables
|
2a0b07d
to
695ae88
Compare
@seuros Agree. I made the appropriate changes. |
|
||
def validate_environment_vars! | ||
if config.required_env_vars.present? | ||
vars = config.required_env_vars.each_with_object([]) do |key, required_vars| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think each_with object is slower that if you do
required_vars = []
config.required_env_vars.each do |key|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missing_env_vars = []
config.required_env_vars.each { |key| missing_env_vars << key.upcase if ENV[key].blank? }
695ae88
to
f618005
Compare
Made the suggested changes. |
if config.required_env_vars.present? | ||
missing_env_vars = [] | ||
config.required_env_vars.each do |key| | ||
missing_env_vars << key if ENV[key].blank? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ENV vars are upcased.
Generally I use |
f618005
to
9ef293a
Compare
if config.required_env_vars.present? | ||
missing_env_vars = [] | ||
config.required_env_vars.each do |key| | ||
key.upcase! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seuros Now upcasing the env var name. The tests now have upcased env variables to reflect this convention.
9ef293a
to
f8b8128
Compare
I think this need a Changelog entry. |
f8b8128
to
69416bd
Compare
Added Changelog entry. |
@@ -511,5 +512,17 @@ def validate_secret_key_config! #:nodoc: | |||
raise "Missing `secret_key_base` for '#{Rails.env}' environment, set this value in `config/secrets.yml`" | |||
end | |||
end | |||
|
|||
def validate_environment_vars! | |||
if config.required_env_vars.present? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about
if Array(config.required_env_vars).any?
just in case someone screw it and put a string or nil
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea. Made the change and wrote a test covering the env as a string case.
69416bd
to
a237474
Compare
I agree that this is redundant with Ruby. Use |
That said this certainly gives a nicer error message. |
@@ -48,6 +48,9 @@ Rails.application.configure do | |||
# Decrease the log volume. | |||
# config.log_level = :info | |||
|
|||
# Enforces that all of the following environment variables are defined before server start | |||
# config.required_env_vars = ['api_key', 'other_api_key'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently when someone uses symbols (just like with the log_tags-config right below), it will raise (no matter the contents of ENV) but it won't be clear from the error why (e.g. assigning [:A] will err with message "Must define the environment variable(s) A").
To anticipate this, either the lookup could be made more flexible (ENV[key.to_s]
) (or the error message more clear ("Must define the environment variable(s) :A")).
Second: as ENV['SECRET_KEY_BASE']
is contained in the default secrets.yml
, it might be a good candidate to either include in the example or even uncomment this line and just set it?
Third (nitpick): upcasing ENV-keys seems a bit odd IMO, in that it adds more surprise than value.
Besides that, while not common/recommended, users might actually have lowercase ENV-keys.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point on stringifying the key.
@seuros Thoughts on the upcasing of the ENV keys? I'm looking through my production apps and all of my keys are upcased, but I agree that there could be exceptions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now making the to_s conversion on the key.
@sgrif The fetch is effective but the issue is that the missing env only triggers the exception when fetched. For lazy initializations, the critical path may not be hit until much after system startup. This is an effort to formally declare these envs up front and fail (if missing) quickly. |
bbd09df
to
7952732
Compare
missing_env_vars = [] | ||
|
||
required_vars.each do |key| | ||
key = key.to_s |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we could do it inline
key = key.to_s.upcase
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not so sure we should be forcing the upcase. I agree with @eval, there's definitely mixed case eng variables out there and developers may not have the control to convert them to all upper case. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds reasonable.
@@ -41,6 +41,7 @@ def initialize(*) | |||
@railties_order = [:all] | |||
@relative_url_root = ENV["RAILS_RELATIVE_URL_ROOT"] | |||
@reload_classes_only_on_change = true | |||
@required_env_vars = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let use a Set
here. Multiple gems could require the same vars.
That will avoid us having a messages like
Must define the environment variable(s) FOO_API_KEY FOO_API_KEY FOO_API_KEY FOO_API_KEY
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call. We want to use a Set as well when aggregating the missing env vars.
Now using a Set and updated the tests to account for duplicate environment var declarations.
7952732
to
2053139
Compare
I agree with @aantix, having this fail early is much better than only triggering it on some edge case/rare path in your app. I like the patch/idea. |
@aantix , could you rebase ? |
…eloper to specify a series of environment variables that must be defined at started (otherwise an exception is thrown). # e.g. config.required_env_vars = ['api_key', 'other_third_party_api_key']
2053139
to
dea3ed8
Compare
Rebased. |
@@ -47,6 +47,9 @@ Rails.application.configure do | |||
# when problems arise. | |||
config.log_level = :debug | |||
|
|||
# Enforces that all of the following environment variables are defined before server start | |||
# config.required_env_vars = ['API_KEY', 'OTHER_API_KEY'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dhh what do you think about this being included directly on production.rb? I can see how this feature can be useful but I don't like the idea of promoting it by default. In my projects we usually don't use any ENV key.
Not a fan of promoting free-floating ENVs like this. My recommendation would be to let this flow through config/secrets.yml like so: development:
secret_key_base: x
stuff: <%= ENV.fetch 'stuff' %> That achieves the same thing, and it bundles all your secrets together to be accessed via Rails.application.secrets. |
We could even explain this pattern in config/secrets.yml as a comment at the top. |
The only problem with the much simpler mechanism of putting it in secrets.yml is that it doesn't enforce the presence of those settings. That's what this ticket is about, catching deployments to new environments/systems where you haven't configured the environment properly. So if the comment looked more like the one below I'd be in favour:
But this then borders on just educating people on a good way of doing it (which could be done in a blog post/Rails docs) rather than having the Framework help people do something that is a common pattern. (edited: I forgot fetch raises an error, but I'd still prefer this to be a specific non-obvious error rather than just KeyError) |
Fetch raises automatically and secrets.yml is evaluated at boot. So if you ENV.fetch something that isn't there, it's not going to boot. Education for this usage can go at the top of secrets.yml.
|
The secrets.yml approach feels a bit strange just because you're specifying the key name twice (once for the secrets.yml entry and once for the ENV fetch). My hunch is that developer would keep the secret key name consistent with the ENV var name. What about an (optional) array in the secrets.yml that will automatically map the corresponding ENV value to a secret entry? E.g. required_env_vars:
- TWITTER_API_KEY
- ROLLBAR_API_KEY Using the logic in the original PR, the above required_env_vars would be checked for their existence upon startup. It's one nice consolidated list of external ENVs. The developer would then access the keys via the conventional Rails.application.secrets[:twitter_api_key] |
The repetition really doesn't bother me because you are pulling something in from the outside world. To me, that's similar to person.name = params[:person][:name] or whatever. Further more, this is not an area that needs excessive comfort. You're not going to have neither dozens of ENVs nor adding/removing them constantly.
|
Turns out that you can't use Probably the simplest way to achieve the desired effect is to add something like the following to # Load the Rails application.
require File.expand_path('../application', __FILE__)
if Rails.env.production?
%w[SECRET_KEY_BASE API_KEY].each do |var|
raise RuntimeError, "Missing environment variable: #{var}" unless ENV.key?(var)
end
end
# Initialize the Rails application.
Rails.application.initialize! Some people would argue that it's duplicating the list of vars since they'd be defined in a .env file but that's somewhat application dependent - not everyone uses .env files. |
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. |
Added the config option required_env_vars. This option allows the developer to specify a series of environment variables that must be defined at started, otherwise an exception is thrown.
If either the ENV['rollbar_api_key'] or ENV['github_api_key'] environmental variables aren't defined, an exception is thrown and the server fails to startup.
We've had several instances where our ops team forgot to export an API key on the server and it wasn't apparent until later.
Especially useful for systems like Heroku where reliance on environment variables is prevalent for Rails apps.