この記事は、Techouse Advent Calendar 2024 3日目です。
昨日は aki による 皆に優しい Ruby が俺にだけ BUILD FAILED してくる でした。
3日目は、satoh が担当します。本日は、異なる Amazon ECS Service のコンテナイメージの整合性を確認するために Sidekiq Middleware を使用した話を書きます。
クラウドハウス労務について
クラウドハウス労務は、企業の人事労務業務を支援するクラウドサービスです。
企業に属する従業員の情報を集めるため扱うデータの量が多く、企業の基幹システムと連携するためのエクスポート処理などでは大量のデータを一度に扱う必要があります。
こうした機能を、クラウドハウス労務では Sidekiq という Ruby の gem を活用した非同期処理により実現しています。
クラウドハウス労務では AWS の ECS(Elastic Container Service) によりコンテナを管理しており、ユーザーからのリクエストを受け取るアプリケーションサーバーも Sidekiq のプロセスも ECS のコンテナ上で動作しています。
Sidekiq のクラウドハウス労務での活用については、以前の記事で紹介していますので、ぜひご覧ください。
Sidekiq による非同期処理
Sidekiq を利用した非同期処理の流れを簡単に説明します。
アプリケーションサーバーがユーザーからのリクエストを受け取り、CSVエクスポートなどの非同期処理が必要になると、実行したいジョブの情報を Redis というインメモリデータベースに登録します。
Redis はメモリ上にデータを保存するため高速に読み書きのアクセスが可能です。
そして、Sidekiq は Redis に登録されたジョブを取り出して、そのジョブ情報をもとに処理を実行します。
以上の流れで、リクエストを処理するアプリケーションサーバーと重い処理を行う Sidekiq プロセスの分離が実現されています。
Redis を介してジョブの登録と取り出しを行っているため、アプリケーションサーバーや Sidekiq プロセスを複数起動し同時並行で動かすことができ、高いスケーラビリティを有しています。
先ほど紹介した記事では、Sidekiq プロセスを増やした分散プログラミングで重い処理を高速化する手法について詳しく説明しています。
コンテナイメージの不整合問題
このように Redis と Sidekiq の技術により非同期処理が見事に達成されているのですが、利用する場合に気を付けなければならないことがあります。
その一つが、ジョブを登録するアプリケーションサーバーとジョブを取り出す Sidekiq プロセスの対応が取れていない可能性があることです。
クラウドハウス労務では、デプロイのタイミングにこの問題が発生する可能性があるため、その例を紹介します。
Amazon ECS でのデプロイ
発生する問題の紹介の前に、まず ECS で管理するコンテナへのデプロイの流れについて説明します。
ECS へのデプロイでは、適用したい新しいコンテナイメージを AWS の ECR(Elastic Container Registry) にプッシュします。そして、新しいコンテナイメージを使うように ECS のタスク定義を更新します。その後 ECS Service を更新することにより、それまで動いていたプロセスの終了と新しいコンテナイメージのプロセスの起動が行われ、デプロイが完了します。
以上の一連のデプロイプロセスの間、サービスのダウンタイムが発生することはありません。
これは、ECS がデプロイ中に起動しているタスクの数が設定された値を下回らないように管理しているためです。
この機能のおかげで、サービスの稼働を続けたままのアプリケーションのデプロイを簡単に行うことができます。
上記の流れはアプリケーションサーバーと Sidekiq プロセスに同じように適用されており、さらに使用するコンテナイメージも同一のものになっています。
具体的には、どちらも同一のコンテナイメージから起動した上で、アプリケーションサーバーは Rails サーバーの起動コマンド bundle exec rails server
により起動し、Sidekiq プロセスは Sidekiq gem が提供しているコマンド bundle exec sidekiq
で起動します。
デプロイ時に起きうる問題
このように同一のコンテナイメージを指定して ECS のタスク定義を更新するのですが、ここで一つ問題が発生します。
Sidekiq のジョブを登録したプロセス(アプリケーションサーバー)のコンテナイメージと、取り出すプロセス(Sidekiq プロセス)のコンテナイメージが食い違う可能性があるのです。
例えば、ジョブの登録は更新前のコンテナイメージのアプリケーションサーバーで行われたが、ジョブの取り出しは更新後の新しいコンテナイメージの Sidekiq プロセスで行われるケースなどが考えられます。
もし新しいコンテナイメージで Sidekiq ジョブの処理を担うコードが修正されていたり、ジョブ登録の形式が変更されていた場合、プログラムが意図していない動作をしてバグが発生してしまう恐れがあります。
デプロイの進み方によっては、登録は新しいコンテナイメージで行われ、取り出しは古いコンテナイメージで行われる状況も発生するかもしれません。
不整合のチェック
前節で示したようなコンテナイメージの不整合によるバグは、不整合発生の原因を知り開発時に注意することである程度抑えられると思われますが、チームでの開発をする上では何かしらの仕組みで防ぐ必要があります。
そうした仕組みとして、クラウドハウス労務ではコンテナイメージの不整合の発生を検知するようにしています。
方法としては、Sidekiq ジョブの登録時にアプリケーションサーバーのコンテナイメージのバージョン情報を追加で登録しておきます。そしてジョブを取り出した Sidekiq プロセス側で、登録されたバージョン情報と実行しているプロセスのバージョン情報を比較し、異なる場合は不整合と判定します。コンテナイメージのバージョン情報は、イメージのビルド時にファイルへ書き出すことで参照できるようにしています。
はじめは、ジョブの登録を行うコードで登録する項目にバージョン情報を追加するようにしていました。しかし、Sidekiq による非同期処理はプロダクトの多くの機能で使われていることもあり、ジョブ登録時にバージョン情報を追加するコードが書かれている箇所と書かれていない箇所が混在している状況が発生していました。
そこで、ジョブの登録時に自動的にバージョン情報を持たせる仕組みを追加しようと考え、これを Sidekiq Middleware を利用することで実装しました。
Sidekiq Middleware 使ってみた
Sidekiq Middleware とは
Sidekiq Middleware は、Sidekiq ジョブの登録/実行の前後に好きな処理を挟むことができる機能です。公式ドキュメント
Client Middleware と Server Middleware の2種類があり、Client Middleware ではジョブ登録時に処理を挟み、Server Middleware ではジョブを取り出して実行する前後に処理を挟むことができます。
今回はジョブの登録時に追加の処理を行いたいため、Client Middleware が上手く使えそうです。
不整合チェックのための Sidekiq Middleware の実装
実際に、Sidekiq Middleware を使用してジョブの登録時にコンテナイメージのバージョン情報を追加する処理を実装しました。
Middleware の実装内容は以下のようになります。
class SetImageVersionMiddleware include Sidekiq::ClientMiddleware def call(worker, job, _queue, _redis_pool) job['args'][0]['image_version'] = IMAGE_VERSION yield end end
initializerで実装したMiddlewareを設定します。
Sidekiq.configure_client do |config| config.client_middleware do |chain| chain.add SetImageVersionMiddleware end end
たったこれだけで、ジョブの登録時にコンテナイメージのバージョン情報を追加することができました。
これにより、ジョブ登録処理の実装ごとにバージョン情報を追加しているケースとしていないケースが混在する問題が解消されました。
また、ジョブの登録/実行の形式を新たに実装するときに、コンテナイメージのバージョン情報の登録処理を書く必要がなくなります。
バージョン情報の登録は Sidekiq ジョブで本来行いたい処理との関連性が薄いため、Middleware に実装を隠蔽することでジョブ登録処理の実装をシンプルに保つこともできています。
Sidekiq Middleware の使いどころ
今回取り上げた例のように、Sidekiq ジョブの処理において共通のものを Sidekiq Middleware にまとめることで、コードの重複を減らし、保守性を高めることができます。
また、もともと行いたかった非同期化対象の処理と、Sidekiq を用いた非同期実行に関わる処理を分離することで、コードの見通しを良くすることができます。
これは裏を返すと、Sidekiq Middleware はあくまでミドルウェアとして用い、本来行いたい処理に関わるものを含めるのは避けるべきとも言えます。
Sidekiq Middleware で行われる処理は開発時に意識されにくいため、アプリケーション側で扱うデータに Middleware 内で手が加えられていたりすると、予期していなかった動作を引き起こす恐れがあるためです。
おわりに
本記事では、プロダクト開発での Sidekiq Middleware の活用事例を紹介しました。もし参考になれば幸いです。
明日のTechouse Advent Calendar 2024は nakayama さんによる 開発チームマネージャが考える、Techouseインターン生が圧倒的成長ができる3つの理由 です。ぜひご覧ください。
Techouseでは、社会課題の解決に一緒に取り組むエンジニアを募集しております。 ご応募お待ちしております。