Rails Rails 最佳实践之配置管理

zamia · 2017年01月15日 · 最后由 zhangbin-github 回复于 2017年11月23日 · 9556 次阅读
本帖已被管理员设置为精华贴

大家日常开发 Rails 项目的过程中一定会遇到些配置项(Configuration),随着配置越来越多,总归需要管理起来,那么如何管理这些配置呢?本文期望梳理一下,找到一个较好的解决方案。

(其实是我们自己项目中的配置文件非常多且乱,所以力图找到一个好的方法来管理,从最终效果来看,这种方法应该是比较合理的,拿来跟大家分享。)

先放结论:

  • 应用配置统一使用 settings.yml 来管理;
  • 每一个资源配置(数据库、redis、第三方 API)使用独立的配置文件管理;
  • 资源配置中的敏感信息要使用环境变量管理;

如果不太理解的话可以往下看,如果项目中已经这么做了,不用浪费时间~

配置的简单分类

先把如何管理放在一边,看看配置都包括哪几个类别:

  • Rails 基础配置 (Framework Configuration) 比如 Rails 需要配置 session store 的地址、asset host、action mailer 等等,这些配置都集中在 /config/application, 不同的环境放在 config/environments下面。

  • 应用配置(Application Configuration) 比如分页时每页的条数,开发环境数据不多,每页 10 条,线上环境需要 50 条。那这个分页条目就是简单的一项配置了。还有一些比如开关类的配置,临时加一个开关,到了固定的时候开启某些功能,都属于典型的应用配置。

  • 资源配置(Resource Configuration) Rails 项目本身应该是无状态的(Stateless),所有的资源都应该独立通过配置的形式体现。比如数据库就是一种资源(Resource),像 Redis、ElasticSearch 都可以认为是一种资源。另外,我们把第三方服务也称为资源,比如项目中需要访问第三方 API,那么这个 API 我们也认为是一种资源。

Rails 的基础配置无需多说,大家应该都很熟悉了,基本上 Rails 都已经做好了样例,按照样例配置就可以了。比如 config/application.rb 可能会有这些东西:

config.autoload_paths += Dir["#{config.root}/app/utilities/"]
config.i18n.default_locale = "zh-CN"
config.active_job.queue_adapter = :sidekiq
config.time_zone = 'Beijing'

所以下面直接说说应用配置和资源配置。

应用配置

简单版本

最常用的方法其实是在 config 目录下面写一个 settings.yml 的文件,比如:

# config/settings.yml
default: &default
    page_size: 10
    enable_some_feature: false

development:
    <<: *default

production:
    <<: *default
    page_size: 100
    enable_some_feature: true

然后添加一个 initializer 来加载它:

# config/initializer/load_settings.rb
$settings = YAML.load_file("#{Rails.root}/config/settings.yml")[Rails.env].symbolize_keys

# use config in somewhere
logger.debug $settings[:page_size)

config_for 版本

Rails 4.2 以后也提供了简单的方法来加载配置:

# config/application.rb
$settings = config_for(:settings)

使用 config_for 可以自动加载对应 RAILS_ENV 的配置,还可以加载 ERB 内容,所以尽量使用 config_for 来加载配置。

但是上面两种方法本质差不多,也可以这样用,不过稍微也有一些不方便的地方:

  • 使用不太方便,都是字符串做 key,如果使用 symblolized_keys 的话又不支持级联;
  • 手动加载时不能使用 ERB,比如加载环境变量等。

使用 gem - railsconfig/config

这个 gem 稍微升级了一下,做了方便使用的改动,比如自动支持多环境、支持 ERB、支持『.』调用,还可以多级调用。使用也比较简单,一看文档即知。

# Gemfile
gem 'config'

# 执行下面的命令,会生成一些模板
# rails g config:install

# 调用时
logger.debug Settings.some_config.other_config

这个 Gem 使用起来很方便,所以我们推荐应用配置均使用 setting.yml,不同的环境的文件放到 settings 目录,这些内容并不包含敏感信息,因此可以放心的 checkin 到 git 库中。

config/settings.yml
config/settings/production.yml
config/settings/development.yml
config/settings/test.yml

资源配置

资源配置会稍微麻烦一些,先说下什么是资源。以下内容来源于 12factor:

  • 把后端服务 (backing services) 当作附加资源。
  • 后端服务是指程序运行所需要的通过网络调用的各种服务,如数据库,消息/队列系统,SMTP 邮件发送服务,以及缓存系统。
  • 类似数据库的后端服务,通常由部署应用程序的系统管理员一起管理。除了本地服务之外,应用程序有可能使用了第三方发布和管理的服务。
  • 12-Factor 应用不会区别对待本地或第三方服务。对应用程序而言,两种都是附加资源,通过一个 url 或是其他存储在 配置 中的服务定位/服务证书来获取数据。

因为后端服务通常由系统管理员(运维同学)来统一管理,运维架构对于开发工程师来讲可能是透明的,比如一个数据库地址,可能是一组集群,管理员只会提供一个入口的 vip 而已。

明白了什么是资源之后,我们就把资源相关的配置也分为两类:

  • 非敏感信息,一般也是开发工程师可以感知的信息。比如一个 API 的地址;
  • 敏感信息,一般不对开发工程师开放。比如一个 API 的认证信息;

resource.yml.example 的方式管理

对于资源类的配置,Rails 默认的做法是采用 resource.yml,但是在 git 库中使用 resource.yml.example 的形式。

比如 rails 项目产生开始就会产生一个 database.yml.example,一般我们通过修改这个文件,然后 copy/move 一份 database.yml 出来使用。

这种方式有几个问题,12factor 也有详细描述;

  1. 随着资源越来越多,这类的 yml 越来越多,管理起来比较麻烦;
  2. 每次添加一个资源都添加 yml 的方式很容易漏掉,导致敏感文件加入了 git 库;
  3. 跟语言绑定,无法跨语言使用等;

使用 yml + 环境变量来管理资源类配置

虽然 12factor 中推荐应用配置存储在环境变量中,但是我们更近一步,只把资源配置中的敏感信息存储在环境变量中,而非敏感信息仍旧存储在 yml 中。

这样的话配置的目录大概是这样:

# 配置相关目录结构
rails-app
  config
    mongo.yml
    redis.yml
    rabbitmq.yml
    mail_api.yml
.env

一个资源的配置文件大概是这样:

# mongo.yml
default: &default
  host: ENV["MONGO_HOST"]
  port: ENV["MONGO_PORT"]
development:
  <<: *default
  database: mongo_development
production:
  <<: *default
  database: mongo_production

.env 文件大概是这样子的:

# .env 文件
MONGO_HOST=127.0.0.1
MONGO_PORT=27017

通过结合 yaml 文件和 env 文件,资源的配置被分割为敏感信息和非敏感信息,所有的 yml 都可以安全的 checkin 到 git 库中,而 .env 文件是在部署的时候由运维工程师通过使用一些自动化工具来部署到线上环境。

因为 .env 文件在 rails 环境中无法做到自动加载,因为我们还需要一个 gem 来辅助:

# Gemfile
gem 'dotenv-rails', require: 'dotenv/rails-now'

通过使用 dotenv 这个 Gem,可以做到很方便的自动加载 .env 文件,它甚至可以自动根据 Rails Env 来加载 .env.production 这样的配置,不过我们不需要这个功能,只需要一个 .env 就可以了。

然后就可以很方面的加载资源类的配置了:

# config/application.rb 中添加
config.redis = config_for(:redis)

# 在 config/initializer/load_redis.rb 中就可以使用了:
$redis = Redis.new(Rails.configuration.redis)

另外,如果资源类文件较多,也可以把所有的资源类的配置统一放在一个目录下,这样就更清晰了。

# config/application.rb

# 所有的资源配置放在 config/resources 下面
config.redis = config_for("resources/redis") 

这样就基本完成了,这样使用 yml + env 的方式的好处包括:

  1. 所有的 yml 都可以安全的 checkin 到 git 库中了,里面不再包含敏感信息;
  2. 一些非敏感的字段在 yml 中给开发工程师来集中管理,这样 env 变量就比较少了;
  3. 部署的时候线上只需要一个 .env 就可以搞定所有的资源类的配置了;比如 capistrano 只需要 link 这一个文件即可。

总结

虽然配置管理是一个小事情,但是随着项目越来越复杂,调用的第三方服务越来越多,如果不好好进行规划,配置比较混乱,同时也容易出现安全事故(安全无小事啊!)

总结一下:

  1. 应用配置,我们通过 config 这个 gem,统一存储在 settings.yml 中,通过 Settings 对象来调用。
  2. 资源配置,基本的资源配置统一存储在 config/resource.yml 中,通过 config_for 来加载;
  3. 资源配置,敏感类信息通过存储在 .env 文件中,在部署时由运维进行管理,dotenv 来加载到 ENV 变量中;

以上~欢迎讨论和吐槽

huacnlee 将本帖设为了精华贴。 01月16日 09:52

如果不需要管理后台修改配置信息的话,光用 Rails config_for 就够用了,没必要引入

我们公司现在都是通过 initializer 来生成一个常量,这样的坏处是社么呢?

如果后台需要更改配置的话 可以通过 config 类似的 gem 动态更新配置文件 但是通过 unicorn 部署的话多进程后台修改配置该如何做呢

#3 楼 @hiveer 其实也没啥问题,土方法而已。

用 YAML 文件的意义在于:

  • 文件都在一处;
  • 可以很容易像 database.yml 那样区分环境;

#6 楼 @huacnlee 嗯,是的哦。我看了这篇文章,然后再看看我们项目,我发现其实也没有很大必要需要改。用着也挺方便的,除了不清楚用常量可好。这样的话我就不去改了。

#7 楼 @hiveer 主要是敏感信息使用 Env 有一些好处,至于 Settings 还是自己写一个 initializer 确实区别不大

#2 楼 @huacnlee 敏感信息还是很有必要使用 ENV 来管理的。应用配置的话方法管理方式很多

变量我都是写在环境变量里面

👍 顶一下,实现有很多种方式,坚持使用更好的!

12 楼 已删除

完全同意楼主对配置的管理方式

还在想北京年后聚会分享这方面东西,我补充一点小技巧

有一些配置,比如 mailer 相关的,通常需要这样来配置

if production?
  config.action_mailer.delivery_method = :smtp
  # ...
elsif devlopment?
  config.action_mailer.delivery_method = :smtp
  # ...
end

用 RailsConfig 的话,还是要硬编码配置项

这里大致了解 Rails 下边各组件如何利用 Rails.application.config 的话,可以做这样一个 Monkey Patch(当然也可以换别的形式)

module ActionMailer
  class Railtie < Rails::Railtie
    initializer 'action_mailer.set_configs.set_yaml_configs', before: 'action_mailer.set_configs' do |app|
      app.paths.add 'config/mailer', with: 'config/mailer.yml'

      configure = app.config_for('mailer').deep_symbolize_keys
      configure.each do |key, value|
        setter = "#{key}="
        unless ActionMailer::Base.respond_to? setter
          raise "Can't set option `#{key}` to ActionMailer, make sure that options in config/mailer.yml are valid."
        end

        app.config.action_mailer.send(setter, value)
      end
    end
  end
end

注意用 Initializer 的话要了解下 Rails 在 Initializer 上使用的 TSort 算法,保证初始化 block 的执行顺序

然后编写 config/mailer.yml 即可

default: &default
  perform_caching: false
  raise_delivery_errors: false
  perform_deliveries: true
  delivery_method: :smtp
  deliver_later_queue_name: 'mailers'

development:
  <<: *default
  smtp_settings:
    # see https://github.com/sj26/mailcatcher
    address: 127.0.0.1
    port: 1025
    domain: localhost
  default_options:
    reply_to: [email protected]
    from: [email protected]

test:
  <<: *default
  delivery_method: :test

production:
  <<: *default
  smtp_settings:
    address: 
    domain: 
    port: 80
    user_name: 
    password: 
    authentication: :plain
  default_options:
    reply_to: 
    from: 

挖个坟~

应用配置需要:

  1. 不发布就能修改生效
  2. 提供后台编辑页面

大家都是咋做的

@zamia @jasl

42thcoder 回复

也许你需要的是 zookeeper

需要 登录 后方可回复, 如果你还没有账号请 注册新账号