こんにちは、クラウド経費 テックリードの宮村(みやむー) @miyamura.koyo です。
以前 Ruby 3.1 へのアップデートを行いましたが、引き続き Ruby 3.2 へアップデートを行いました!本記事ではその内容を紹介しようと思います。
※ なお 3.3 以降も順次アップデートした内容を記事にしてお届けする予定です。
今回も長年運用されているアプリケーション特有の課題を解決する場面がありましたので、こちらで紹介できればと思います!
方針
まず方針として、以前と同様に本番に細かくリリースすることを意識しました。
クラウド経費・債務支払は多数のユーザーが使っており、企業の業務にも密接に関わっているため Ruby のアップデートに伴ってこれらの開発に影響を与えることは避ける必要があります。
また、活発に機能開発やバグ修正が行われているので、できるだけそれらに影響を及ぼさないように作業する方針としました。
具体的には、できるだけ本番に細かくリリースしたり、細かくアップデート記事を書いたり周知したりですね。
やったこと① CI の仕組みを抜本的に変える
この作業は Ruby アップデートそのものとは関係ないのですが、Ruby バージョンアップに伴い CI の仕組みを抜本的に変える必要がありました。
作業前は CI の仕組みが古く、他のプロダクトも巻き込むため、変更コストが高いことによって Ruby 3.2 の CI イメージが作成できない状況になっていました。
また、AWS ECR ではない別のコンテナレジストリからイメージを Pull していたり、CI 環境は本番とは別の Linux OS を利用しているため細かい機能が一致していなかったり、CI イメージを各プロダクト個別にカスタマイズすることが難しい状況になっていたりと、以前から課題がある状態でした。
なのでこれを機に CI の仕組みを抜本的に変更しました。作業内容としてはざっくり以下のことを実施しています。
- CI イメージ用のコンテナイメージを設計
- CI イメージの定期更新内容を SRE チームと連携しながら設計
- 既存の CI イメージを一つずつ置き換え
- OS のディストリビューションが変わったため一部 CI が落ちるようになったので CircleCI config やらなんやらを全体的に修正
- 開発チーム向けに CI の仕組み変更に伴うドキュメントを執筆・周知
- 英語で執筆しつつ、英語話者の開発者の質問にも対応
これ自体が一つのプロジェクトになるくらい大規模になり、他の業務と並行しつつではありますが、1ヶ月近くでやり切ることができました!
とはいえ今後の Ruby アップデートやベースイメージに変更を加えないといけない場面で必須になる対応だったので、全体の開発生産性のために先んじて実施できて良かったです。
やったこと② Kernel#=~
削除に伴う古い gem のエラー対応
...ということでやっと CI が回せるようになったので Ruby 3.2 にアップデートして CI を実行しました。
その結果、まず一部古い gem でエラーになりました。例えば Pry gem v0.13.1 では以下のようなエラーが発生しています。
rails aborted! NameError: undefined method =~' for class Pry::Code' (NameError)
これは Ruby 3.2 のリリース情報にも記載されているように、 Kernel#=~
が削除されているためでした。
以下の非推奨のメソッドは削除されました -
Kernel#=~
Ruby 3.1 までは Integer#=~
などでは nil が返却されていたのですが、3.2 以降ではエラーになっています。
# Ruby 3.1 irb(main):001> 1 =~ "a" => nil # Ruby 3.2 irb(main):001> 1 =~ "a" (irb):1:in `<main>': undefined method `=~' for 1:Integer (NoMethodError) from /Users/miyamura.koyo/.rbenv/versions/3.2.8/lib/ruby/gems/3.2.0/gems/irb-1.15.2/exe/irb:9:in `<top (required)>' from /Users/miyamura.koyo/.rbenv/versions/3.2.8/bin/irb:25:in `load' from /Users/miyamura.koyo/.rbenv/versions/3.2.8/bin/irb:25:in `<main>'
Pry gem の場合は最新にアップデートすれば解決しました。
また、クラウド経費・債務支払で引っかかった事例ではありませんが、他のプロダクトで問題になりそうなので共有です。Web サーバーである Unicorn には以下のように Kernel#=~
に依存している挙動があります。
if value =~ /\n/ ...
これはアプリケーションから指定された HTTP レスポンスヘッダーを書き込んでいる箇所なのですが value
には String
以外を指定することができます(するべきではないですが、以下のように指定することは可能です)。
response.headers['Number'] = 1
Ruby のアップデート後 Unicorn でエラーになった場合は上記を思い出してみてください。
なお、エラーが発生した際には、以下のいずれかの方法で解消できる可能性がありますので、参考にしてみてください。
- 1 master ブランチ では
=~
が使われてなさそうなので master ブランチの unicorn を使うようにする。 - 2
value =~ /\n/
の箇所だけモンキーパッチを行う- 例えば
value.respond_to?(:=~) && value =~ /\n/
のような形に変更する
- 例えば
やったこと③ シンボリックリンクを削除する
Ruby のバージョンアップに伴い、ベースイメージの変更をしました。これにより、コンテナに含まれる OS の内容も変更がありました。
具体的には OpenSSL の周りで以下のように記載している部分がありましたが、それが不要になりました。
ln -s /etc/ssl/certs/ca-certificates.crt /usr/lib/ssl/cert.pem
GitLab でも同様の変更を行なっていますね。
同じようなエラーが出ている方は上記を参考にしてみてください。
やったこと④ E2E テストが通らない謎と戦う
CI で単体テストが通過したので、E2E テストを実施しました。
単体テストでもほぼ問題なかったので、E2E テストもすんなり通過するか...と思いきや問題発生!
アプリケーションが不定期に 502 エラーを返す
E2E テストを回すと Ruby バージョンアップ後に不定期に 502 エラーを返すということがわかりました。
はじめは E2E テストが不安定なせいかと疑っていたのですが、何回回しても同じ結果になります。流石に「これはおかしいぞ...」と思い調査を始めました。
しかし LB / アプリケーションサーバー / Rails ログ など様々なログを調べましたがどこにもエラーが出ていない……。
数十回 E2E テストを回したり、他のエンジニアに相談したりして試行錯誤を積み重ねた結果、原因がわかりました。
ps コマンドがない
なんとアプリケーションが不定期に 502 エラーを返す原因は ps
コマンドがなくなったことでした。Ruby のバージョンアップに伴い、ベースイメージを差し替えたところ、一部 OS コマンドが削除されてしまっていました。
そのうちの一つが ps
コマンドです。
Ruby 3.1 のベースイメージだと ps
コマンドが同梱された procps
パッケージが入っていますが
Ruby 3.2 だとありません……(ガーン!)。
アプリケーション Pod の死活監視が ps コマンドに依存していた
では ps
コマンドがないことでなぜ 502 エラーが不定期に発生したのでしょうか?それは、Pod
の死活監視が ps
コマンドに間接的に依存していたからでした(クラウド経費・債務支払では k8s
を用いています)。
これにより ps
コマンドがなくなったことで、Pod
が正常に動作しているにも関わらず、Pod
に異常が発生した扱いになっていて、頻繁に再起動を繰り返し 502 エラーを返していたというカラクリです。
これまで暗黙的にインストールされていたコマンドに依存していたということですね……。
コンテナイメージに明示的に procps
コマンドをインストールしたところ、無事問題が解消しました。
こちら1〜2週間を費やしましたが、リリース前に何とか解決できてよかったです。
これを放置してリリースすると、正常な Pod
が頻繁に再起動を繰り返し、アプリケーションが不安定になり障害が発生し、最悪のケースではクラスタ障害に繋がっていたかもしれません。ファインプレーでした。
リリース後
無事リリース!を行いました。
事前に綿密に調査を行い、品質を担保したことで、本番環境では Ruby バージョンアップを起因とした障害は起きていません(やったね)。
リリースしたことで Ruby の新しい機能が使えるようになったので色々試してみました。
IRB.conf[:USE_AUTOCOMPLETE] = false
を削除した
Ruby 3.2 から環境変数 IRB_USE_AUTOCOMPLETE
で irb
の補完の有無を設定できるようになり、なおかつ Rails 7.0.5 以降は Rails.env.production?
で自動的に false
が設定されるようになりました。
https://st0012.dev/whats-new-in-ruby-3-2-irb#heading-new-configurationsst0012.dev
.irbrc
に IRB.conf[:USE_AUTOCOMPLETE] = false
を設定するなどして、上記の挙動を自前で設定していた箇所があったので、アップデートを機に削除しました。
Data クラスを使ってリファクタリングを行った
Ruby 3.2 から Data クラスが導入されました。値オブジェクト(value object)など、イミュータブルなクラスを宣言したい際に簡潔に行える機能です。
私が以前実装した機能で、Data クラスを用いてリファクタリングできる箇所があったので、やってみました!
リファクタ後はかなり見通しが良くなり、コードの意図も明確になりました。また、こちらの実装は全社のエンジニアや同じプロダクトのエンジニアにもサンプルとして共有しました。
せっかくの新機能、どんどん活用していきたいと思います!
まとめ
10年もののプロダクトである、クラウド経費・債務支払の Ruby バージョンアップを行いました。
ユーザーに不利益を及ぼさないようにしつつ Ruby をアップデートするには、綿密な計画や検証が必要だと学んだプロジェクトとなりました。
やるたびに思ってもみない問題が発生する Ruby バージョンアップですが、常になるべく新しい環境で開発し、ユーザーの皆様に価値を届けられるように今後もやっていきたいと思います!
マネーフォワード福岡開発拠点では、多くのユーザーが使っているプロダクトの Ruby バージョンアップをしたいエンジニアを募集しています!
福岡開発拠点のサイトもあるのでぜひみてください!