こんにちは。DXチーム所属の@hrysdです。
この記事では、カラーミーショップのアプリケーションで利用している Ruby on Rails(以下 Rails)をバージョンアップした流れと、カラーミーショップ特有の大変だった点を紹介したいと思います。
この取り組みは昨年実施した Pepabo Tech Conference #13 において発表したものの続編・補強版だと思っていただければと思います。
おさらいと Rails アプリケーションのアップデートの方法
今回のアップデート対象のアプリケーションはざっくり以下の通りです。
- JSON を返す Web API を担当
- Rails のバージョンは 5.0 系
- 当時の
rake stats
の結果から一部抜粋Code LOC: 28664 Test LOC: 78891 Code to Test Ratio: 1:2.8
アップデートの流れ
当初の予定ではアップデート前のバージョンが 5.0 系であり、すでに EOL であったこともあり、一番近いセキュリティパッチの対象である5.2系へのアップデートを目標に作業をはじめましたが、最終的に目標のバージョンを 6.0 系に変更しました。これについては後続の内容で説明したいと思います。
各種 gem のアップデート、非推奨機能の修正、公式のガイドでも紹介されている方法を基本として進め、対応自体の長期化、差分の肥大を考慮し古いバージョンでも動作する修正は都度リリースしていくという方法をとりました。
また、 Rails アプリケーションの設定にはバージョン間の互換性が無いものが存在しているので、Rails::Application::Configuration#load_defaults
を使い利用しているバージョンのデフォルトを使いつつ、Configuration Rails Applicationsや、ソースコードを読み、非互換な設定等を明示的に無効にするという対応をしました。
Rails のアップデートには、例えば gem をアップデートする際は bundle update --conservative
使う等の細かいテクニックがあると思いますが、インターネットには参考になる情報がたくさんあるのでここでは割愛します。ぜひみなさんのベストプラクティスを教えてください。
アップデートでの苦労話
ここからは、実際に苦労した点・それに対しての対応を紹介したいと思います。
new_framework_defaults_x_x.rb が反映されない
これは rails app:update
実行後に生成されるファイルで、新規に追加された Rails の設定の制御を目的としています。このファイルでは Rails::Application::Configuration
を通して値を設定し、Rails に含まれる主要ライブラリ(例: Active Record)の初期化時に ActiveSupport.on_load
を用いて設定されます。
以下は、 新規に追加された new_framework_defaults_x_x.rb で設定し、それが実際に反映される流れのイメージです。
# config/initializers/new_framework_defaults_x_x.rb
Rails.application.config.belongs_to_required_by_default = false
# https://github.com/rails/rails/blob/v6.0.3.5/activerecord/lib/active_record/railtie.rb から一部抜粋
initializer "active_record.set_configs" do |app|
ActiveSupport.on_load(:active_record) do
configs = app.config.active_record
configs.each do |k, v|
send "#{k}=", v
end
end
end
設定した値はアプリケーションの初期化の流れで呼ばれるので、起動後は指定した値が設定されています。
しかし、ライブラリ側で require 'active_record'
のようにした場合はこの限りではありません。Rails が config/initializers/*.rb を読み込むより先に、require 'active_record'
されることで、タイミングがズレて、Rails::Application::Configuration
から値を読み込むより先に Active Record の初期化が終わってしまいます。
この原因を調べるには、どのライブラリが直接 require 'active_record'
を実行しているか調べる必要があります。今回は Kernel.require
を上書きし、require
が呼ばれたタイミングで標準出力に吐くという形で調査を行いましが、結果として修正しづらい箇所にあったため 、conifg/application.rb で Rails の設定を指定することで回避しました。
module App
class Application < Rails::Application
config.load_defauls 6.0
config.active_record.belongs_to_required_by_default = false
end
end
thiagopradi/octopus
対象のアプリケーションは複数データベースで読み書きを分ける(Read/Write Splitting)ためthiagopradi/octopusを採用していました。
READMEを読む限りメンテナンスモードとの記述はありましたが、Rails をアップデートしても動くであろうと考え作業を進めました。実際に 5.2 系で動かした結果はそうならず、私たちの環境では期待通りの動作をしませんでした。
しかし、これは Rails 6.0 系の目玉機能?の複数データベース対応で代替できると考え簡単なスパイクを行い代替できると判断しました。その結果、アップデートの目標を 6.0 系に変更しました。もし同様に困っている方がいたら思い切って Rails 6 にするのはいかかがでしょうか。
composite-primary-keys/composite_primar_keys
今回アップデートを実施するアプリケーションの特徴として、データベースで複合主キーを利用しているという点があります。
Rails 自体は複合主キーをサポートしていないため、カラーミーショップではcomposite-primary-keys/composite_primary_keysを使い複合主キーへの対応を実現しています。これは個人的な感想ですが、このライブラリは Acitve Record を大幅に拡張していて、バージョン間の差分を読み取る難易度が高いです。
このライブラリをアップデートするうえでの苦労した点は以下です。
- 出力される SQL が変わる
- パッチを書くのが難しい
一つ目の "出力される SQL が変わる" ですが、composite_primary_keys に限らず、Active Record のような ORM を利用した場合、抽象化されたクエリを扱うことになり、ライブラリの更新の前後で実際に発行される SQL が従来期待していたものから変化する可能性があります。実際に検証環境において動作確認を行った際に、新たにスロークエリが発生したため、日頃からこのような可能性を考慮しておく必要があると思います。
今回は composite_primary_keys の更新により SQL が変わったことを確認できたので、ActiveSupport::LogSubscriber と minitest-around を組み合わせ、テスト内で実行される SQL を収集し、composite_primary_keys のバージョン間でどこで発行される SQL が変わったのかバージョン間で比較を行いました。SQL を収集したと言いつつも単なる文字列でしかないため、比較の際には微妙に増えたスペースを消すといった泥臭いことを行い発行箇所を特定しました。以下はそのイメージです。
require 'minitest/around/unit'
module TestCase
def around(&block)
LogSubscriber.attach_to :active_record
yield
LogSubscriber.detach_from :active_record
end
end
ActiveSupport::TestCase.prepend TestCase
class LogSubscriber < ActiveRecord::LogSubscriber
IGNORE_EVENTS = ['SCHEMA', 'EXPLAIN']
IGNORE_QUERYIES = [/ar_internal_metadata/]
def sql(event)
payload = event.payload
return if payload[:name].nil?
return if IGNORE_EVENTS.include?(payload[:name])
return if IGNORE_QUERYIES.any? {|regex| payload[:sql].match?(regex) }
p sql
end
end
次に、二つ目の"パッチを書くのが難しい”です。ライブラリが期待通りの動きをしない場合、以下のようなモンキーパッチをアプリケーション側に用意することがあると思います。
if CompositePriaryKeys::VERSION !== N.N.N || Rails.version !== N.N.N
raise 'rails, compsite_primary_keys を更新したら #{__FILE__} を確認しよう!'
end
module CompositePrimaryKeys
# 省略....
モンキーパッチを書くこと自体は Ruby の柔軟性を発揮できるポイントであり、書くこと自体の楽しさもある(個人的な感想です)一方で、メンテナンスの観点では手間が増えます。こういったことを考えるとライブラリ自体に修正を加え、ライブラリ経由でその恩恵を受けるのがベストです。
今回のアップデートにおいてモンキーパッチを書く機会があったので、composite_primary_keys へパッチを還元することを試みましたが、実際には各種データベースで実行できる SQL にできなかったため断念しました。アップストリームへの還元という目的は果たせませんでしたが、個人的には composite_primary_keys の気持ちを知ることができたので無駄にはならなかったと思っています。composite_primary_keys に興味のある人、ぜひお話ししましょう!
最後に
この記事ではアップデートを通して、大変だった点・工夫してみた点などを紹介しました。
今回のようなアップデート作業はバージョンの飛び幅、影響範囲が共に大きくどうしても気分の乗らない作業である一方で、今回のアップデートの経験を生かし、次のバージョンを見据えた行動に継続的に取り組むことで、アップデート自体の難易度を軽減していけると考えています。
Ruby on Rails はフレームワークでもありながら、絶えず更新されており、その更新内容については単なる機能追加に止まらずセキュリティへの対応、パフォーマンスの向上と多岐に渡っています。 この恩恵を最大限に受け、フィードバックという形で貢献していけるのがフレームワーク、ひいては OSS と歩むアプリケーションの理想像なのではないでしょうか。
引き続き、内外に向け快適な環境を提供できるよう頑張っていきます。