Sane Defaults
Tags: configuration, practices, rails, kubernetes
Published: 2020-02-28 22:32:57

---
title: Sane Defaults
published: true
description: This post explains one of the practices we use to improve web service configuration management. As an example, we will use a Ruby on Rails application running on the Kubernetes environment. However, the general concept of sane defaults is technology-agnostic and entirely applicable to different programming languages and frameworks.
tags: configuration,practices,rails,kubernetes
---

**This post explains one of the practices we use to improve web service configuration management. As an example, we will use a Ruby on Rails application running on the Kubernetes environment. However, the general concept of sane defaults is technology-agnostic and entirely applicable to different programming languages and frameworks.**

Imagine medium to large scale web service maintained by a team of 5 engineers. The production environment constitutes a distributed system of Rails servers, auxiliary microservices, and data storage. Everything is running on a Kubernetes cluster. As usual, besides the production, there is also a staging configuration — very similar, but not identical. It is smaller and cheaper. And there are also local development environments typically containing a minimal task-specific subset of services.

To maintain multi-environment Rails application, we need it to be configurable. Most common approach here is to follow [The Twelve-Factor App](https://12factor.net) methodology and store configuration in the system environment variables.

Say, we use `dotenv` gem to set some arbitrary configuration parameters for development environment:

```
# .env
MAIN_DOMAIN=app.local
REDIS_URL=redis://127.0.0.1:6379/0
AVATAR_SIZE=100
IMGPROXY_URL=https://imgproxy.local
SECRETS_IMGPROXY_KEY=key
SECRETS_IMGPROXY_SALT=salt
```

Now when we can address configuration values like so: `ENV.fetch('MAIN_DOMAIN')`. Dead-straight, cheap and easy? Sort of. Let's take a closer look at the potential shortcomings related to environment variables-based configuration in a mature Rails project.

# Downsides of using environment variables

**Configuration bloat.** A more extensive project configuration may include hundreds of parameters. This fact itself is healthy — big configuration is better than hardcoding magic values all over the codebase, anyway. But you need to expect the config to require some gardening to prevent duplication, obsoletion, and other potential issues.

**Each change in the configuration require manual effort.** If your project uses `dotenv` on the development environment, each team member will have to maintain an up to date copy of the `.env` file with all the necessary configuration variables.

> `.env` file may include individual settings — like a personal S3 bucket name you use for testing — and not supposed to be committed to the version control system. Therefore, it is not possible to automate config updates propagation amongst the team members.

**Application startup errors.** Typically it is a good practice to check mandatory configuration parameters during the application initialization stage and raise an error if something is missing (note `#fetch` method call in the above example). I will skip a detailed explanation here, assuming you already familiar with the [fail-fast](https://en.wikipedia.org/wiki/Fail-fast) principle in system design. But in the practical context of this article, this validation means each configuration change may cause a hiccup in every team member's working process. Nobody likes it.

**Fragile tests.** Sometimes you need to use your configuration during testing, directly or indirectly. That means your test suite execution result depends on the system it is running on. A successful test run on a developer's machine does not guarantee the same result on CI, even in a containerized environment. And the reasons for the failure could be much less evident than the wrong `DATABASE_URL`.

# Sane defaults

Our experience of maintaining configuration for larger web services shows that a significant part of configuration parameters don't change too often. Based on this observation, it is possible to mitigate most of the issues listed above by complementing configuration parameters with default values:

```ruby
# const/defaults.rb

module Defaults
  AVATAR_SIZE = 100
  MAIN_DOMAIN = 'amplifr.local'
  IMGPROXY_MAX_SRC_RESOLUTION = 9000
end
```

Rails autoload mechanism will make `Defaults` module resolvable from everywhere in your project, making it possible to use the constants as a safe fallback each time you need the corresponding configuration parameter:

```ruby
ENV.fetch('AVATAR_SIZE') { Defaults::AVATAR_SIZE }
```

Default constants make it safe to remove related environment variables from `.env`, or `Dockerfile`, or`docker-compose.yml`, or whatever you use to keep your configuration. Configuration changes will automatically propagate over the version control system, preventing some of the application startup errors. And from now on, it is easy to keep an environment-agnostic test suite (just keep using `Defaults::*` values instead of the `ENV`).

Furthermore, I'd like to highlight that sane defaults plays well with the Twelve-Factor App methodology since environment variables always have higher priority over the constants.

There are some limitations, though: not every configuration parameter could have a default. Each default value should match the following conditions:

- A sane default supposes to remain identical for all code execution environments, including Rails environments, individual developer machines, server instances, CI, etc.
- Default values should never be secret. In other words, defaults do not apply to API keys or other configuration parameters that you don't want to keep in the repository.
- A default does not change often or ever.

# Making it easy to use

We solved the issues of the environment variables-based configuration, but ~~at what cost?~~ accessing configuration params with `fetch` calls is bulky and cumbersome:

```ruby
ENV.fetch('AVATAR_SIZE', Defaults::AVATAR_SIZE)
```

Also, it would be a bad idea to rely on the developer to remember about the defaults each time they use a related configuration. And what about required parameter validation during the application initialization?

Luckily it is easy to solve all of these problems with [config](https://github.com/rubyconfig/config) gem. It allows to define structured configuration with YAML files, similar to Rails' `database.yml`:

```yaml
# config/settings.yml
---
main_domain: <%= ENV.fetch('MAIN_DOMAIN') { Defaults::MAIN_DOMAIN } %>
redis_url: <%= ENV.fetch('REDIS_URL') { Defaults::REDIS_URL } %>
avatar_size: <%= ENV.fetch('AVATAR_SIZE') { Defaults::AVATAR_SIZE } %>

imgproxy:
  base_url: <%= ENV.fetch('IMGPROXY_URL') %>
  key: <%= ENV.fetch('SECRETS_IMGPROXY_KEY') %>
  salt: <%= ENV.fetch('SECRETS_IMGPROXY_SALT') %>
  max_src_resolution: <%= ENV.fetch('IMGPROXY_MAX_SRC_RESOLUTION') { Defaults::IMGPROXY_MAX_SRC_RESOLUTION } %>
```

Being added to your `Gemfile`, `config` gem builds a globally available settings object during Rails application startup, providing a set of concise accessors to the configuration params, like `Settings.main_domain` or `Settings.imgproxy.base_url`.

# Summary

Here is a list of benefits you gain from complementing environment-based configuration with sane defaults:

- Prevent configuration bloat.
- Automate shared configs propagation over developer machines.
- Prevent startup errors caused by the lack of shared configuration params.
- More stable tests.