Anyway Config: Keep your Ruby configuration sane

栏目: IT技术 · 发布时间: 4年前

内容简介:Configuration is one of the most critical markers of codebase health: the more your application grows and matures, the harder it is to deal with API keys,The most popular configuration pattern in Rails apps is to store all values in theThat pattern itself

It’s time we have a serious conversation about configuration, settings, secrets, credentials, and environment variables in mature Ruby, and especially Rails projects. As a part of this friendly intervention, I will introduce Anyway Config , a gem of my design that keeps project configuration sane at Evil Martians, and, hopefully, will help you escape the “ENV Hell.”

Configuration is one of the most critical markers of codebase health: the more your application grows and matures, the harder it is to deal with API keys, .env files, and other settings. In my RailsConf 2019 talk “Terraforming legacy Rails applications” ( slides , video ), I talked about “ENV Hell” —something I’m sure most of the readers working on larger Ruby apps are familiar with.

Anyway Config: Keep your Ruby configuration sane

ENV Hell slide from the RailsConf 2019 talk

Do you live in ENV Hell?

The most popular configuration pattern in Rails apps is to store all values in the .env file and to load them into process environment on application start (with dotenv or dotenv-rails gems), so they can later be accessible with ENV["KEY"] from code.

That pattern itself comes from a good place: one of the well-respected twelve-factor principles states “Store config in the environment.”

However, let’s perform a simple experiment. Go to your shell, navigate to the project that has a .env file in the root folder, and execute this command:

cat .env | grep '[^\s]' | wc -l
  52 # That's the average value we see in mature projects we are invited to work on

What’s the number? Is it in the order of dozens? Then my condolences, you live in the ENV Hell . It’s all right. We’ve all been there.

For keeping ENV usage in Rails applications under control, we wrote a RuboCop cop called Lint/Env . Use it to make sure the global state is not leaking outside of the configuration files.

Here’s how ENV Hell usually feels like:

  • .env file grows and becomes barely understandable;
  • .env.sample goes out of sync causing hard-to-debug failures during local development;
  • ENV is a global state representing the  outer world , which makes debugging and especially testing harder.

These problems usually creep into deployment setup: for example, Heroku apps tend to have hundreds of environment variables in their configuration. You can quickly check this by running:

$ heroku config -a legacy-project | wc -l

  131

Now, take this quick survey to assess the situation in your current Rails project:

  1. Is it possible to launch rails s right after the project has been bootstrapped ( bin/setup or similar)?
  2. In addition to  rails s , do tests pass?
  3. Do you know where to get credentials if the application is missing them?
  4. Is there a clear workflow for adding new values to configuration?
  5. Is it possible to use personal secrets (like third-party API tokens) without changing the application code?
The more no’s you have, the quicker you should take action and reconsider your approach to configuration.

Stay tuned to find out how.

Secrets and Settings: two types of configuration parameters

Putting all eggs (configuration parameters) into one basket ( .env file) has one major downside: we lose the information about the  nature of our values. We mix them all: sensitive and non-sensitive; business logic- and framework- related.

To become better at keeping track of all the configuration values—we could mentally split them into Settings and  Secrets . Let’s see how they differ.

Settings are internal

Settings modify technical characteristics and framework settings, for example: WEB_CONCURRENCY , RAILS_MAX_THREADS , RAILS_SERVE_STATIC_FILES . Let’s call them framework settings , where “framework” is not only Rails but all parts of our stack, such as Puma and Sidekiq, for example.

There are also application settings that change the application behavior as a whole, such as global feature toggles ( CHAT_ENABLED=1 ) or flags to enable dev tools ( GRAPHIQL_ENABLED=1 ).

Ideal Settings have the following properties:

RAILS_SERVE_STATIC_FILES
config/environments/development.rb

Secrets are external

Secrets carry the information required to interact with other systems and services. They also could be grouped into two types: system and  service .

System secrets include access credentials for the essential parts of the infrastructure, such as databases ( DATABASE_URL ) and cache servers ( REDIS_URL ). Heroku add-ons, for example, set these values for you in production, you only need to take care of them in development (we put them intodocker-compose.yml).

The second group, service secrets , contains the credentials of the third-party services (API keys, tokens, whatever).

Not all the information that counts as a secret has to be sensitive: think API hostnames and limit values.

There is one important technical difference between system and service secrets:

The application must fail on boot if a system secret is missing or invalid

That should not be true for service secrets—not every piece of configuration is essential for your application to start.

Active Storage example

Say, you worked on a file uploading feature and decided to use Active Storage with the Amazon S3 backend. Here are the storage.yml and configuration files that have been merged to master (code comes from a real-life commit):

# config/application.rb
config.active_storage.service = :s3

# config/environments/test.rb
config.active_storage.service = :local
# config/storage.yml
local:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

s3:
  service: S3
  access_key_id: 
  secret_access_key: 
  region: us-east-1
  bucket: ENV["AWS_BUCKET"]

Now everyone on your team who has not set Amazon-related ENV variables won’t be able to run the application even in development . We are artificially creating a barrier for entry, even though directly using AWS SDK locally is not required: Active Storage can use :local setting not only for tests, but for development too. One may say: “I want to have my local environment as close to production as possible!” That’s a good intention, but should it be a  hard requirement ?

Imagine we could make using real AWS buckets for development an opt-in feature.

# config/application.rb
config.active_storage.service =
  if AWSConfig.storage_configured?
    $stdout.puts "Using :s3 service for Active Storage"
    :s3
  else
    :local
  end

All right, that seems foreign and does not come out of the box with Rails. What is AWSConfig , and how does it know that it’s configured? That is the  Anyway Config gem in action, the one I’ve been taunting you with since the beginning of this article.

Enter Anyway Config

Besides using ENV for storing configuration data, modern Rails gives you plenty of other options: from directly editing parameters in config/initializers to using plain old YML files and, since Rails 5.2, encrypted credentials that are safe to check into source control.

Rails 6.0 also adds per-environment credentials. You can back-port them to Rails 5.2 using this gist .

Here are the golden configuration rules we try to follow at Evil Martians:

  • Store sensitive information in Rails credentials (each environment has its own *.enc file).
  • Keep non-sensitive information in  named YAML configs .
  • Allow overriding any value via ENV .
  • Store local (development) secrets and settings in  *.local.yml and  credentials/local.yml.enc files.
  • If you need to share extra-sensitive credentials with all your team members, use centralized encrypted storage. Keybase does this job for us.
It is hard to avoid the embarrassment of riches: using all these different ways to get configuration into your app adds to the cognitive overhead. So we came up with a tool that provides a standard, pure Ruby interface to all configuration settings.

Anyway Config is a gem that allows you to manage different sources of data transparently. Moreover, it makes your code independent of the way you store your settings by introducing configuration classes . No more Rails.credentials , Rails.application.config_for , or  ENV calls; you only need to deal with Ruby classes.

The gem has a long story: extracted initially from the first gem of mine, Influxer , it has been used mostly in libraries (for instance, AnyCable ) for a long time. The  recent 2.0 release is heavily inspired by the application development use-cases we had in Evil Martians in the last couple of years.

Let’s return to our Active Storage example to see how we could have configured it with Anyway Config.

Adding anyway_config to your Gemfile in Rails gives you access to handy generators that create new configuration classes:

$ rails generate anyway:config aws access_key_id secret_access_key region storage_bucket
    generate  anyway:install
       rails  generate anyway:install
      create  config/configs/application_config.rb
      append  .gitignore
      insert  config/application.rb
      create  config/configs/aws_config.rb
Would you like to generate a aws.yml file? (Y/n) n

The reason why we use config/configs and not app/configs has to do with how Rails auto-loads and reloads constants, see more here .

That would add two files to your project:

  • config/configs/application_config.rb —base class for all configuration classes (only created if not exists):
# Base abstract class for config files.
# It provides the `instance` method, which returns the default
# instance for this config.
#
# It also delegates all the missing methods to this instance,
# thus allowing you to use the class itself as a singleton config instance.
class ApplicationConfig < Anyway::Config
  class << self
    delegate_missing_to :instance

    def instance
      @instance ||= new
    end
  end
end
  • config/configs/aws_config.rb —AWS configuration class:
class AWSConfig < ApplicationConfig
  # attr_config defines readers and writers
  # for configuration parameters
  attr_config :access_key_id, :secret_access_key,
              :region, :storage_bucket
end

Note that you need to configure Rails inflector to understand “AWS” by adding an acronym to config/initializers/inflections.rb :

ActiveSupport::Inflector.inflections do |inflect|
  # ...
  inflect.acronym "AWS"
end

If you answer “yes” to the generator prompt, config/aws.yml file will be added as well. For now, we don’t want to store any information in plain text; we’re going to use credentials.

Let’s edit our configuration class and add the default value for the region as well as the #storage_configured? method:

class AWSConfig < ApplicationConfig
  # We can provide default values by passing a Hash
  attr_config :access_key_id, :secret_access_key,
              :storage_bucket, region: "us-east-1"

  def storage_configured?
    access_key_id.present? &&
      secret_access_key.present? &&
      storage_bucket.present?
  end
end

Then, we need to populate values for production. Let’s open our credentials file and define the following values:

$ RAILS_MASTER_KEY=<production key> rails credentials:edit -e production

aws:
  access_key_id: "secret"
  secret_access_key: "very-very-secret"
  storage_bucket: "also-could-be-a-secret"

Depending on your use-case, you may not consider storage_bucket to be a piece of sensitive information. If that’s the case, you can define it in  config/aws.yml :

# config/aws.yml
production:
  aws:
    storage_bucket: my-public-bucket

At any time, you can override any value by providing the corresponding environment variable:

AWS_STORAGE_BUCKET=another-bucket rails s

Did you notice that your code doesn’t care about where the configuration values come from and only knows about the AWSConfig class? That’s the main benefit of this approach.

If one day you decide to use AWS locally, you can do that by putting your personal credentials to config/aws.local.yml . If you’re worried about storing secrets as plain text on your machine, you can use local Rails credentials:

rails credentials:edit -e local

Anyway Config will assign higher priority to your local data.

And as a bonus (especially useful in production), you can track the source of every value:

AWSConfig.to_source_trace
# =>
# {
#  "access_key_id" => {value: "XYZ", source: {type: :credentials, store: "config/credentials/production.yml.enc"}},
#  "secret_access_key" => {value: "123KLM", source: {type: :credentials, store: "config/credentials/production.yml.enc"}},
#  "region" => {value: "us-east-1", source: {type: :defaults}},
#  "storage_bucket" => {value: "example-bucket", source: {type: :yml, path: "config/aws.yml"}}
#  }

You can also pretty-print it to get a more human-friendly output:

pp AWSConfig.new
# =>
# #<AWSConfig
#  config_name="aws"
#  env_prefix="AWS"
#  values:
#    access_key_id => "XYZ" (type=credentials store=config/credentials/production.yml.enc)
#    secret_access_key => "123KLM" (type=credentials store=config/credentials/production.yml.enc)
#    region => "us-east-1" (type=defaults)
#    storage_bucket => "my-public-bucket" (type=yml path=config/aws.yml)

To sum up, Anyway Config gives you:

  • Configuration classes instead of different data source wrappers.
  • Support for local secrets and settings.
  • Separate configuration files instead of a single bloated .env or  application.yml .

As Ruby application grows, configuration management can quickly become a nightmare. You can keep it under control by paying more attention to how you organize configuration files and how you treat different kinds of values.

Making codebase free of knowledge of every particular configuration data source also helps to keep your project healthy. Anyway Config provides you with a common abstraction that suits all cases—now you are free to mix, match, and override pieces of configuration coming from different sources without any cognitive overhead.

Use ENV responsibly!

By the way, if you’re looking into “terraforming” your mature Rails application to introduce best practices to date: from setting up containerized development environment to speeding uptests, adoptingGraphQL, optimizing database queries or generally getting rid of any performance bottlenecks—feel free to drop us a line, my colleagues and I can definitely help.


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

因计算机而强大

因计算机而强大

[美]西摩 佩珀特 Seymour Papert / 梁栋 / 新星出版社 / 2019-1 / 38

本书有两个中心主题—— 孩子可以轻松自如地学习使用计算机; 学习使用计算机能够改变他们学习其他知识的方式。 (前苹果公司总裁 约翰·斯卡利) 最有可能带来文化变革的就是计算机的不断普及。 计算机不仅是一个工具,它对我们的心智有着根本和深远的影响。 计算机不仅帮助我们学习 ,还帮助我们学习怎样学习。 计算机是一种调解人与人之间关系的移情对象。 一个数学的头脑......一起来看看 《因计算机而强大》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

在线进制转换器
在线进制转换器

各进制数互转换器

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具