SmartHR Tech Blog

SmartHR 開発者ブログ

8年の歴史を持つRailsアプリのRuby 3.1への道 〜そしてOSSコントリビュートへ〜

こんにちは。プロダクトエンジニアのkitazawaとqwyngです。 先日SmartHR基本機能のRubyバージョンを3.0から3.1にアップデートしました! SmartHR基本機能では開発をLeSSで行っていますが、Rubyのアップデートは開発チーム内の有志のメンバーで実施しています。 その際にいくつかあった問題とその解決方法について紹介しようと思います。

Ruby 3.1へのアップデートを開始

まずはじめにRuby 3.1でCIを実行してみました。キーワード引数の対応などが大変だった3.0のアップデートに比べると失敗しているテストは少なく、修正の時間はあまりかかりませんでした。 そのため、すべてのテストが成功するようになるまでは苦労することなくすんなりと進めることが出来ました。

最初の問題

CIは通るようになったので動作確認をするため、staging環境にデプロイしようとしました。が、ここで最初の問題が発生します。なんとエラーが出てデプロイに失敗してしまいました。

エラーの内容を確認すると以下のようなメッセージが出力されていました。

LoadError: cannot load such file -- matrix

CIでは通っていたのになぜかmatrix gemが見つかりません。調査していくと、以下のことが分かりました。

  • matrix gemはRuby 3.1からbundled gemsになった
  • prawn gemがmatrixに依存しているが、依存関係にmatrix gemを追加したバージョンが未リリース
  • 結果、matrix gemが存在せずLoadErrorとなった
  • CIではCapybaraがmatrix gemに依存していたため問題とならなかった

この問題に関しては、Gemfileにmatrix gemを追記することで回避しました。

OpenSSLの問題

matrix gemの問題はすぐに対応し、staging環境で動作確認をしたところ問題なさそうに見えました。が、とあるエンジニアのローカル開発環境で問題が発生しました。

SmartHRには電子申請という機能があり、署名のためにユーザーの電子証明書を使用するのですが、その証明書の読み込みに失敗している状態でした。staging環境では問題無く読み込めていたため、調べたところ以下の違いがあることが分かりました。

  • staging環境
irb> OpenSSL::OPENSSL_LIBRARY_VERSION
=> "OpenSSL 1.1.1f  31 Mar 2020"
irb> OpenSSL::VERSION 
=> "3.0.1"
  • エラーになったローカル環境
irb> OpenSSL::OPENSSL_LIBRARY_VERSION
=> "OpenSSL 3.1.0 14 Mar 2023"
irb> OpenSSL::VERSION 
=> "3.0.1"

staging環境ではリンクされているOpenSSLのバージョンが1.1.1tでしたが、ローカル開発環境ではバージョンが3.1.0になっていました。もともとOpenSSL 1.1系のEOLが近づいていたこともあり、OpenSSL自体のアップデートも検討していたので、一度revertして再検証をすることになりました。

まず、staging環境がOpenSSL 1.1系になっている件についてはコンテナのベースイメージがUbuntu 20.04であったためでした。OpenSSL 3.0を使用するためベースイメージをUbuntu 22.04にした所、ローカル環境で発生していた証明書の読み込みエラーが再現することが確認できました。

OpenSSL 3.0のマイグレーションガイドを確認したところ、いくつかのアルゴリズムがデフォルトで使用できなくなっていることがわかりました。そして、読み込めなかった証明書がデフォルトで使用できなくなったアルゴリズムを使っていたということも判明しました。 さらにガイドを読み進めるとデフォルトで使用できなくなったアルゴリズムを有効化する手段がいくつかありそうでした。

  1. 設定ファイルで有効化する
  2. provider apiを使って有効化する

ただ、調査時点ではRubyのopenssl gemで、2のprovider apiを使って有効化する機能がありませんでした。そのため、以下のようにlegacy providerを有効化したopenssl.cnfをリポジトリ内で管理して読み込むようにして解決しました。

# legacy providerを有効化する最低限の設定を抜粋
[provider_sect]
default = default_sect
legacy = legacy_sect

[default_sect]
activate = 1

[legacy_sect]
activate = 1

上記の対応をするとCIが無事通るようになり一安心しました。 しかし、問題はさらに発生します。今度はローカルで rails server を立ち上げた場合にうまく証明書が読み込めていませんでした…

openssl.cnfの読み込みには環境変数 OPENSSL_CONF を設定したのですが、 config/application.rb で設定していました。CIではここでも問題なかったのですが、 rails server の場合、 config/application.rb を読み込む前にopensslが参照されてしまうようで、設定ファイルの読み込みが間に合っていませんでした。 configの読み込み順を調べてみると config/boot.rb が一番早く読み込まれるようだったので、 OPENSSL_CONF の設定位置を変更してみた所 rails server でも証明書が読み込めるようになりました。やったー!

しかししかし、まだ問題は発生します。今度はstaging環境にデプロイして確認すると証明書が読み込めません。詳細までは調査しきれていないですが、 config/boot.rb を読み込むより前にアプリケーションサーバーがopensslを参照してしまっていたようです。ということで、コンテナの環境変数に OPENSSL_CONF を設定してあげることで無事staging環境でも証明書が読み込まれるようになりました。

その後、本番環境に無事リリースされ何事もなく元気に稼働しています。

openssl gemへのコントリビュート

SmartHR内での問題はcnfファイルの設定で解決しました。 しかし、「ruby/opensslにはproviderを設定する機能が無い」という問題が残っています。

前述の通り、providerを操作するAPIは存在していたのでruby/opensslにPRを出しました。 https://github.com/ruby/openssl/pull/635

最初は拙いコードでしたがメンテナの方からの沢山の助言を元に修正しmergeして頂きました。この場を借りて感謝を申し上げます。 リリースされればproviderをRubyコードで設定できるようになります。

OpenSSL::Cipher.new("RC4")
=> `initialize': unsupported (OpenSSL::Cipher::CipherError)
  from (irb):5:in `new'
  from (irb):5:in `<main>'
~~~~~ snip ~~~~~
OpenSSL::Provider.load("legacy")
=> #<OpenSSL::Provider name="legacy">
cipher = OpenSSL::Cipher.new("RC4")
=> #<OpenSSL::Cipher:0x0000000108eb3648>

SmartHRは各種OSSのおかげで成り立っているので、これからも色々な形で貢献して行きたいですね。

余談ですが、PR作成にあたりRubyの拡張ライブラリの作り方というドキュメントに助けられました。RubyのC拡張を作る時だけでなく、読む時にも役に立つドキュメントです。Rubyistにオススメです。

最後に

SmartHRの基本機能は歴史も長く大きなアプリケーションなので、コア部分のアップデートには様々な技術検証が必要になります。しかし、心強いメンバーと一緒に難しい課題を解決していくことが出来る現場でもあります!興味のある方はぜひカジュアル面談でお会いしましょう!
ぜひ下記サイトからご応募ください! hello-world.smarthr.co.jp