
Config variables should never be embedded directly in the codebase. It is a common practice to extract them into a separate configuration layer. A standard approach for Ruby on Rails apps is to use static ENV
variables that can only be changed via a release process. In this blog post, I’ll describe the use cases for using dynamic config variables that can be modified on the fly. I’ll also share a simple way to start using them in your app without including additional gem dependencies.
Dynamic vs. static configuration
It is perfectly OK to use static ENV
variables for storing values that are not likely to change, i.e., database connection URL, API keys, etc. For the development environment, I recommend direnv or dotenv for managing local variables configuration. Tooling in the production environment is different depending on the stack. But, it usually has a form of static YAML files that are parsed during a deployment process.
However, while conducting my Rails performance audits, I’ve found static configuration limiting in some cases. Achieving the optimal performance often consists of tweaking various dials and buttons. When applying a change to a bottleneck endpoint is often impossible to know which value will work the best before testing it with actual production traffic patterns.
A few examples of values that could require dynamic tweaking are:
- cache refresh time thresholds
- throttling limits for Sidekiq jobs
- collections pagination config
- feature flags
Deploying a change to a static ENV
variable might take over a dozen minutes, depending on a project’s release mechanism. Often you need to wait for an elaborate continuous integration process to tweak a single value. Misconfiguring a variable in a bottleneck endpoint might result in a prolonged degraded performance or even a downtime before the new release goes live.
Read on to learn how you can dynamically change configuration variables without a need for a sluggish release.
Redis as storage for dynamic config variables
I’ve found Redis to be an optimal tool for storing dynamic config. Reading data has virtually no overhead, it does not require cumbersome schema migrations, and 99% of Rails projects out there already have it in their stack.
Let’s have a look at a service object that you can use to read and write config variables:
class ConfigVars
include Singleton
def set(key, value)
redis.set("CFG-#{key}", Marshal.dump(value))
end
def get(key)
value = redis.get("CFG-#{key}")
if value.nil?
ENV[key]
else
Marshal.load(value)
end
end
def keys
redis.keys.select { |k| k.include?("CFG-") }
end
private
def redis
@redis ||= Redis.new(url: ENV.fetch("REDIS_URL"))
end
end
Since, these are global values we’ll leverage the built-in Singleton
module. The get
and set
methods offer a lightweight abstraction over the Redis API. We add keyname prefixes for easier debugging and fallback to ENV
values. We also utilize Marshal
methods to serialize values to strings.
You can use this service object like that:
storage = ConfigVars.instance
storage.set("USERS_PAGINATION_LIMIT", 50)
storage.get("USERS_PAGINATION_LIMIT") # => 50
storage.set("BLACKLISTED_IPS", ["1.2.3.4", "4.3.2.1"])
storage.get("BLACKLISTED_IPS") # => ["1.2.3.4", "4.3.2.1"]
storage.keys # => ["CFG-USERS_PAGINATION_LIMIT", "CFG-BLACKLISTED_IPS"]
Practical examples of using dynamic config values
Let’s now see our service object in action. When working with dynamic variables, you need to be careful to use them well in a dynamic way. It’s easy to accidentally freeze the value of a dynamic config if you use it incorrectly.
Let’s consider a sample controller where you want to configure size of a paginated collection using a static variable:
class UsersController < ApplicationController
PER_PAGE = ENV.fetch("PER_PAGE")
def index
@users = User.limit(PER_PAGE)
end
end
If we want to dynamically control the size of a returned collection we can use the following implementation:
class UsersController < ApplicationController
def index
@users = User.limit(
ConfigVars.instance.get("PER_PAGE")
)
end
end
Please notice that you have to read the value of a dynamic variable inside the index action. If you saved it to a constant like in the former example, then changing the value wouldn’t have any effect without an additional release.
Summary
Dynamic config variables can be extremely useful for tweaking details of your project with an instant feedback loop. They incur a small complexity overhead. In return, this technique allows you to deploy changes with more confidence that errors can be quickly reverted without impacting your users.