Akka(Scala)で書かれたレガシーシステムをRailsでリプレイスした話

前置き

この記事では、AkkaとRails,もしくはScalaとRubyのどちらが優れている、などという話は出てきません。
結論を先にも書いておくと、

  • フレームワークや言語に関わらず、テストや仕様書、CIなどがいかに整っているかが保守性に関わる
  • 技術選定には、会社全体の状況も考慮に入れる必要がある

という実体験に基づく主張をしています。

また、ソフトウェア全体のフルリプレイスに成功した事例として、どのようにリプレイスを進めていったかも書いていくので、そちらも参考にしていただければと思います。

はじめに

イタンジのSREチームの田渕です!

今回は、SREチームで保守していた、あるレガシーなマイクロサービスをリプレイスしたときの話をします。
かなり大仕事でしたが、手際よく無障害で進めることができたので、どのようにレガシーシステムに立ち向かったかの話をします。

どんなサービスだったか

タイトルにある通り、Scalaで書かれたレガシーシステムでした。

レガシーという単語を明確に定義しておくと、レガシーコード改善ガイドという本から定義を借り、「テストがないこと」とします。

引き継いだときのシステムの大きな問題点を以下に羅列します。

  • テストが1行もない
  • 仕様書が多くない上に、書かれている部分も古くなっている
  • データベース設計が正規形になっておらず、EAV(Entity-Atribute-Value)が採用されている。
  • 必要以上に多くのサービスから依存されている

現状では、開発も落ち着いているサービスも多く、要件も定まりきっているので、自分が引き継いだタイミングは問題点を解決していくには良い機会でした。

方針

Scalaで健全な状態に直すよりも、Railsで書き直したほうが良いと判断しました。以下のような理由です。

  • データベース設計から大幅に変わるので、ほとんどの部分を書き換える必要があり、Scalaを修正することとRailsで書きなおすことの間に、コードの変更量にそれほど差がなさそう
  • 自分はRailsのほうが書き慣れているため、工数で換算するとRailsのほうが早く、かつ良い品質で作ることができそう
  • 会社としてRailsエンジニアの割合は多く、このサービスに依存するサービスは全てRailsのプロダクトであるため、Railsで書いたほうが将来の保守コストが下がる
  • サービスの内容的に、パフォーマンスは問題になり得ない

相談の上、他プロダクトと兼任しながら2ヶ月ほどの時間(1人月くらいの工数)を頂いて、Railsでフルリプレイスすることにしました。

仕様書の作成

まず仕様書がなかったため、コードから読み取れる範囲で可能な限り仕様書を作りました。

なお、元のシステムにテストは追加しませんでした。

より安全に倒すためには、旧システムにもE2Eテストを作るべきなのでしょうが、工数の関係と、プロダクトの規模の大きさを考えて、今回は見送りました。
リプレイスの際に、一部は仕様変更をする必要があったことも原因です。

その代わりといってはなんですが、仕様書作成に力を入れ、新システムのテストはカバレッジが100%になるまで書くことで安全性を高めました。

不要な依存関係の削除

仕様書を書きながら、システムの使われている部分を見ると、7つのサービスに依存されていました。
現状では排除できる依存がほとんどだったため、各プロダクトの担当者に依頼し、依存を減らしていただきました。

この時点では、旧システム自体には変更を加えていません。

全社を巻き込んでの作業になりましたが、結果的には3プロダクト以外からの依存は全て消すことができ、今後の作業がかなり安全に進められるようになりました。

仕様の調整とDB設計

残った3プロダクトの担当者と話し合い、仕様を調整しました。

元のDB設計は、EAV(Entity-Atribute-Value)が採用されていました。これにより、外部キーに空文字が入っているなどのレコードが防げていませんでした。

他サービスのコードやログ、テーブルを確認しながら、全ての矛盾したレコード100件ほどを手で直しました。
その上で、新しく作った第5正規形のデータベースに流し込むSQL文を準備しました。

実装

できる限り保守性が高く、引き継ぎも容易にするように、以下のように進めました。
いつか引き継ぐ時のことを考え、最大限自分に厳しくしました。

  • まずCI/CDを整え、「Yay! You're on Rails!」の画面の時点でデプロイをし、mergeされるたびにCircleCIからデプロイされるように設定した
  • rubocopを最大限厳しく設定し、CIで設定した
  • 仕様書を書き、OpenAPIでAPIドキュメントも作成し、各プロダクト担当者に共有した
  • 実装し、テストももれなく書いた(テストのカバレッジもCIで測定し、カバレッジが100%じゃないとマージできないようにした)
  • 完成後は、サービスをラップするprivate gemと、それを使ったサンプルを共有し、移行がスムーズに進むようにした

このように、仕様書・lint・CI/CD・テスト・ライブラリ・サンプルを全て整えた形でサービスを作りました。

f:id:ktabuchi:20200508180818j:plain
カバレッジ100%になると嬉しいですね

リプレイス結果

リプレイス作業は特に障害もなく、最初に決めた期限通りに終わらせることができました。
リプレイス後に半年程度運用しましたが、サービス内にバグは一つも見つかっていません。

リプレイス後は他の開発者がコードを参照するコストも下がり、PRも気楽に送ることができるようになり、主にRailsを書いている他の開発者にも感謝されました。(幸いなことに弊社ではRailsを書けないエンジニアはほぼいないため、逆に困るようになった例はありませんでした)

ScalaエンジニアよりRailsエンジニアの人数のほうが多いため、会社のリソースでは実現が難しかった要望もたくさんありましたが、徐々に機能が追加されています。

教訓

このリプレイスを経て得た教訓は以下のようなものでした。

  • 言語自体の優劣よりも、環境に適した言語がある(弊社の場合はRubyのほうが触りやすい人が多い)

保守性という点だけで考えても、型の有る馴染みの浅い言語より、型のない馴染みある言語のほうが勝っている場合があると感じました。

  • 言語やフレームワーク以前の問題として、テストや仕様書、CIなどがいかに整っているかが保守性に関わる

極論を言うと、どんな古い言語で書かれていようと、テストや仕様書、CIなどが整っていれば保守はしやすいし、どんなにモダンで言語機能が豊富な言語で書かれていようと、それらがなければ保守は非常に困難だなと感じました。

終わりに

今回はリファクタリングではなくリプレイスでしたが、仕様の確認→依存の整理→CI等を整える→テストを書きながら実装、という基本的なパターンで、非常にスムーズかつ安全に進められたかなと思います。

また、技術選定には、その技術自体のコミュニティだけではなく、会社全体の状況、例えば、「人事がその技術に特化したエンジニアを雇えるような候補者の母集団が形成できているか」とかにまで依存していることを学びました。

今後も、イタンジ全体として、会社のフェーズや会社のビジョンまで見据えた上で、betterな技術選定をしていきたいです!