freeeの開発情報ポータルサイト

8年以上開発されているRailsプロダクトーーfreee会計をRails 6にするまで

こんにちは、freee会計でエンジニアをしている @sakakibara-setu です。 普段は債権債務に関する機能を担当するチームに所属して開発を行っていますが、この度freee会計のRailsアップデートを担当することになりました。

実はfreee会計は、先日2021年12月にRails 5系からRails 6系へとメジャーアップデートされました。 ありがたいことにこのメジャーアップデートによる問題は一件も発生しなかったため、皆様には特にお変わりなくご利用いただけたかと思います。 その上で社内の開発環境においては様々な恩恵を得ることができたので、結果は成功と言っていいと思います。

しかしながら、その道のりはお世辞にもうまくいったことばかりではなく、反省すべきことも多々ありました。 アップデート作業には壁とも言えるような問題がいくつもありましたが、それはfreee会計が8年以上開発されているRailsプロダクトである、ということも要因のひとつだと言えるでしょう。 本記事では、8年以上開発されているRailsプロダクトであるfreee会計をRails 6にメジャーアップデートするにあたり、ぶつかった壁についていくつかピックアップして紹介していきたいと思います。

目次

開発メンバー

2018年新卒として入社した自分と、2021年新卒の@ukiくんという新卒出身の2人で主に実施しました。 もちろん2人だけで全ての作業を実施したわけではなく、開発中には影響する機能に関係するエンジニア、リリース前にはQAメンバーなど、多くのメンバーの力を借りています。

作業の全容

今回のRailsアップデートの大まかな作業は以下の手順で行われました。

  1. CHANGE LOG の調査
  2. gem update & 不要なgem削除
  3. 設定ファイルの更新
  4. ローカル環境における rails s を通す
  5. rspec を通す
  6. その他リリース前の準備
    1. DEPRECATION WARNING 対応
    2. パフォーマンスチューニング
  7. QA
  8. リリース & 社内告知

社内ではちょうど、他のRails 5系のプロダクトもRails 6へアップデートするプロジェクトが進んでいるものもありました。 しかし、このプロジェクト開始時に完了しているものはなかったため、Rails 5系のときのアップデートは参考にした以外は知見のない中で実施することになりました。

作業はfeatureブランチを切り、そのブランチに対してアップデートのための変更を加えていくことで実施しました。 一方で、不要なgemの削除や非推奨メソッドの置換など、一部の先出しできる変更についてはdevelopブランチにマージするようにしました。 featureブランチの差分が大きくなりすぎないようにするためです。

QAといった、後にあるテスト工程のことを考えれば、この差分は少なければ少ないほど、問題が起きた時に原因を特定しやすくリスクを減らすことができます。そうでなくともconflictに晒されることが減り、開発がしやすくなります。 ですので理想を言えば、Rails 6にアップデートする差分は可能な限り少なくするべきなのでしょうが、そうはいかないこともありました。

これらの手順について、具体的にどう行った作業が必要だったかを、それぞれ解説します。

1. CHANGE LOGの調査

結論から言えば、こちらの手順はあまり重要ではありませんでした。

メジャーアップデートということもあり、Rails 5 -> 6の間には破壊的な変更も多く含まれます。 そこで以下のRailsのリリースノートにある今回対象となるバージョン間のCHANGE LOGを、事前にすべて調査しました。 確認したのはどのような変更か、それはfreee会計に影響があるか(対応が必要か)という点です。

CHANGE LOG: https://github.com/rails/rails/releases

ただ問題だったのは、CHANGE LOGはとても膨大だったということ、そうであってもすべてのコミットを網羅しているわけではないということです。 またコミットについてはコードの変更を一文で解説してくれています。例えば破壊的な変更には 'Remove', 'Deprecate' といった単語が登場します。 しかし、その内容はfreee会計への影響の大きさを意味しません。 Railsにとっては単なる 'Bug Fix' であっても、freee会計にとっては正常に動作しなくなるような変更だったこともありました。 ひとつひとつの変更は些細なこともあり、コードまで確認すればすぐに判断できるようになる、というものでもなかったと思います。(そもそも変更量は膨大であり、コードまですべて確認するのはあまり現実的ではありません)

つまりCHANGE LOGだけでfreee会計への影響を把握することは難しいということです。(当然といえば当然ですが...) CHANGE LOGを元に対応の必要性をリストアップしましたが、掛けた労力の割に、結局あまり参照することはありませんでした...。

よって、事前の準備としてはRailsアップグレードガイドの「削除されたもの」あたりと眺めておくくらいで十分なのではないかと思います。 まずは、「えいや」くらいの気分で手元でアップデートしてみて、実際に落ちたものを対処するというのが結局労力としては最小限になりそうです。 そして後述しますが、テストカバレッジが一定量ありQAも行うfreee会計にとっては、それは正しいと言えそうでした。

Railsアップグレードガイド: https://railsguides.jp/6_0_release_notes.html

2. gem update

次にしたのはGemfileの更新です。 Railsアップグレードガイドを参考に、事前に当時のRails 5系の最新であったRails 5.2.6にアップデートしてから、以下の流れで行いました。

  1. Gemfileファイル内のRailsバージョン番号を6系に変更し、 bundle update を実行
  2. version依存があるgemがあればエラーが出るので、先にそちらのgemを gem update [GEMNAME] してから 1. の手順を再実行
  3. エラーがなくなるまでupdateを繰り返す

ほとんどのgemについては上記の流れでアップデートできますが、一部追加で作業が必要なものがありました。

2-1. 依存する社内gemのアップデート

freeeでは複数プロダクトに渡って共通する機能を社内gemとして切り出すことがあります。 例えば電子申告を行う機能は、freee会計とfreee申告で共通しているため、ひとつのgemとして切り出して利用しています。 その社内gemにRails 5系に依存したコードがある場合、先にその対応をする必要があります。

具体的な手順としては、まず対応する変更を取り込み、その変更を含んだバージョンを各種gemで新たに切ります。そしてそのバージョンをfreee会計のGemfileに指定してgem updateしました。 ただし複数プロダクトに渡って利用されているgemの場合は、そのような変更を行ったことを関係者にも共有してから実施しなければなりませんでした。

2-2. 社内gemから標準機能への乗り換え

社内gemのひとつに、データベースの読込先をmasterとslaveに切り替えやすくするものがあります。 これはRails 5以前において複数DBを利用するためのものでしたが、Rails 6から標準にある機能で複数DBに対応できるようになったため、今後はそちらを利用するようにし、アップデートではなく削除することにしました。

削除にあたっては、その過程で新たにgemの利用箇所が生まれてしまうことがないよう、以下のようなwrapper methodを用意し、事前にgemの利用箇所を置換しました。 以後社内エンジニアにこのmethodを利用してもらうようにすることで、Railsのバージョンを意識せずに開発してもらうことができますし、Rails 6になれば自動的に標準機能を利用することができます。

def connected_to_reader
  if ::ActiveRecord.gem_version < ::Gem::Version.new('6.0')
    # 社内gemの利用
  else
    # Rails 6標準機能の利用
  end
end

2-3. CIで使うMySQLイメージの更新

freee会計ではGitHub上でPRを作成すると、その差分を含めたCIを実行する仕組みになっています。 その高速化の一貫として、CIで使うMySQLイメージに事前にdumpしたものを利用していました。

またこのとき実行するテストの中で、テストごとにDBをリセットするため、 database cleaner というgemを利用しています。 このgemにおいて、リセットの方式がアップデートによって変更されたため、MySQLイメージをdumpし直す必要がありました。

2-4. モバイルでエラーになってしまうgem

jBuilder というjson形式を扱うgemがあります。 このgemのアップデートはweb版には影響がなかったのですが、一部のレスポンスに差異が生じたため、iOSアプリ版でエラーになってしまうことが発覚しました。 この問題を解決するにはiOSアプリを強制アップデートする必要がありますが、ユーザ体験としてよくはないため、なるべく避けたいというのがfreeeの方針でした。 幸い、こちらのアップデートはRails 6時点では警告こそ出るものの必須ではなかったため、モバイルチームと協議し、1年ほどのスパンをかけて徐々にアップデートを促していく方針をとりました。

2-5. 不要なgemの削除

セキュリティ的に問題のあるgemについては別に監視する仕組みがあり、そちらで適宜アップデートしているため、問題にはなりません。 一方で、そうではない、例えば、かつて導入はしてみたものの結局利用されていないものや、利用しているコードは削除済みだが巻き戻せるようにgemだけ残しているといった、不要なgemがいくつか存在していました。 ちょうどいい機会ということもあり、削除しました。

実施してみて改めて、freee会計というプロダクトは社内の他プロダクトとの関係が非常に深いことがわかりました。 利用していないgem ひとつとっても、どこかで利用している可能性もあるため、ひとつひとつ確認し、合意を取りつつ進めました。 freeeではエンジニアのコミュニケーションにはslackを用いており、developer全員が集まるチャンネルもあるのでそこで告知や質問ができます。 またあえて共有という文化から社内docはほぼすべてGoogle Driveから検索できるため、当事者がすでに社内にいない場合にはその資料を漁って背景を確認しました。(さすがに昔すぎると資料そのものがありませんので、そこは検証が必要でしたが...)

結果として、対応したgemはそれぞれ以下の通りでした。

  • gem updateでアップデートしたgem:9件
  • gem update以外に作業が必要だったgem:5件
  • 利用していないため削除したgem:7件

※ Gemfileに記載のものだけカウントしています

3. 設定ファイルの更新

config/application.rb の設定を見直します。

ここで発見したのですが、freee会計では load_defaults の設定がされておらず、どのような設定がされているかすぐには判断できない状態でした。 よってこちらを6系に設定したのち、これまでのバージョンで追加されていった設定を確認し、現状のfreee会計に合ったものを選択しました。

なるべくRailsの更新に沿った設定にしたいところですが、いくつかそうはいかないことがありました。 例えば Zeitwerk の利用です。 Zeitwerk はRails 6.0で新たに導入された定数の自動読み込みの仕組みです。 ただしこちらを利用するには、自動読み込みパスの下にあるファイル名が Zeitwerk のドキュメント に記載されているとおりに定義された定数と一致しなければなりません。 freee会計にはそうなっていないものがあり、またその対応には時間がかかりそうだったため、今回は対応を見送りました。

4. rails s を通す

次にしたことは、ローカル環境で rails s してfreee会計を立ち上げ、ログインできるようにすることです。 ここでもいくつものエラーが出ましたが、対応した中でも大きかったものを載せます。

  • constraintsの値をstringで読み取ろうとする問題

Rails のactionpackにおいて、いままでそのまま参照していた部分が Regexpクラスで読み取られるようになりました 。 結果として、現状integerでconstraintsの値を記述していた部分がtypeエラーで落ちていました。

  • whereでid指定できなくなる問題

whereやfindでid指定すると、発行されるクエリが常に id = NULL に上書きされてしまうという問題でした。 原因は rails_42_patch.rb というpatchでした。 今回Rails 6側で Integer classの ensure_in_range が値を返すようになる変更が入ったのですが、このpatchはその ensure_in_range をoverrideしており、値を返さないままの状態が維持されてしまっていたことが原因でした。

これはファイル名の通り、かつてRails 4 -> 5に上げた時に作成されたmonkey patchです。 関連先のカラムが unsigned int の場合、関連先のカラムが signed int であっても、マイナス値を入れるとエラーになってしまいます。 freee会計では、仮のレコードを id: -1 と置いて利用しているものがあり、それを前提としたコードが多いため、unsigned int の場合でも signed int 相当として扱うようにoverrideしていました。

このmonkey patchについては、完全にfreee会計独自の問題であり、類似の問題が見つからないのでなかなかの手間でした。 これ以外にもfreee会計は歴史的経緯から、当時必要な仕組みを実装するためのmonkey patchがいくつか存在しています。 個人的にそれによる修正は見落としがちになるので、注意しておくとよさそうだと思いました。

ただこれはmonkey patchをやめるべき、ということではありません。どうしても必要であればpatchを追加してでも対応すべきことはあります。 ただいつかはそれを削除する時はきますので、そのときのために「なぜこのpatchを追加する必要があったのか」「どういう状況なら削除できるか」を判断できる情報は記載しておいた方がよさそうです。

5. rspec を通す

続いてrspecを通すことを目指しました。 freee会計では6万件ほどのテストがrspecで実装されていますが、2000件ほどが落ちました。 CHANGE LOGにちゃんと記載されているものが原因で落ちているものよりも、そうでないものによって落ちているケースの方が多かった印象です。 また「社内gemから標準機能への乗り換え」で述べたように、標準機能を採用するようになったことで、これまでとテスト環境における挙動が変わり、落ちてしまっているケースもありました。

ただ非常に良かった点は、rspecさえ通してしまえば、それ以降freee会計の基本動作において大きな問題はほぼ起きませんでした。 freee会計におけるrspecのカバレッジは70%ほどあり、これに加えてE2Eテストなども行なっているため、全体でかなりのカバレッジを誇っていると考えて良さそうでした。

作業がconflictしないよう、事前に共通した原因で落ちていそうなものを分類してから、チームメンバー全員で手分けして調査・修正していきました。 対応した内容のうち一部を載せます。

  • MySQL8.0にともないメタデータを取得する table が key_column_usage -> statistics に変わった(link
  • renderformats 指定は symbol でするようになった(link
  • ActiveRecord::ConnectionAdapters::Transaction から joinablesetter が削除された(link
  • ActiveRecord::Core の中で Thread.current.thread_variable_get("ar_connection_handler") されるようになった(link
  • ActiveRecord::Core#blank? されたときは必ず false を返すようになった(link

落ちる原因を調査する時は、以下のようにして、まず関係ありそうなコードのバージョン間に差分があるかを確認するのが個人的には効率がよかったです。 https://github.com/rails/rails/compare/[VERSION]...[VERSION]

また、上記のような修正を通して、これまであまりよくない書き方をしているところを発見し、リファクタリングすることもできました。 以下はその一例です。

  • Thread.current ではなく RequestStore を使う

今回発見した理由としては、rspecの中で allow(Thread.current).to receive(:thread_variable_get).with のようにしていました。 allowwith が指定されている場合、それ以外の引数が入ってくるとエラーになります。 Rails 6より、ActiveRecord::Core の中で Thread.current.thread_variable_get("ar_connection_handler") されるようになったのでエラーになっていました。

こういうことがあるため、 Thread.current をstubすること自体よくありませんが、そもそも Thread.current 自体、グローバルにアクセスできてしまう上に、本当の意味でのリクエスト毎キャッシュでは無いのでサーバー構成や処理系によってはリセットされない可能性もあるため、多用したくはありません。 そこでfreee会計では、扱う値をクラスで明示的にして、Thread.currentを隠蔽して誤用を防ぐようにした、request_stores というレイヤを用意しています。 裏側の仕組みには steveklabnik/request_store を使っています。 Rackのレイヤでリクエスト毎に Thread.current をリセットしてくれるので、本当のリクエスト毎キャッシュとして機能することができます。

この機会に移行されきっていなかった Thread.current をすべて RequestStore に置き換えました。

6. その他リリース前の準備

ここまでの作業が完了すればリリースは近いです。 リリースの質をさらに高めるためにいくつか作業や工夫をしていきます。

6-1. DEPRECATION WARNING を直す

あくまでも警告なので必須というわけではありません。 ただ今回の場合、あまりに多すぎてrspecを動かした時などのログが、普通に読むのに支障が出るくらいDEPRECATION WARNINGが表示されてしまったため、特に出ているものを対応しました。

6-2. パフォーマンスチューニング

IN句に大量の id を指定した時に分割してくれる機能が Rails 6.0 でなくなるので、id が大量に指定されうるコードは分割して実行するなどの対応が必要でした。 例えばfreee会計では会計帳簿をはじめとする複数のレポートを確認することができます。これらレポートではこれまでに登録された大量の取引を扱うため、内部的に何十万何百万のデータを扱うことはよくあります。 事前に ActiveRecord::Associations::PreloaderExtension#load_records に対して以下のようなmonkey_patchを仕込んでおき、一定以上の id が指定された時に通知するような仕組みを仕込んでおきます。 ここで発見されたものに対して、分割処理を実装しました。

module ActiveRecord::Associations
  module PreloaderExtension
    def load_records(&block)
      notify(owner_keys.length, model.name) if owner_keys && owner_keys.length > notify_threashold
      super(&block)
    end
end

7. QA

freeeでは大きな機能リリースをする際は、QAチームがテストします。 基本的には各プロダクトごとに担当のQAチームが存在するのですが、今回のRails アップデートはfreee会計全体、すなわちfreee会計とやりとりするすべてのプロダクトに影響があるため、他プロダクトのQAチームにも協力を依頼しました。

QAではDBアクセスが必要な動作を中心に、約3週間ほどの期間で、基本的な動作を確認しました。 freeeではQAなどのために、本番に近い環境をいくつか用意しており、そのひとつにfreatureブランチをデプロイしてテストしました。 余談ですが、Gemfileにおいて、 group :production, :staging にあるものはローカルで実行されないため、テスト環境にデプロイする段階になってインストールされ、内部的にRails 6に対応しておらず落ちた、というものがありました。

8. リリース & 社内告知

QAが完了すればいよいよリリースです。 freee会計では通常1日に2回の定時デプロイを行なっています。 ただ今回のリリースにおいては万が一問題が発生した時に巻き戻しがしやすいよう、他のデプロイを一旦ストップして実施しました。 同様の理由で、デプロイが完了するまで、他のエンジニアがローカルのdevelopブランチを取り込むことやfeatureブランチにマージすることも禁止しておきます。

無事リリースできた後は、変更に伴ってローカル環境でいくつかの作業が必要になるため、freee会計をローカルで利用しているエンジニアに向けて告知を行います。 freee会計は関連するプロダクトが多いため、社内のほぼすべてのエンジニアが対象となりました。

告知は以下のような内容です。

  • ローカル環境への最新 develop ブランチの取り込み
  • bundle install の実行
  • 現時点でマージしていないPRやfeatureブランチがある場合、Rails 6でも動作するか確認するため最新の develop ブランチをマージし直すこと

また本記事のような変更内容をまとめた記事を社内向けに公開しました。

参考文献

rails/rails: Ruby on Rails

Ruby on Rails 6.0 リリースノート - Railsガイド

おわりに

Rails 6アップデートは、freee会計の過去からの負債に振り回されたものの、最終的に積み重ねてきたものに助けられるそんなプロジェクトでした。 この記事はすべて完了してから書いているので、先回りしている内容もありますが、もし現在の知識を持ったまま、アップデートする前に戻ったことを考えると、特に以下の部分は気をつけたいところです。

  • CHANGE LOGの調査はそこそこにして手元でえいやで動かす
    • 結局事前に調査しても、freee会計への影響を完璧に割り出すことは難しい
    • QA, rspec, E2Eを信頼してリリースまでの期間をなるべく早くすべき
    • それが結局他の開発ともかちあわず、問題が起きにくいことにつながる
  • feature branchの差分が大きくなりすぎないようにする
    • すべての変更がRails 6に上げるのと同時にする必要はなく、developブランチにマージできるものは先に切り分けてリリースする
    • 問題が発生した時の原因の切り分けがしやすくなり、リスクが下がる
    • もちろん標準機能への切り替えといった完全な切り離しが難しい作業もあるが、 Zeitwerk 見送りのように一旦はリリースを目指して保留することも必要

ちなみに、Rails 6にアップデートした恩恵として、ローカル環境で rails s した時の立ち上がりが早くなったりしました。 これはdevelop環境で eager_load がなくなったことによるものです。

Rails 6にはアップデートされましたが、Rails 7もすでにリリースされていますので、今後も対応を続けていきます。 よりスピード感を持って新機能を提供できるよう、今後も努めてまいりますのでご期待ください。

また今回のようなプロジェクト以外にも、新機能開発など、freeeをよりよくするために一緒に働く仲間を募集しています。 ぜひ一緒に8年以上続くRailsプロダクトをRails 7に上げたり、リファクタリングしましょう!

jobs.freee.co.jp