Using Dynamic Config Variables in Ruby on Rails Apps

 
Dynamic setting configuration is represented by a control panel Photo by Steve Harvey on Unsplash

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.



Back to index