はじめに

こんにちは。enza サーバーサイドエンジニアの緑川です。
先日、運用しているプロダクトでRubyのバージョンアップを行いました。
Rubyのバージョンアップ対応をするのが初めてだったので、自分の振り返りも含め、改めて今回実際に行った対応をまとめてみました。
この記事がRubyをバージョンアップする際に少しでもお役に立てれば幸いです。

バージョンアップの手順

私たちのチームでは使用している言語やライブラリのバージョンを管理し、優先度を付けながら定期的にバージョンアップを行っています。 Ruby2.5系の公式サポートが今年3月までとなっていため、今回Ruby2.5.8 → 2.6.8へバージョンアップを行いました。 今回のバージョンアップはこのような手順で行っています。
  • 公式リリースのチェック
  • 各種ファイル記載のRubyバージョンを上げる
  • gemのバージョンアップ
  • CIが通ることを確認
  • 性能試験をする
  • QAチームの検証
  • 本番環境リリース
ローカル環境でのバージョン変更や、検証環境へのリリース、本番環境反映後の検証など他にも細かな作業はありますが、大きくはこの流れで進めています。ここから具体的に対応したことを紹介していきます。

公式リリースのチェック

今回は2.5.8 → 2.6.8のバージョンアップだったので、その間のバージョンアップでどのような修正が加えられているかの差分を、リリースノートやnewsで確認します。観点としては既存のコードに影響が出るような変更が含まれていないかをチェックします。 2.5.8~2.6.8までの差分を各リリースノート等で確認し、既存コードに大きく影響を与えることがないことを確認しました。変更内容としては主に不具合、脆弱性対応が多かったですね。 併せて、今回のバージョンアップで新たに使えるようになった機能にも目を通しておきます。

各種ファイル記載のRubyバージョンを上げる

Rubyのバージョン管理ファイル等、Rubyのバージョンを指定する記述があるファイルを修正していきます。バージョンアップに伴うエラーなのかどうかを切り分けるために、事前にRSpecが通ることを確認してから始めましょう。

Dockerfileを修正

Dockerイメージを作成するための定義ファイルであるDockerfileにもRubyのバージョン指定の記述があるので、修正します。
Alpine Linuxを使用しているのですが、今回こちらのバージョンも上げました。

ARG RUBY_VERSION=2.6.8
ALPINE_VERSION=3.13
ARG BUNDLER_VERSION=1.17.3
ARG BASE_PACKAGES="busybox-suid git openssh shadow tzdata logrotate"

FROM ruby:${RUBY_VERSION}-alpine${ALPINE_VERSION} as builder

...

Ruby2.6.8と対応するAlpine Linuxのバージョンが3.13 or 3.14だったため、3.14へバージョンを上げました。
今回少しハマったポイントとなるのですが、コンテナのbuild時に下記のようなpermimssionエラーが発生してしまいました。
...
#13 4.467 ERROR: While executing gem ... (Gem::FilePermissionError) #13 4.467 You don't have write permissions for the /usr/local/bundle directory.
...
原因を調査するとalpine3.14では、Linuxのセキュリティ機能であるseccompを使用する際に誤ったエラーが返されるような不具合があったようでした。そのため今回はバージョンを3.13で指定するようにしました。 参考:
https://gitlab.alpinelinux.org/alpine/aports/-/issues/12821 https://github.com/opencontainers/runc/issues/2151

CI用Ruby imageをバージョンアップ

私たちのプロダクトではCircleCIを使っており、そこで使っているDockerイメージのRubyバージョンを変更しました。CircleCIから公式のDockerイメージが提供されており、今回そちらを使用しました。 .circleci/config.ymlのdoker-imageを書き換えるだけです。

...

 rails_image: &rails_image
   image: circleci/ruby:2.6.8-node
   environment:
     RAILS_ENV: test
     DYNAMODB_HOST: "http://127.0.0.1:8000"
     MYSQL_HOST: "127.0.0.1"
     REDIS_HOST: "127.0.0.1" 

...

nodeも使っているプロジェクトなので、node入りのruby:2.6.8-nodeを使うことにしました。

バージョン管理ファイルを修正

Rubyのバージョン管理ファイルの指定を修正します。 rbenvを使っている場合は.ruby-versiton、asdfを使っている場合は.tool-versionを変更します。ちなみに、asdfはrubyのようなプログラミング言語だけではなく、kubectlやterraformなどのツールのバージョン管理も行うことができるので便利ですね。

- 2.5.8 
+ 2.6.8







RSpec、RuboCop実行

各種バージョンアップファイルを修正後、RSpecが問題なく通ることを確認します。
RSpec実行時には問題なかったのですが、RuboCop実行時に下記エラーが出ました。
$ bundle exec rubocop
Error: Unknown Ruby version 2.6 found in .ruby-version. Supported -> versions: 2.2, 2.3, 2.4, 2.5
静的コード解析ツールのgemであるRuboCopが、Ruby2.6系に対応していないバージョンである旨のエラーが発生したため、今回はRuboCopのバージョンアップも対応しました。

gemアップデート(RuboCop対応)

上記理由でRuboCopのバージョン上げたので、それに依存するgemもアップデートしています。
また、RuboCopのバージョンが上がったことに伴いデフォルトのコーディング規約も若干変わったため、該当する記述の修正も行いました。
下記は対応したルールの一部です。



  • Layout::BlockEndNewline
    • Block内のendが独自の行に記載されているか

# bad
blah do |i|
  foo(i) end

# good
blah do |i|
  foo(i)
end

→ 可読性を上げる観点から、こちらのルールを適応させました。 参考:
https://www.rubydoc.info/gems/rubocop/RuboCop/Cop/Layout/BlockEndNewline
  • Naming/RescuedExceptionsVariableName
    • レスキューされた例外の変数に期待どおりの名前が付いているかどうか(デフォルトだと”e”)

# bad
begin
  # do something
rescue MyException =>; exception
  # do something
end

# good
begin
  # do something
rescue MyException => e
  # do something
end

→ exceptionが使われている箇所があったので、変数名をeに統一しました。 参考:
https://www.rubydoc.info/gems/rubocop/0.75.0/RuboCop/Cop/Naming/RescuedExceptionsVariableName 上記のようなコーディング規約の対応をしていき、無事CIが通ることを確認しました。

性能試験をする

CIが通ることを確認したら、開発環境へデプロイし手動で動作確認します。 問題ないことを確認できたのちに、サービスのパフォーマンスに劣化が発生していないかを確認する性能試験を行いました。
私達のプロジェクトではGatlingを使って性能試験を行っており、こちらも大きな性能の劣化がないことが確認できました。 下記はGatlingでの出力結果です。

性能試験後は、検証環境でQAチームにテストを実施してもらい問題ないことを確認してもらいます。

これでRubyのバージョンアップが無事に終了です!

おまけ Ruby2.6系で使えるようになった使えそうな機能

(とっくに最新のRubyバージョンを使っているぜ!という方は温かい目で見守って下さい)
Ruby2.6から使えるようになった新機能や変更された点は沢山ありますが、使えそうなものを少しだけ抜粋。

to_hでブロックを受け取れるようになった


# キーに大文字、値に小文字を持つ要素のハッシュを生成する 

# before 

["Hoge", "Fufa"].map {|x| [x.upcase, x.downcase] }.to_h
#=> {"HOGE"=>"hoge", "FUGA"=>"fuga"} 

# after 

["Hoge", "Fuga"].to_h {|x| [x.upcase, x.downcase] }  
#=> {"HOGE"=>"hoge", "FUGA"=>"fuga"} 

ハッシュ生成時に、ブロック内で [key, value] となる配列を返すと、それが要素になるハッシュが生成されます。

merge(merge!)メソッドに複数のハッシュを渡せるようになった


# 値に文字列を持つ要素のハッシュを生成する

# before

a = { a: "hoge" }
b = { b: "fuga" }
c = { c: "piyo" }
a.merge(b).merge(c)
#=> { a: "hoge", b: "fuga", c: "piyo" }

# after

a = { a: "hoge" }
b = { b: "fuga" }
c = { c: "piyo" }
a.merge(b, c)
#=> { a: "hoge", b: "fuga", c: "piyo" }

Ruby2.6以前はmerge(merge!)メソッドは1つの引数のみ受け付けていましたがが、Ruby 2.6からは複数のハッシュを渡せるようになりました。

終端を省略できるようになった


# 配列内の要素を取得する

# before

a = [0, 1, 2]
a[1..]  #=> syntax error, unexpected ']'
a[1...] #=> syntax error, unexpected ']'

# after

a = [0, 1, 2]
a[1..]  #=> 1, 2
a[1...] #=> 1, 2

Ruby 2.6から範囲(Range)リテラルで、終端を省略して書けるようになりました。Ruby2.6以前ではa[1..-1]のように書きます。 その他変更はこちらで

おわりに

本記事ではRubyのバージョンアップ手順についてまとめてみました。
私達のプロダクトは、複数のリポジトリが関係しており、バージョンアップの影響が他リポジトリへ影響することもあるので、バージョンアップの内容を確認しながら慎重に行いました。
最新のRubyバージョンは3.0.2(安定版 2020年9月現在)なので、これからガンガン上げていきたいですね。
Rubyはすんなりいきましたが、Railsのバージョンアップの方が色々と大変だったりするので、次回はRailsのバージョンアップをやってみようと思います。