SMARTCAMP Engineer Blog

スマートキャンプ株式会社(SMARTCAMP Co., Ltd.)のエンジニアブログです。業務で取り入れた新しい技術や試行錯誤を知見として共有していきます。

Ruby 2.7に飽きたから秋田からRuby 3移行した話

Ruby のロゴについて

自己紹介

2023年1月1日付け入社のはかまたです。

BOXILカンパニープロダクト本部配属でBOXIL SaaSの開発エンジニアとして働いています。

スマートキャンプはニックネーム文化があり、私は「職人(しょくにん)」になりました。 (GitHubのアカウント名を寿司職人にしていたらそうなった・・・)

最初は職人と呼ばれることに若干の抵抗がありましたが、不思議なもので今ではもう呼ばれ慣れています。

職人に見合った仕事を全うできるように日々奮闘中です。

私は秋田県からフルリモートで働いています。 最初は戸惑いながらも徐々に環境を整えたり、他の社員の方々とたくさんコミュニケーションをとってようやく慣れてきた気がします。

スマートキャンプは社員同士のコミュニケーションが本当に活発です。 あたたかく受け入れてくださった皆さまには本当に感謝しています。

Ruby 3への移行

タイトルが「Ruby 2.7に飽きたから秋田からRuby 3移行した話」となっていますが、 もちろん私1人で対応したわけではなく、BOXIL SaaS開発メンバー全員で対応しました。 (ただ私自身が秋田での勤務のため、飽きたと秋田をかけたかっただけです・・・)

BOXIL SaaSはこれまでRuby 2.7を使用していましたが、 2023年3月31日でEOLを迎えるため、Ruby 3に移行する必要がありました。

しかし、Ruby 3にバージョンアップするには依存ライブラリのバージョンも上げていく作業が必要でした。

脱Refile

BOXIL SaaSでは資料や画像のアップロードにRefileというgemが使われていました。 しかしRefileはもうメンテナンスがされておらず、Ruby 3に対応していません。

https://github.com/refile/refile 最終コミットが3年ほど前になっています。

そのため、Refileを剥がし、Shrineというgemに移行する必要がありました。

過去の先駆者

幸いなこと?に脱Refile、Shrineへの移行の作業はすでにGitHub上のPRにありました。 そのPRはRefileとShrineのデータを同期するところまでは完了しているようでしたが、それ以降音沙汰がなさそうな状況でした。 PRは2020年に作られていたようです。

何があってこのPRが放置されてしまったのか、既存の開発メンバーに経緯を確認してみたところ、 もともとこのPRは開発体験向上のためのタスクとして、取り組んでいたそうです。 ですが、当時取り組んでいた別のタスクの方が優先度が高く、その作業をしているうちに月日が流れ、開発メンバーも入れ替わりました。 その結果、当時対応していた開発メンバーもいなくなり、対応し難い状況から放置されてしまったということらしいです。

私たちはこれらの蓋を開けていくことになりました。

開戦

BOXIL SaaSはSaaSを導入したいユーザーとSaaSを提供しているベンダーをつなぐリボンモデルのプロダクトです。 そのため、SaaSのサービスロゴやSaaSに関する資料や画像のデータが多く存在します。

BOXIL SaaSのどこにRefileが使われているのか確認していくと

  • サービスのロゴ
  • サービスの資料
  • サービスのスクリーンショット
  • ホワイトペーパー
  • ライト会員用資料
  • 会社のロゴ
  • プロフィール画像

のアップロードと表示に使われていることがわかりました。

ありがたいことにShrineの公式サイトには Refile移行のためのドキュメントが整備されています。 これを元に移行を進めていくことになりました。

Shrineは1つのモデルに対して、画像をアップロードするアップローダー(実体はClass)を作成します。 そこにMIMEタイプのバリデーションや頻繁に使用される画像サイズをあらかじめ定義できます。 移行にあたり、それらを修正する必要がありましたが、その作業自体は難しくないものでした。

ですが、問題はここからでした。

問題その1 画像のURLがS3のエンドポイントになっている問題

アップロードしたファイルはS3に保存されるような実装になっています。 画面からファイルのURLを確認するとS3のエンドポイントになっており、直接ダウンロードができてしまうという状況でした。 BOXIL SaaSはCloudflareを通し、画像を最適化しつつ表示しているので、S3から直接画像を取得してしまうと最適化も機能しません。

この問題はShrineのプラグインであるDownload Endpointを使用して解決しました。

具体的には以下のような設定を追加します。

# config/initializers/shrine.rb
Shrine.plugin :download_endpoint, prefix: "attachments/files/images"
# config/routes.rb (Rails) 
Rails.application.routes.draw do
  # ... 
  mount Shrine.download_endpoint => "/attachments/files/images"
end

これで、ファイルのエンドポイントのホストはboxil.jpになり、ファイル自体へのURLはハッシュ化された状態になりました。 実際にBOXIL SaaSのサービスロゴの画像のimgタグを確認してみると以下のようになっています。

<img alt="BOXIL" class="service-logo-image" loading="lazy" src="/attachments/files/images/eyJpZCI6IjBjMGE1MzRmMWQyYjY1NjQ0Y2EyOWFkZjVjZDNhNzViLnBuZyIsInN0b3JhZ2UiOiJzZXJ2aWNlX2xvZ28iLCJtZXRhZGF0YSI6eyJmaWxlbmFtZSI6ImltYWdlX3Byb2Nlc3NpbmcyMDIzMDQxMC0xMjQtNHRtbjdpLnBuZyIsInNpemUiOjkyMjUsIm1pbWVfdHlwZSI6bnVsbH19">

この対応をすることで、S3のエンドポイントは公開されることがなくなりました。

問題その2 移行対象のレコードが大量問題

BOXIL SaaSでは実際のサービスを紹介しているページがあります。 そこにはサービス画面を紹介するためのスクリーンショットが表示されています。

サービス画面のスクリーンショットは9000件のレコードがあり、このレコードをShrine用のデータに変換しつつ、元画像から各画面表示用に最適化された画像を生成する必要がありました。

変換と画像の生成処理をバッチを作成して単純に実行したところ、実行結果が戻ってきませんでした(念の為、5〜6時間くらいは待った)。 のちのち確認したところ、件数が大量すぎて処理がロールバックされており、変換もされていませんでした。

これはidを指定した範囲で絞り込み、少しずつ変換してあげるようにしたところ、無事最後まで変換できました。

問題その3 画像が荒くなる問題

画像を単純に表示すると、画質が荒くなったり、画像が大きすぎてはみ出てしまう問題がありました。

これは縦横比を固定したまま指定された画像サイズにするメソッドresize_to_limitやCSSを駆使してなんとか解決しました。

https://www.rubydoc.info/gems/carrierwave/CarrierWave%2FMiniMagick:resize_to_limit

幾多の障害を乗り越え

無事にRefileからShrineへの移行が完了しました。

その他gemの更新

Ruby 3に対応していなかったバージョンのgemも順次アップデートを実施しました。

  • unicorn
  • simple_form
  • rubocop
  • image_processing
  • mini_magick
  • ddtrace
  • bugsnag
  • faraday
  • faker
  • slack-api ※自前実装して削除
  • slack-notifier ※自前実装して削除

こうして見ると細かいGemの更新が全然できておらず、プロダクトとしても不健全な状況でした。 この機会にアップデートできて良かったと思います。

ついにRuby3へアップデート

BOXIL SaaSはAWS ECS上で起動しています。 Dockerfileの内容を更新し、BOXIL SaaSはついにRuby 3へとバージョンアップしました。

もちろんすんなりバージョンアップできたわけではありませんでした。 CIを実行するとエラーが発生し、それら1つ1つを修正する必要がありました。

1番の影響

Ruby 3へのバージョンアップをするときに1番影響があった変更点はキーワード引数の仕様が変わったことです。
https://www.ruby-lang.org/ja/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/

Ruby 2.7では警告で済まされていたものが、Ruby 3ではエラーとなってしまいます。 例えば以下のようなコードです。

def example(arg: 1)
  p arg
end

# Ruby 2.7: 引数のHashは自動でキーワード引数に変換される
example({ arg: 100 })

# Ruby 3.0: ArgumentErrorとなる
# example({ arg: 100 })はNG
example(arg: 100)

この変更に影響されたのかFakerもキーワード引数を受け取るような仕様に変更がされており、それも変更する必要がありました。 例えば以下のようなコードです。

# Ruby 3.0: ArgumentErrorとなる
Faker::Number.number(4)

# キーワード引数として引数を受け取るようになった
Faker::Number.number(digits: 4)

このパターンとなっているようなコードを一つずつ修正していき、やっとのことでCIが通るようになりました。 そしてようやくRuby 3へのバージョンアップができました。

Ruby 3へのバージョンアップを終えて

今思うとかなり地味で大変な作業だったと思いますが、私はこの手の作業の経験が薄かったので、とても充実した時間を過ごせたと思っています。

システムを定期的にバージョンアップをしていくには

  • CI/CDを導入して、自動テストができている
  • テストコードがある程度、網羅されている

ということがマストだなと思い知らされました。

これが実現できていない状況でのバージョンアップは工数もかかりますし、工数のほとんどがテストに持っていかれてしまうので作業自体も楽しいものではなくなってしまいます。

理想としては100%全パターンを網羅したテストコードを書くべきだと思います。 しかし、ほとんどの場面では現実的ではないと思います。

スピードを求めてサービスを展開したいという場合はテストコードを書く時間すらも惜しいということもあると思います。 さまざまな経緯があってCI/CDの環境やテストコードがないというシステムもあると思います。 しかし、テストコードを書かないことによってどのようなリスクがあるのかを把握しておくことは必要だと思います。

今回のようなバージョンアップ作業でも大いにテストコードが役に立っていました。 自分が入社した時点でもある程度のテストコードが整備されていたので、そのおかげでバージョンアップ作業もスムーズに進めることができました。

システムの将来を見据えた開発をしていくのであれば、完璧なテストコードではなくとも、ある程度のテストコードを整備していくことはマストだと思います。

最後に

今回得られた知見はドキュメントにまとめて、チームの皆さんにも共有し、定期的にBOXIL SaaSのバージョンアップをしていきたいと思います。

もっとBOXIL SaaSを理解して、より良いシステムにしていきたい!

最後まで読んでいただき、ありがとうございました!