非同期処理してますか?
Rails 界隈では Sidekiq や Resque がメジャーな選択肢ですが、スマートバンクではaws-activejob-sqsを使っています。aws-activejob-sqs は Amazon SQS をジョブキューとして使用するための gem であり、Ruby on Rails の ActiveJob アダプターを提供してくれます。
2024 年末にこの gem のメジャーバージョン v1.0.0 がリリースされ、我々も喜び勇んでメジャーバージョンアップデート対応に踏み込んだわけですが、v1.0.0 には解決のための手数が必要な破壊的変更がいくつか含まれておりました。
本記事では我々が aws-activejob-sqs v1アップデートにて実際に遭遇した課題と解決方法について紹介します。aws-activejob-sqs を使用している or 検討している方、あるいは直面している破壊的変更の乗り越え方の参考になれば幸いです。
aws-activejob-sqs の歴史おさらい
昔々…というほどではないですが2015年からaws-sdk-railsという gem がありまして、AWS の各種サービスと Rails を統合するための様々な機能が一つの gem にまとまっていました。SQS をジョブキューとして使う ActiveJob アダプターもこの中に含まれていました。
2024 年 11 月に aws-sdk-rails が v5 を迎えた折、各種機能・サービスをモジュール化された gem として切り出す変更が行われ、SQS 関連のモジュールは aws-activejob-sqs v0.1.0 として切り出されることになりました。
また同時に、aws-activejob-sqs は Rails と組み合わせずとも使えるように再設計されました。
このような経緯から v0.1 時代にはコードベース上の module 名や class 名や実行コマンドには実態とそぐわないものが少なからず存在していました。このしがらみをたち切るように、v1.0.0 リリースと同時に多くの破壊的変更が行われたのです。
v1 の破壊的変更内容と遭遇した問題
主な変更点を見ていきます。
📢 Announcement: aws-activejob-sqs 1.0.0 release and upgrade information 📢 · Issue #16 · aws/aws-activejob-sqs-ruby · GitHub にも記載されているので詳細はそちらをご覧ください。親切ですね。
デフォルト設定ファイル名変更
# v0.x config/aws_sqs_active_job.yml # v1.x config/aws_active_job_sqs.yml
シンプルな変更ですね。
Queue URL の設定構造変更
キュー URL の記述方法が変わりました。
# v0.x queues: default: 'https://my-queue-url.amazon.aws' # v1.x queues: default: url: 'https://my-queue-url.amazon.aws'
module 名の変更
先述の通り Rails を前提とせず使えるようになったので module 名が変わっています。
# v0.x Aws::Rails::SqsActiveJob # v1.x Aws::ActiveJob::SQS
これまたシンプルな変更です。grep して置換するだけですみました。
ここからが手数を要した変更です 🌶️
- 🌶️ : ちょっと手間
- 🌶️🌶️ : まぁまぁ手間
- 🌶️🌶️🌶️ : 悪い、やっぱ辛いわ
StandardError が発生した際のエラーハンドラー 🌶️
v0.1.1 では retry_standard_errors
というオプションがあり、StarndardError発生時のリトライ有無を制御することができました。我々のRailsアプリではデフォルト設定の true
で動いており、ジョブで StandardError が起きた時には何もせず SQS キューに message を残すことで後々リトライされるという挙動でした。*1
v1.0 では retry_standard_errors
オプションは消え poller_error_handler
というハンドラーをユーザーがRubyで自前で書けるようになりました。というか、エラーハンドラーを指定しないと、StandardError 発生時にワーカーがシャットダウンされる挙動がデフォルトになりました。*2
解決策: v0.1.1 の挙動を再現するハンドラーを書く
poller_error_handler
を追加することで v0.1.1 時代と同じ挙動(何もせず SQS キューに message を残す)を再現するようにしました。
# config/initializers/aws_active_job_sqs.rb # @see https://github.com/aws/aws-activejob-sqs-ruby/blob/v1.0.2/README.md#retry-behavior-and-handling-errors Aws::ActiveJob::SQS.configure do |config| config.poller_error_handler do |_exception, _sqs_message| # エラーハンドラーを設定しない場合、Aws::ActiveJob::SQS::Executorはpoller (= worker) をshutdownするため # no-opなハンドラーを設定することでその挙動を防ぐ。メッセージはキューに残りリトライされる。 # ref: https://github.com/aws/aws-activejob-sqs-ruby/blob/v1.0.1/lib/aws/active_job/sqs/executor.rb#L119-L127 end end
といっても何もしないハンドラーではあります。
コマンドラインオプション -e
の削除 🌶️ 🌶️
v0.x 時代はコマンドラインオプション -e
経由でRAILS_ENV
を渡していましたが、これができなくなりました。先述の通り Rails を前提とせず使えるようになったため、RAILS_ENV
への依存をなくす意図と思われます。
なお、v1.0.0 以降で -e
オプションをつけるとコマンドが失敗するようになります。ゆえに我々のアプリケーションでもこのオプションを消さなければなりませんでした。
# v0.x bundle exec aws_sqs_active_job -e ${RAILS_ENV} # v1.x bundle exec aws_active_job_sqs
しかし困ったことがありました。v0.1.1 時点のコードで -e
オプション (下記リンク先の @options[:environment]
) を消すと ENV['APP_ENV']
が採用されてしまい、ワーカーが意図しない RAILS_ENV
で起動してしまうのです。*3
def set_environment @environment = @options[:environment] || ENV['APP_ENV'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development' end
我々の Rails アプリケーションは自らがどの環境にデプロイされているかを判別するために ENV['APP_ENV']
を使っており、これは RAILS_ENV
とは異なる値をとります。*4
環境名 | RAILS_ENV | APP_ENV | これまでのコマンド | -e オプションを除くとどうなる? |
---|---|---|---|---|
ステージング環境 | production | staging | aws_sqs_active_job -e production | 意図せず staging になる |
本番環境 | production | production | aws_sqs_active_job -e production | production のまま |
-e
オプションを消した際に ENV['APP_ENV']
にフォールバックされるとステージング環境で意図しない動作をするため、何らかの対策が必要だということがわかりました。
解決策: -e
オプションなしでも指定時と同じ挙動となるようモンキーパッチをあてる
ENV['APP_ENV']
ではなく ENV['RAILS_ENV']
にフォールバックするよう config/environment.rb
に一時的なモンキーパッチをあてました。v1.0 以上にアップデートしたらすぐ消す、一時的なパッチです。
# config/environment.rb if 'staging' == ENV['APP_ENV'] && defined?(Aws::ActiveJob::SQS::Poller) Aws::ActiveJob::SQS::Poller.prepend(Module.new do # オリジナルの実装では ENV['APP_ENV'] を優先して参照していたがここでは無視するようにした。 # # v1.0.0 以上ではこのメソッドは存在せず呼ばれないため、このコードが残ったままv1.0.0以上に移行しても問題ない。 # ref: https://github.com/aws/aws-activejob-sqs-ruby/blob/v1.0.1/lib/aws/active_job/sqs/poller.rb def set_environment @environment = @options[:environment] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development' end end) end # Load the Rails application. require_relative "application"
これにより -e
指定時の挙動を再現し、 v0.1.1 を使いつつも同オプションを削除できるようになりました。
なぜ config/environment.rb
なのか?
先述の #set_environment
が呼ばれる手前でこのパッチをあてなければならず、その手前で読まれるファイルが config/environment.rb
だったためです。*5
各種 initializers 等は #set_environment
のあとに読まれるため、ここが良いと判断しました。
Poller (ワーカー) の実行ファイル名変更 🌶️ 🌶️ 🌶️
# v0.x aws_sqs_active_job # v1.x aws_active_job_sqs
これはシンプルに見えてちょっと厄介でした。というのも、v1.0.0 になった時点で古い方の実行ファイル aws_sqs_active_job
が削除されていたためです。
aws-activejob-sqs gem のバージョンやコードを管理している Rails アプリケーションだけでなく、実行コマンドを記述している ECS タスク定義も更新する必要がありました。
# ECS タスク定義で使用していたコマンド bundle exec aws_sqs_active_job <options> # 新しいコマンド bundle exec aws_active_job_sqs <options>
我々の環境ではこれらはそれぞれ別 repositories で管理されており、Rails アプリケーションのデプロイと ECS タスク定義の変更(弊社の場合は Terraform による管理)は atomic に実行できません。そのため互換性を保ちながら段階的にアップデートする方法を考える必要がありました。
解決策: メンテナーにフィードバックして移行パスを設けてもらう
この課題を解決するため、upstream のメンテナーに「こういう問題があって困っている」とフィードバックしました。結果として、v1.0.1 では新旧両方の実行ファイルが提供されるようになり、このバージョンをステップとして踏むことで安全な移行が行えるようになりました。*6
別案として自前でソフトマイグレーション用の executable を用意することも検討したのですが、世の他のユーザーも困っているかもと考え upstream にまずは起票しました。もし upstream の反応がなかったり、対応しないという決定が下っていたら自前で用意したと思います。
実際の移行手順
上記の課題と解決策を踏まえて行なったアップデート全体の移行手順は以下のようになりました。
1. (v0.1.1) -e
オプションを削除しても大丈夫なようにモンキーパッチをあてる
- モンキーパッチで
-e
オプションなしでも動作するよう対応 RAILS_ENV
が正しく設定されるように調整
2. (v0.1.1) ECS タスク定義の実行コマンドから-e
オプションを削除
-bundle exec aws_sqs_active_job <options> -e ${RAILS_ENV} +bundle exec aws_sqs_active_job <options>
3. (v0.1.1) モンキーパッチの削除
- 一時的に追加したモンキーパッチを削除
4. (v0.1.1 → v1.0.1) Rails アプリケーション側で v1.0.1 にアップデート
- v1.0.0 では古いコマンドが存在せず、v1.0.2 では古いコマンドが削除されているため v1.0.1 を選択
- 新旧両方の executable が利用可能になる
- デフォルト設定ファイル名、Queue URL、namespace など諸々の変更もここでまとめて行う
5. (v1.0.1) ECS タスク定義の実行コマンドを変更
-bundle exec aws_sqs_active_job <options> +bundle exec aws_active_job_sqs <options>
6. (v1.0.1 → v1.0.2) Rails アプリケーション側で最新の v1.0.2 にアップデート
- 移行の中間ステップとして踏んだ 1.0.1 はもう不要なので最新化
まとめ
お疲れ様でした。公式の移行ガイドに従えば簡単にアップデートできるかと思いきや、実際は想定よりも複雑でした。
aws-activejob-sqs v1.x へのアップデート手順を実体験に基づいて解説してきましたが、振り返ると以下が難しくも面白いポイントだったなと感じます。
- Rails アプリとインフラ設定の変更を俯瞰しつつ段階的な移行計画を練る必要があった
- gemの歴史やChangelog、コードベースを読み解いて解決策を探る必要があった
また、自分たちのコードと OSS を地続きに捉え、困った時は upstream にフィードバックすることの大切さも実感しました。メンテナーの協力的な姿勢のおかげで達成できた移行でもあるため、改めて感謝の念を伝えておきました。
最後になりますが、ECS 側の変更や Rails 側のコードレビューにあたっては同僚の @shmokmt @osyoyu に協力してもらったおかげでスムーズに進めることができました。Thank you!
本記事は Engineering Manager の @ohbarye が執筆しました。
参考
- aws-sdk-rails https://github.com/aws/aws-sdk-rails
- aws-activejob-sqs-ruby https://github.com/aws/aws-activejob-sqs-ruby
- 📢 Announcement: aws-sdk-rails v4.2 modularization and aws-sdk-rails v5 upgrade information 📢 https://github.com/aws/aws-sdk-rails/issues/165
- 📢 Announcement: aws-activejob-sqs 1.0.0 release and upgrade information 📢 https://github.com/aws/aws-activejob-sqs-ruby/issues/16
*1:https://github.com/aws/aws-activejob-sqs-ruby/blob/v0.1.1/lib/aws/active_job/sqs/executor.rb#L77-L81
*2:https://github.com/aws/aws-activejob-sqs-ruby/blob/v1.0.1/lib/aws/active_job/sqs/executor.rb#L119-L127
*3:https://github.com/aws/aws-activejob-sqs-ruby/blob/v0.1.1/lib/aws/active_job/sqs/poller.rb#L30
*4:development, productionそれぞれでのRailsの振る舞いはかなり異なるので、本番となるべく差異が少ない環境としたいステージングなどで RAILS_ENV=production と設定するのは定石だと考えています
*5:https://github.com/aws/aws-activejob-sqs-ruby/blob/v0.1.1/bin/aws_sqs_active_job#L7-L9
*6:v1.0.2 では改めて削除されています。 https://github.com/aws/aws-activejob-sqs-ruby/issues/16#issuecomment-2556832461