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
endSince, 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
endIf 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
endPlease 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.