Don't Use the UNIX Environment Directly
June 10, 2016 📬 Get My Weekly Newsletter ☞
Getting configuration from the UNIX environment is a hallmark of 12-factor application design, and is a great practice. Problems arise, however, when your code is littered with direct references to it. This is because the UNIX environment is a very poor database, but we need to treat it as a better one.
Instead of having your code that needs configuration grab values directly from the environment, you should use a lightweight abstraction layer that your code depends on. This has three advantages:
- It allows you to deal with the fact that the UNIX environment is essentially typeless.
- It’s a centralized place for all default values needed for optional settings.
- It’s a single place for things that might need to be configurable, but aren’t yet.
Environment Variables are Strings
Most programming languages vend environment variables as strings. This leads to errors like so:
if ENV["SOME_FLAG"]
puts "Flag enabled!"
else
puts "Flag disabled."
end
In Ruby, all non-nil, non-false values are “truthy”. Since ENV#[]
will only ever return either nil
or a String, the “Flag disabled” path wil never execute:
> SOME_FLAG=false ./test.rb
Flag enabled!
This means that you have to coerce your environment variables to a type you want. Often, developers do this:
if ENV["SOME_FLAG"] == "true"
puts "Flag enabled!"
else
puts "Flag disabled."
end
This is somewhat verbose, easy to mess up, and creates other problems when you have someone who prefers “0” and “1” instead of “true” and “false”:
> SOME_FLAG=1 ./test.rb
Flag disabled.
If you have a layer between your code and the environment, you can handle that in a common way.
class Settings
def self.some_flag?
["1","true"].include?(ENV["SOME_FLAG"].to_s.downcase)
end
# or maybe
def self.some_flag?
boolean("SOME_FLAG")
end
private
def boolean(env_var)
["1","true"].include?(ENV[env_var].to_s.downcase)
end
end
Your code then becomes much cleaner:
if Settings.some_flag?
puts "Flag enabled!"
else
puts "Flag disabled."
end
It also works :)
> SOME_FLAG=1 ./test.rb
Flag enabled!
> SOME_FLAG=True ./test.rb
Flag enabled!
> SOME_FLAG=false ./test.rb
Flag disabled.
With an abstraction layer, we can also handle default values for optional environment variables.
Centralizing Defaults
Suppose we want to allow the configuration of a timeout for talking to our payment processor. We have an idea of what the right value is, but we may need to tweak it in production, so we don’t want to have to do a code change every time. So, we’ll grab it from the environment, but set a reasonable default.
class Settings
def self.payment_processor_timeout
ENV["PAYMENT_PROCESSOR_TIMEOUT"].try(:to_i) || 2000
end
end
Note we have to use try
because nil.to_i
returns 0, not nil
. So, we’re saying “if a value has been set, coerce it to an integer, otherwise use 2000”.
Without try
, you can do:
class Settings
def self.payment_processor_timeout
(ENV["PAYMENT_PROCESSOR_TIMEOUT"] || 2000).to_i
end
end
With such a system set up, you can use this to centralize all your application’s configurable bits, even if you don’t need or want them overridden by the environment.
Centralizing Configuration
For example, you might be using S3 to store files, and want all code that uses S3 to use the same bucket, but you have no real need to configure that bucket in the environment.
class Settings
def self.s3_bucket_name
"my-app-files"
end
end
This now means the code that needs the bucket name can just ask the settings for it, and if you later need it to be configurable, it can easily be extracted from the environment.
Isn’t this a solved problem?
We use the mc-settings gem, that uses an ERB-ized YAML file:
some_flag: <%= ["1","true"].include?(ENV["SOME_FLAG"].to_s.downcase) %>
payment_processor_timeout: <%= ENV["PAYMENT_PROCESSOR_TIMEOUT"].try(:to_i) || 2000 %>
s3_bucket_name: "my-app-files"
This allows us to write Setting.some_flag
. It even supports nested settings, like so:
payment_processing:
- timeout: <%= ENV["PAYMENT_PROCESSOR_TIMEOUT"].try(:to_i) || 2000 %>
- api_key: <%= ENV["PAYMENT_PROCESSOR_API_KEY"] %>
We can then do Setting.payment_processing(:timeout)
to access the configured value.
Conclusions
Don’t litter your code with references to the environment. It’s easy to create bugs because the environment is a somewhat degenerate settings database. It also makes your code harder to follower because you are using SCREAMING_SNAKE_CASE
instead of nice, readable methods. It also makes it hard to centralize type coercions and default values.