Ravioli.rb 🍝
Grab a fork and twist your configuration spaghetti in a single, delicious dumpling!
Ravioli combines all of your app's runtime configuration into a unified, simple interface. It combines YAML or JSON configuration files, encrypted Rails credentials, and ENV vars into one easy-to-consume interface so you can focus on writing code and not on where configuration comes from.
Ravioli turns this...
key = ENV.fetch("THING_API_KEY") { Rails.credentials.thing&["api_key"] || raise("I need an API key for thing to work") }...into this:
key = Rails.config.dig!(:thing, :api_key)🚨 FYI: Ravioli is two libraries: a Ruby gem (this doc), and a JavaScript NPM package. The NPM docs contain specifics about how to use Ravioli in the Rails asset pipeline, in a Node web server, or bundled into a client using Webpack, Rollup, or whatever else.
Table of Contents
- Installation
- Usage
- Automatic Configuration
- Manual Configuration
- Deploying
- License
Installation
- Add
gem "ravioli"to yourGemfile - Run
bundle install - Add an initializer (totally optional):
rails generate ravioli:install- Ravioli will do everything automatically for you if you skip this step, because I'm here to put a little meat on your bones.
Usage
Ravioli turns your app's configuration environment into a PORO with direct accessors and a few special methods. By default, it adds the method Rails.config that returns a Ravioli instance. You can access all of your app's configuration from there. This is totally optional and you can also do everything manually, but for the sake of these initial examples, we'll use the Rails.config setup.
Either way, for the following examples, imagine we had the following configuration structure:*
host: "example.com"
url: "https://www.example.com"
sender: "reply-welcome@example.com"
database:
host: "localhost"
port: "5432"
sendgrid:
api_key: "12345"
sentry:
api_key: "12345"
environment: <%= Rails.env %>
dsn: "https://sentry.io/whatever?api_key=12345"*this structure is the end result of Ravioli's loading process; it has nothing to do with filesystem organization or config file layout. We'll talk about that in a bit, so just slow your roll about loading up config files until then.
Got it? Good. Let's access some configuration,
Accessing values directly
Ravioli objects support direct accessors:
Rails.config.host #=> "example.com"
Rails.config.database.port #=> "5432"
Rails.config.not.here #=> NoMethodError (undefined method `here' for nil:NilClass)Accessing configuration values safely by key path
Traversing the keypath with dig
You can traverse deeply nested config values safely with dig:
Rails.config.dig(:database, :port) #=> "5432"
Rails.config.dig(:not, :here) #=> nilThis works the same in principle as the dig method on Hash objects, with the added benefit of not caring about key type (both symbols and strings are accepted).
Providing fallback values with fetch
You can provide a sane fallback value using fetch, which works like dig but accepts a block:
Rails.config.fetch(:database, :port) { "5678" } #=> "5432" is returned from the config
Rails.config.fetch(:not, :here) { "PRESENT!" } #=> "PRESENT!" is returned from the blockNote that fetch differs from the fetch method on Hash objects. Ravioli's fetch accepts keys as arguments, and does not accept a default argument - instead, the default must appear inside of a block.
Requiring configuration values with dig!
If a part of your app cannot operate without a configuration value, e.g. an API key is required to make an API call, you can use dig!, which behaves identically to dig except it will raise a KeyMissingError if no value is specified:
uri = URI("https://api.example.com/things/1")
request = Net::HTTP::Get.new(uri)
request["X-Example-API-Key"] = Rails.config.dig!(:example, :api_key) #=> Ravioli::KeyMissingError (could not find configuration value at key path [:example, :api_key])Allowing for blank values with safe (or dig(*keys, safe: true))
As a convenience for avoiding the billion dollar mistake, you can use safe to ensure you're operating on a configuration object, even if it has not been set for your environment:
Rails.config.dig(:google) #=> nil
Rails.config.safe(:google) #=> #<Ravioli::Configuration {}>
Rails.config.dig(:google, safe: true) #=> #<Ravioli::Configuration {}>Use safe when, for example, you don't want your code to explode because a root config key is not set. Here's an example:
class GoogleMapsClient
include HTTParty
config = Rails.config.safe(:google)
headers "Auth-Token" => config.token, "Other-Header" => config.other_thing
base_uri config.fetch(:base_uri) { "https://api.google.com/maps-do-stuff-cool-right" }
endQuerying for presence
In addition to direct accessors, you can append a ? to a method to see if a value exists. For example:
Rails.config.database.host? #=> true
Rails.config.database.password? #=> false
ENV variables take precedence over loaded configuration
I guess the headline is the thing: ENV variables take precedence over loaded configuration files. When loading or querying your configuration, Ravioli checks for a capitalized ENV variable corresponding to the keypath you're searching.
For example:
Rails.config.dig(:database, :url)
# ...is equivalent to...
ENV.fetch("DATABASE_URL") { Rails.config.database&.url }This means that you can use Ravioli instead of querying ENV for its keys, and it'll get you the right value every time.
Automatic Configuration
The fastest way to use Ravioli is via automatic configuration, bootstrapping it into the Rails.config method. This is the default experience when you require "ravioli", either explicitly through an initializer or implicitly through gem "ravioli" in your Gemfile.
Automatic configuration takes the following steps for you:
1. Adds a staging flag
First, Ravioli adds a staging flag to Rails.config. It defaults to true if:
-
ENV["RAILS_ENV"]is set to "production" -
ENV["STAGING"]is not blank
Using query accessors, you can access this value as Rails.config.staging?.
BUT, as I am a generous and loving man, Ravioli will also ensure Rails.env.staging? returns true if 1 and 2 are true above:
ENV["RAILS_ENV"] = "production"
Rails.env.staging? #=> false
Rails.env.production? #=> true
ENV["STAGING"] = "totes"
Rails.env.staging? #=> true
Rails.env.production? #=> true2. Loads every plaintext configuration file it can find
Ravioli will traverse your config/ directory looking for every YAML or JSON file it can find. It loads them in arbitrary order, and keys them by name. For example, with the following directory layout:
config/
app.yml
cable.yml
database.yml
mailjet.json
...the automatically loaded configuration will look like
# ...the contents of app.yml
cable:
# ...the contents of cable.yml
database:
# ...the contents of database.yml
mailjet:
# ...the contents of mailjet.json
NOTE THAT APP.YML GOT LOADED INTO THE ROOT OF THE CONFIGURATION! This is because the automatic loading system assumes you want some configuration values that aren't nested. It effectively calls load_file(filename, key: File.basename(filename) != "app"), which ensures that, for example, the values in config/mailjet.json get loaded under Rails.config.mailjet while the valuaes in config/app.yml get loaded directly into Rails.config.
3. Loads and combines encrypted credentials
Ravioli will then check for encrypted credentials. It loads credentials in the following order:
- First, it loads
config/credentials.yml.enc - Then, it loads and applies
config/credentials/RAILS_ENV.yml.encover top of what it has already loaded - Finally, IF
Rails.config.staging?IS TRUE, it loads and appliesconfig/credentials/staging.yml.enc
This allows you to use your secure credentials stores without duplicating information; you can simply layer environment-specific values over top of a "root" config/credentials.yml.enc file.
All put together, it does this:
def Rails.config
@config ||= Ravioli.build(strict: Rails.env.production?) do |config|
config.add_staging_flag!
config.auto_load_files!
config.auto_load_credentials!
end
endI documented that because, you know, you can do parts of that yourself when we get into the weeds with.........
Manual configuration
If any of the above doesn't suit you, by all means, Ravioli is flexible enough for you to build your own instance. There are a number of things you can change, so read through to see what you can do by going your own way.
Using Ravioli.build
The best way to build your own configuration is by calling Ravioli.build. It will yield an instance of a Ravioli::Builder, which has lots of convenient methods for loading configuration files, credentials, and the like. It works like so:
configuration = Ravioli.build do |config|
config.load_file("things.yml")
config.whatever = {things: true}
endThis will return a configured instance of Ravioli::Configuration with structure
things:
# ...the contents of things.yml
whatever:
things: trueRavioli.build also does a few handy things:
- It freezes the configuration object so it is immutable,
- It caches the final configuration in
Ravioli.configurations, and - It sets
Ravioli.defaultto the most-recently built configuration
Direct construction with Ravioli::Configuration.new
You can also directly construct a configuration object by passing a hash to Ravioli::Configuration.new. This is basically the same thing as an OpenStruct with the added helper methods of a Ravioli object:
config = Ravioli::Configuration.new(whatever: true, test: {things: "stuff"})
config.dig(:test, :things) #=> "stuffAlternatives to using Rails.config
By default, Ravioli loads a default configuration in Rails.config. If you are already using Rails.config for something else, or you just hate the idea of all those letters, you can do it however else makes sense to you: in a constant (e.g. Config or App), or somewhere else entirely (you could, for example, define a Config module, mix it in to your classes where it's needed, and access it via a config instance method).
Here's an example using an App constant:
# config/initializers/_config.rb
App = Raviloli.build { |config| ... }You can also point it to Rails.config if you'd like to access configuration somewhere other than Rails.config, but you want to enjoy the benefits of automatic configuration:
# config/initializers/_config.rb
App = Rails.configYou could also opt-in to configuration access with a module:
module Config
def config
Ravioli.default || Ravioli.build {|config| ... }
end
endadd_staging_flag!
load_file
Let's imagine we have this config file:
config/mailjet.yml
development:
api_key: "NOT_USED"
test:
api_key: "VCR"
staging:
api_key: "12345678"
production:
api_key: "98765432"In an initializer, generate your Ravioli instance and load it up:
# config/initializers/_ravioli.rb`
Config = Ravioli.build do
load_file(:mailjet) # given a symbol, it automatically assumes you meant `config/mailjet.yml`
load_file("config/mailjet") # same as above
load_file("lib/mailjet/config") # looks for `Rails.root.join("lib", "mailjet", "config.yml")
endconfig/initializers/_ravioli.rb
Config = Ravioli.build do |config|
%i[new_relic sentry google].each do |service|
config.load_file(service)
end
config.load_credentials # just load the base credentials file
config.load_credentials("credentials/production") if Rails.env.production? # add production overrides when appropriate
config.staging = File.exists?("./staging.txt") # technically you could do this ... I don't know why you would, but technically you could
endConfiguration values take precedence in the order they are applied. For example, if you load two config files defining host, the latest one will overwrite the earlier one's value.
load_credentials
Imagine the following encrypted YAML files:
config/credentials.yml.enc
Accessing the credentials with rails credentials:edit, let's say you have the following encrypted file:
mailet:
api_key: "12345"config/credentials/production.yml.enc
Edit with rails credentials:edit --environment production
mailet:
api_key: "67891"You can then load credentials like so:
``config/initializers/_ravioli.rb`
Config = Ravioli.build do
# Load the base credentials
load_credentials
# Load the env-specific credentials file. It will look for `config/credentials/#{Rails.env}.key`
# just like Rails does. But in this case, it falls back on e.g. `ENV["PRODUCTION_KEY"]` if that
# file is missing (as it should be when deployed to a remote server)
load_credentials("credentials/#{Rails.env}", env_key: "#{Rails.env}_KEY")
# Load the staging credentials. Because we did not provide an `env_key` argument, this will
# default to looking for `ENV["RAILS_STAGING_KEY"]` or `ENV["RAILS_MASTER_KEY"]`.
load_credentials("credentials/staging") if Rails.env.production? && srand.zero?
endYou can manually define your configuration in an initializer if you don't want the automatic configuration assumptions to step on any toes.
For the following examples, imagine a file in config/sentry.yml:
development:
dsn: "https://dev_user:pass@sentry.io/dsn/12345"
environment: "development"
production:
dsn: "https://prod_user:pass@sentry.io/dsn/12345"
environment: "production"
staging:
environment: "staging"Deploying
Encryption keys in ENV
Here are a few facts about credentials in Rails and how they're deployed:
- Rails assumes you want to use the file that matches your environment, if it exists (e.g.
RAILS_ENV=productionwill look forconfig/credentials/production.yml.enc) - Rails does not support environment-specfic keys, but it does now aggressively loads credentials at boot time.
This means RAILS_MASTER_KEY MUST be the decryption key for your environment-specific credential file, if one exists.
But, because Ravioli merges environment-specific credentials over top of the root credentials file, you'll need to provide encryption keys for two (or, if you have a staging setup, three) different files in ENV vars. As such, Ravioli looks for decryption keys in a way that mirrors Rails' assumptions, but allows progressive layering of credentials.
Here are a few examples
| File | First it tries... | Then it tries... |
|---|---|---|
config/credentials.yml.enc |
ENV["RAILS_MASTER_KEY"] |
ENV["RAILS_ROOT_KEY"] |
config/credentials/#{RAILS_ENV}.yml.enc |
ENV["RAILS_MASTER_KEY"] |
ENV["RAILS_#{RAILS_ENV}_KEY"] |
config/credentials/staging.yml.enc |
ENV["RAILS_MASTER_KEY"] |
ENV["RAILS_STAGING_KEY"] |
Credentials are loaded in that order, too, so that you can have a base setup on config/credentials.yml.enc, overlay that with production-specific stuff from config/credentials/production.yml.enc, and then short-circuit or redirect some stuff in config/credentials/staging.yml.enc for staging environments.
TLDR:
- Set
RAILS_MASTER_KEYto the key for your specific environment - Set
RAILS_STAGING_KEYto the key for your staging credentials (if deploying to staging AND you have staging-specific credentials) - Set
RAILS_ROOT_KEYto the key for your root credentials (if you have anything inconfig/credentials.yml.enc)
License
Ravioli is available as open source under the terms of the MIT License.