Ruby on Rails
イベント
マガジン
技術ブログ
はじめに こんにちは、WEAR開発部バックエンドブロックの小山です。普段は弊社サービスである WEAR のバックエンド開発を担当しています。 WEARではハイブリッド検索などの新たな検索体験の実現を目指しています。その実現に必要な ハイブリッド検索 はOpenSearch 2.11で導入された機能です。Elasticsearch 7.10.2では利用できないため、Amazon OpenSearch Service上のエンジンをOpenSearch 2.11.0以上へ移行する必要がありました。今回はOpenSearch 2系の最新バージョンだった2.19.0を採用しました。本記事では、この移行にあたり対応したSearchkickの導入、ダブルライト戦略によるインデクシング移行、カナリアリリースによる段階的トラフィック切り替えについてご紹介します。 目次 はじめに 目次 抱えていた課題 Elasticsearch 7.10.2の限界 既存のアーキテクチャ 課題を解決したアプローチ 1. Searchkickとopensearch-rubyへの移行 elasticsearch-modelからSearchkickへ elasticsearchからopensearch-rubyへ 既存Searchableとの並存 2. インデクシングのダブルライト戦略 embulk-outputの変更 RakeタスクとDigdagワークフローの追加 3. クエリ種別ごとの動作確認 確認の目的と方針 確認対象の抽出方法 確認したクエリ種別 確認方法 4. 負荷試験 試験条件 試験結果 5. カナリアリリースによる段階的トラフィック移行 リリーススケジュール 各段階での確認項目 確認結果 効果と得られた知見 移行後のアーキテクチャ Searchkickとopensearch-rubyへの移行による保守性向上 並行稼働時のインデクサー移行方法 カナリアリリースの有効性 おわりに 抱えていた課題 Elasticsearch 7.10.2の限界 WEARではコーディネートや動画、メイクの投稿検索にAmazon OpenSearch Service上でElasticsearch 7.10.2を利用していました。しかし、以下の課題がありました。 新機能の利用不可:WEARではハイブリッド検索などの新たな検索体験を計画していたが、Elasticsearch 7.10.2はハイブリッド検索に対応しておらず、実現できない状態 サポートの先行き不透明:Elasticsearch 7.10.2は、Amazon OpenSearch Serviceで提供される最終のオープンソースElasticsearchバージョン。今後の新機能追加やセキュリティパッチの提供が見込めない状態。Elasticsearch 7.1〜7.8の標準サポートは2025年11月に終了しており、7.10.2も同様のサポート終了が予想される状態。AWS側でもOpenSearchエンジンへの移行を推奨 ライブラリのメンテナンス性: elasticsearch gem 7.14.0以降ではAmazon OpenSearch Service上のElasticsearchへ接続不可。gemのバージョンを7.13.3に固定せざるを得ず、アップデートができない状態 既存のアーキテクチャ WEARの検索基盤は、以下のシステム構成で運用していました。 検索機能: elasticsearch-model gemを利用し、検索メソッドを提供。内部では elasticsearch gemが提供する Elasticsearch::Client を通じてOpenSearch Serviceと通信 マッピング定義: elasticsearch-model gemを利用し、モデルにマッピング定義を記述 インデックス操作: elasticsearch gemを利用し、Rakeタスクによるインデックス作成、エイリアス切り替え、旧インデックス削除、ドキュメント削除 インデクシング:トラフィックを考慮し、レコード更新ごとではなくDigdagワークフローと Embulk による定時バッチ(日次の洗い替えと差分更新)でインデクシング 課題を解決したアプローチ 今回の移行では、既存ドメインのインプレースアップグレードではなく、OpenSearch 2.19.0の新規ドメインを作成し、エンドポイントを段階的に切り替える方法を採用しました。その理由は以下の通りです。 インプレースアップグレードでは、Elasticsearch 7.10.2からOpenSearch 2.19.0へ直接移行できず、 OpenSearch 1.xを経由する必要がある elasticsearch-model / elasticsearch から searchkick / opensearch-ruby へのgem移行が必要であり、アプリケーションコードに破壊的変更が生じる 検索基盤は影響範囲が大きいため、カナリアリリースで段階的にリリースしたい これらを踏まえ、Elasticsearchをダウンタイムなく移行させるために以下のアプローチで段階的に進めました。 Searchkickとopensearch-rubyへの移行 インデクシングのダブルライト戦略 クエリ種別ごとの動作確認 負荷試験 カナリアリリースによる段階的トラフィック移行 1. Searchkickとopensearch-rubyへの移行 移行前後のgemの対応関係は以下の通りです。 責務 Elasticsearch利用時 OpenSearch移行後 検索機能 elasticsearch-model (内部で elasticsearch を利用) searchkick (内部で opensearch-ruby を利用) マッピング定義 elasticsearch-model searchkick インデックス操作 elasticsearch 直接利用 opensearch-ruby 直接利用 elasticsearch-modelからSearchkickへ 検索機能とマッピング定義については、既存の elasticsearch-model の代わりに、 searchkick に移行しました。Searchkickを選定した理由は以下の通りです。 OpenSearchを公式にサポートしている リポジトリが継続的にメンテナンスされている nested型への対応など、 elasticsearch-model との互換性がある reindex時のアトミックなエイリアス切り替えが組み込まれているほか、ハイブリッド検索やセマンティック検索にも対応しており、高度な機能を備えている elasticsearchからopensearch-rubyへ インデックス操作のRakeタスクでは、 elasticsearch を使用していました。OpenSearch移行に伴い、これを opensearch-ruby に置き換えました。 - require 'elasticsearch' - client = Elasticsearch::Client.new(client_options) + require 'opensearch-ruby' + client = OpenSearch::Client.new(client_options) client.indices.update_aliases(...) client.indices.delete(...) opensearch-ruby は elasticsearch とAPIの互換性が高いため、クライアントの初期化部分とエラークラスの変更で、既存のインデックス操作ロジックをそのまま利用できました。 唯一の例外がインデックス作成タスクで、ここではSearchkick経由でマッピング定義を取得して作成しています。 task :create_index , [ :index_name ] => :environment do |_, args| index_class = index_class_name(args[ :index_name ]).singularize.capitalize.constantize index = Searchkick :: Index .new(args[ :index_name ]) model_config = index_class.search_index.index_options # Searchkickからマッピング取得 index.create(model_config) # Searchkick経由で作成 end このように、マッピング定義はSearchkickに一元化しつつ、その他のインデックス操作は opensearch-ruby を直接使用する構成としました。 既存Searchableとの並存 WEARでは、モデルごとに *Searchable というconcernを定義し、 elasticsearch-model を利用した検索用のデータ定義とマッピング定義を集約していました。 移行期間中は、Elasticsearchを利用するサーバーとOpenSearchを利用するサーバーを並行稼働させる必要がありました。そこで、モデルごとに *OpensearchSearchable concernを新設し、既存の *Searchable と並存させる構成をとりました。 既存の *Searchable はElasticsearch用のconcernです。 # 既存: Elasticsearch用 module Searchable extend ActiveSupport :: Concern # elasticsearch-model を利用したデータ定義とマッピング定義 end 新設した *OpensearchSearchable はOpenSearch用のconcernです。 # 新規: OpenSearch用 module OpensearchSearchable extend ActiveSupport :: Concern included do searchkick index_name : Rails .configuration.x.application[ :opensearch ][ :index_name ], settings : Rails .configuration.x.application[ :opensearch ][ :settings ], callbacks : false , merge_mappings : true , mappings : search_mappings def search_data # searchkick を利用したデータ定義 end end module ClassMethods def search_mappings # searchkick を利用したマッピング定義 end end end merge_mappings: true を指定することで、独自に定義したマッピングをSearchkickの自動生成マッピングにマージしています。 callbacks: false を指定することで、Searchkickの自動インデクシングを無効化し、既存のEmbulkによるインデクシングとの競合を防いでいます。 2. インデクシングのダブルライト戦略 移行期間中、ElasticsearchとOpenSearchの両方にデータを投入するダブルライトを実施しました。WEARのインデクシングは日次バッチによる洗い替え方式のため、ダブルライトを開始した時点で既存データも含めてOpenSearchに自動で同期されます。そのため、既存データの移行作業を別途行う必要はありませんでした。 embulk-outputの変更 前述の通り、既存の構成ではEmbulkを介して、BigQueryからデータを取得してElasticsearchにインデクシングしていました。インデクシング時のBigQueryのクエリコストが高額なため、OpenSearchにもインデクシングを行う際に単純にジョブを複製してしまうと、費用が2重に掛かってしまうという課題がありました。 そこで、embulk-outputの出力先をElasticsearchとOpenSearchの両方に向けることで、SQLの実行は一度だけで双方にデータを転送できるようにしました。 移行前はElasticsearchのみに出力していました。 # Elasticsearchへのインデクシング時 out : type : elasticsearch mode : insert nodes : - { host : {{ elasticsearch_host }} , port : {{ elasticsearch_port }}} index : {{ elasticsearch_index }} { % Elasticsearchの設定値 % } ダブルライト時は type: multi を使い、ElasticsearchとOpenSearchの両方に出力しました。 # ElasticsearchとOpenSearchにダブルライトするインデクシング時 out : type : multi outputs : - type : elasticsearch mode : insert nodes : - { host : {{ elasticsearch_host }} , port : {{ elasticsearch_port }}} index : {{ elasticsearch_index }} { % Elasticsearchの設定値 % } - type : elasticsearch mode : insert nodes : - { host : {{ opensearch_host }} , port : {{ opensearch_port }}} index : {{ opensearch_index }} { % OpenSearchの設定値 % } ダブルライトのために embulk-output-multi を新たに導入し、複数出力先への分岐を実現しました。OpenSearch側の出力も type: elasticsearch を指定しています。 embulk-output-elasticsearch はOpenSearchとのAPI互換性により、そのままOpenSearchへの出力にも利用できました。 RakeタスクとDigdagワークフローの追加 OpenSearch向けのインデックス操作のRakeタスクとDigdagワークフローを作成し、OpenSearchに対しても実行できるようにしました。 # 既存のElasticsearchのインデックス作成 +create_index_elasticsearch: sh>: ... rails "elasticsearch:create_index[${index_name}]" # 追加したOpenSearchのインデックス作成 +create_index_opensearch: sh>: ... rails "opensearch:create_index[${index_name}]" 3. クエリ種別ごとの動作確認 OpenSearch移行後にすべてのクエリ種別が正常に動作するかをQA環境で確認しました。 確認の目的と方針 Elasticsearchに送信されるクエリの種別ごとに、OpenSearch上でも同等の結果が返ることを確認しました。クエリ種別が重複するエンドポイントは確認対象外とし、効率的に網羅性を担保しました。 確認対象の抽出方法 確認対象の抽出は以下の手順で行いました。 対象エンドポイントの洗い出し:リポジトリ内でElasticsearchのQueryクラスを呼び出している箇所をリストアップ WEAR Webの対象画面の特定:Webマスタ仕様書から対象エンドポイントが使用されている画面を確認 クエリの特定:APIのリクエストパラメーターから生成されるOpenSearchのクエリJSONを特定し、使用されているクエリ種別を分類 確認したクエリ種別 以下のクエリ種別を対象に、WEAR iOS・Android・Webの各プラットフォームで動作確認を実施しました。 分類 クエリ種別 検索クエリ term 、 terms 、 range 、 nested 、 bool ( filter / must_not / must / should )、 function_score 、 exists ソート sort ページング from 、 size グループ化 collapse 複合検索 msearch 確認方法 WEAR iOS・Android・Webの各プラットフォームで、以下の方法で確認しました。また、対応するRSpecテストを実行し、OpenSearchに対するクエリが正常に動作することはCI上で確認しています。 WEAR iOS・Android:QA環境のAPIに対してcurlコマンドでリクエストを送信し、レスポンスを確認。 WEAR Web:ブラウザ上で対象画面を操作し、APIレスポンスと画面表示を目視確認。 すべてのクエリ種別で正常な動作を確認し、負荷試験に進みました。 4. 負荷試験 本番リリース前に、OpenSearchクラスターがElasticsearch利用時と同等のリクエスト量を処理できるかを確認するため、QA環境で負荷試験を実施しました。 試験条件 QA環境のOpenSearchクラスターを本番環境のElasticsearchと同等のスペックに設定 検索エンドポイントのRedisキャッシュを無効化し、OpenSearchへの直接的な負荷を計測 k6を用いて、各検索エンドポイントに対して本番のピーク帯のMAX rps相当のリクエストを6時間継続 試験結果 レイテンシ :Datadog APMで各検索エンドポイントのp99レイテンシを直近1か月の平均と比較した結果、OpenSearchがボトルネックとなるレイテンシ劣化は観測されなかった エラー :Datadog APMで各検索エンドポイントを確認した結果、OpenSearch起因のエラーは発生しなかった クラスターメトリクス :本番のピーク帯MAX値相当のリクエストを6時間継続した。CPUUtilizationはリクエスト量に対して許容範囲内、JVMMemoryPressureは本番環境と同程度であり、各種メトリクスに大きな影響はなかった この結果をもとに、カナリアリリースによる段階的な本番投入を判断しました。 5. カナリアリリースによる段階的トラフィック移行 本番リリースでは、カナリアリリースによって段階的にトラフィックを移行しました。 リリーススケジュール 日時 内容 2025/9/30 13:00 canary podの作成、APIの正常確認、1%リリース 2025/9/30 17:00 10%リリース 2025/10/1 14:00 50%リリース 2025/10/2 13:30 100%リリース 2025/10/2〜10/6 正常性の継続監視 各段階での確認項目 各段階で以下の項目を確認し、問題がなければ次の段階に進みました。 OpenSearchのレイテンシ比較とエラー確認:Datadog APMでOpenSearchとElasticsearchのレイテンシを比較し、劣化がないことを確認。OpenSearchのエラーがないことを確認。 各検索エンドポイントのレイテンシ比較とエラー確認:Datadog APMで各検索エンドポイントのレイテンシを比較し、劣化がないことを確認。OpenSearch起因のエラーがないことを確認。 クラスターメトリクス:SearchLatency、IndexingLatency、CPUUtilization、JVMMemoryPressureを監視し、劣化がないことを確認。 インデックスの整合性:ElasticsearchとOpenSearchのドキュメント件数に差異がないことを確認。 確認結果 OpenSearchでレイテンシが低い傾向を確認した(平均・最小・最大いずれもOpenSearchの方が高速) OpenSearch起因のエラーが発生しなかった OpenSearchでJVMMemoryPressureがやや高い傾向にあったが、MAXでも60%未満であり問題なかった CPUUtilizationはOpenSearchの方が低い傾向だった 100%リリース後の監視でも劣化が見られず、移行完了を判断した 効果と得られた知見 移行後のアーキテクチャ 移行後の検索基盤は、以下のシステム構成になりました。 検索機能: searchkick gemを利用し、検索メソッドを提供。内部では opensearch-ruby gemが提供する OpenSearch::Client を通じてOpenSearch Serviceと通信 マッピング定義: searchkick gemを利用し、モデルにマッピング定義を記述 インデックス操作: opensearch-ruby gemを利用し、Rakeタスクによるインデックス作成、エイリアス切り替え、旧インデックス削除、ドキュメント削除 インデクシング:既存のDigdagワークフローと Embulk による定時バッチ(日次の洗い替えと差分更新)でのインデクシングを継続 Searchkickとopensearch-rubyへの移行による保守性向上 elasticsearch-model から searchkick 、 elasticsearch から opensearch-ruby に移行し、以下の効果と知見がありました。 OpenSearchの将来的なバージョンアップへの追随が容易になった reindex処理のアトミックなエイリアス切り替えが組み込みで利用可能になった ハイブリッド検索の機能が利用可能になった opensearch-ruby はAPI互換性が高く、Rakeタスクの移行コストが低かった 並行稼働時のインデクサー移行方法 ダブルライト戦略により、以下のメリットがありました。 ElasticsearchとOpenSearchを並行稼働させることで、いつでも切り戻し可能な状態を維持 Embulkを利用した既存のインデクシングパイプラインを最小限の変更で拡張 移行時のクエリコスト増大を防止 Digdagワークフロー層での制御により、アプリケーションコードへの影響を最小化 カナリアリリースの有効性 段階的なトラフィック移行により、以下の知見が得られました。 1%リリースと10%リリースで、JVMMemoryPressureの変動が大きく見られた。これは、リリース後の低トラフィック時にキャッシュヒット率が低いことに起因する可能性が高く、50%リリース以降は安定した。 検索基盤のような影響範囲の大きいミドルウェアの移行にはカナリアリリースが有効であることを実感した。 おわりに 本記事ではWEARにおけるElasticsearch 7.10.2からOpenSearch 2.19.0への移行プロセスを紹介しました。同様の移行を検討している方の参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに ビジネスdアプリ開発チームの徳原です。 私は地元の金融機関で12年間営業職として勤務した後、IT業界へキャリア転換しました。 本記事では、これまで私が転職で経験したことやキャリアの自律に向けた取り組みについて紹介します。 目次 はじめに これまでのキャリア 金融機関からIT業界へ 前職(外資コンサル)でのSE業務 キャリアを動かしたきっかけ 継続的な学習 前職のインフラ運用業務で苦戦したこと 前職のアプリ開発で苦戦したこと 現職へ転職することになったきっかけ 現職の業務とキャリアの広がり 学習の支援 外部発表の機会 現職のアプリ開発について これまでの経験から感じたキャリアの自律 おわりに これまでのキャリア これまでの私の経験を簡単にまとめました。 地元金融機関で営業12年 外資コンサルにSE転職 営業資料作成(希望外の業務)→インフラ運用→アプリ開発(入社時に希望した業務) ドコモビジネス入社 ビジネスdアプリ開発 金融機関からIT業界へ 金融機関では、主に個人のお客さま向けに金融サービスの営業を担当していました。 定期貯金の契約、年金の請求手続き、保険の契約、相続に関する相談など、生活に関わるさまざまな手続きをサポートしていました。 担当するお客さまは常時数百世帯にのぼり、多くの相談に対応する中で大変な場面もありましたが、「相談して良かった」「以前提案してもらった保険が役に立った」といった言葉をいただけたときは大きなやりがいを感じました。 一方で、日々の業務では申込書や稟議書など紙を中心とした手続きが多く、関係部署とのやり取りに時間がかかることに課題を感じていました。 そうした経験から、テクノロジーによってこれらの手続きを効率化できれば、より多くの価値を提供できるのではないかと考えるようになりました。 ちょうど世の中でDXという言葉が広まり始めた頃でもあり、「デジタルの力で世の中の非効率な業務を改善したい」と思うようになりました。 そしてエンジニアという仕事に興味を持ち、思い切ってIT業界へ転職することを決意しました。 前職(外資コンサル)でのSE業務 転職直後に担当したのは開発ではなく営業資料の作成業務でした。 希望していた職種ではなかったためとても残念でしたが、転職当時ITスキルがほとんどなかった自分を採用してくれたことを考え、まずは目の前の仕事をやり切ろうと決めました。 キャリアを動かしたきっかけ きっかけは小さな行動からでした。 上長が行政DXのプロジェクトを兼任していることを知り、「そちらも手伝わせてください」とお願いしました。 当時はすぐに参画できませんでしたが、前向きに検討してもらうことができました。 そこで諦めず、引き続き交渉を続けていきました。その結果、半年後にはそのプロジェクトのクラウドインフラ運用を兼務できることになりました。 しかし、私が本当にしたいのは開発業務でした。このプロジェクトにはアプリ開発チームがあると知り、また希望を出し続けることにしました。最初は少しずつ関わらせてもらうところからでしたが、最終的にはそれまでの業務との兼務という形ではなく、専従でアプリ開発業務を担当しました。 開発業務ではプログラミングだけではなく資料作成やインフラ環境の構築もあり、振り返ると一見バラバラに見える経験が、後々全て必要なスキルとなりました。 当時は無駄に思っていた仕事でも、後々経験となって活きてくると感じました。 継続的な学習 転職前や転職後、さらに新しい業務に踏み出す際には、自主的に学習を進めることを意識していました。 挑戦の機会に合わせて学習を継続したことが、新しい領域へのチャレンジを後押ししてくれたと感じています。 前職のインフラ運用業務で苦戦したこと インフラ運用では、デプロイ作業やインフラ構築を担当しました。 ステージング環境の構築手順を1つ飛ばしてしまい、検証用サーバへログインできなくなるトラブルを経験したことがあります。 この出来事をきっかけに、手順書の整備と手順を1つ1つ確認しながら進めることが安定した運用につながると強く意識するようになりました。 前職のアプリ開発で苦戦したこと 前職で初めてJavaによるウォーターフォール型の開発に参画した際、Railsでの開発との違いに戸惑いました。 Railsは規約が強く、ある程度のレールに沿って実装を進めることができますが、Javaでは設計書を読み込み、クラス構成やレイヤー構造を理解しなければ実装に着手できません。 特に、インターフェース設計や影響範囲を考慮した修正対応など、「動かす前に考える」文化への適応に苦戦しました。 一方で、この経験を通じて設計の重要性とレビューの重要さを学ぶことができました。 現職へ転職することになったきっかけ 開発エンジニアとして業務に携わる中で、次第に「企画に近い立場でサービスを作りたい」と思うようになりました。 単に要件に沿って実装するだけではなく、ユーザーの視点やこれまでの経験を活かしてサービスの価値そのものに関わりたいと考えるようになったためです。 そうした中で、ドコモビジネスではサービスの企画・開発・セールスまでを自社で一貫して担う体制に大きな魅力を感じました。 自分もそのような環境で、サービスに近い立場から開発に関わりたいと考え、応募しました。 現職の業務とキャリアの広がり 現在は、スクラムをベースとしたアジャイル開発でビジネスdアプリの開発に従事しています。 開発と企画が一体となり、ユーザー価値を短期間で届ける開発スタイルを実践しています。 前職では受託開発として要件に沿った実装する立場でしたが、現職では企画段階から議論に参加し、サービスの方向性を検討する立場で業務に関わっています。 営業時代に経験した業務理解をもとに、実際の利用シーンを踏まえた改善提案を意識しています。 たとえば社内報のPCブラウザ版開発では、中小企業の利用実態を踏まえたPC導線の必要性について意見を出し、検討の一要素として取り入れていただきました。 単に実装するだけでなく、「なぜこの機能が必要か」を議論できる環境にあることは、前職との大きな違いです。 転職後に経験した営業資料作成・インフラ運用・開発経験といった一見異なる業務が、現在の職務でも活かされていると感じます。 現在は、企画〜実装〜改善まで一貫して関われる環境であり、想定していたよりもサービスに近い立場で仕事ができていると感じています。 学習の支援 これまでは自主的に学習を進めてきましたが、現職では資格取得の支援制度が整っており、明確な目標を持ってスキルアップに取り組める環境があります。 上長に取得したい資格を申告し承認を得ることで、受験費用・外部研修の費用を会社側で負担してもらえます。 資格取得が単なる自己満足ではなく、組織として推奨される目標の1つになっている点は、非常にありがたいと感じています。 外部発表の機会 社内外への発信も推奨されており、テックランチやエンジニアブログでの発信機会があります。 また、Google Cloud Next Tokyoでの登壇、Google Cloudとドコモグループ共催イベントでのハッカソン優勝された方など、社外イベントで活躍されているチームメンバーもおり、外部発表が1つの目標として位置づけられています。 自分自身もテックランチやエンジニアブログ執筆を通じて、これまで経験したことや学んだことを発信する機会をいただいており、アウトプットしながら学ぶことを実践できています。 現職のアプリ開発について React開発では、型定義や状態管理の理解に苦労しました。 特にTypeScriptの型設計や、状態の責務分離については、実務を通じて学ぶことが多くありました。 コードレビューでは、パフォーマンスを意識した設計、再利用性を高めるコンポーネント設計、可読性を意識した実装といった観点でフィードバックをいただき、改善を重ねています。 またペアプロ・ペアレビューや生成AIを活用しながら理解を深め、品質向上を意識した開発に取り組んでいます。 モバイルアプリ開発に限らず、1つの機能をフロントエンドからサーバ、分析基盤まで横断して担当できることも大きな特徴です。 サーバレスを前提とした構成のため、インフラ構築の負担が比較的少なく、機能開発や継続的な改善に集中できる点も魅力の1つです。 ビジネスdアプリについては過去の記事をご覧ください。 サーバレスをフル活用したビジネスdアプリのアーキテクチャ [前編] [後編] ビジネスdアプリの社内報PCブラウザ版リリース:レスポンシブ対応とGTM導入で実現した開発効率化 これまでの経験から感じたキャリアの自律 終身雇用が前提ではない今の時代、「会社が面倒を見てくれる」前提でキャリアを考えるのは難しいときもあります。 だからこそ、学び続けることと、行動の積み重ねがキャリアの自律につながるのだと思っています。 おわりに 金融営業からIT業界へのキャリア転換は、業務内容や求められるスキルが大きく変わる挑戦でした。 不安や戸惑いを感じる場面も多くありましたが、実務を通して学び続けることで少しずつ役割を広げていくことができました。 キャリアは一度の異動や抜擢で大きく変わるものではなく、日々の業務への向き合い方と学習の積み重ねによって形づくられていくものだと感じています。 本事例が、今後のキャリア形成や新しい分野への挑戦を考える方にとって、1つのヒントになれば幸いです。
.entry .entry-content .table-of-contents > li > ul { display: none; } はじめに こんにちは、WEAR開発部バックエンドブロックのブロック長を務めている伊藤です。普段は弊社サービスである WEAR のバックエンド開発・組織運営を担当しています。 WEARのバックエンドブロックは約10名のエンジニアで構成されています。組織としてはマトリックス型を採用しており、各メンバーはバックエンドブロックに所属しながら、複数の職種で構成されるスクラムチームにも1〜3名ずつ配置されています。スクラムチームにはPdM(プロダクトマネージャー)やデザイナー、フロントエンドエンジニア、QAなど他職種のメンバーが集まります。加えてリモートワークが基本の環境です。 この体制ではコードレビューのリードタイムが長期化しやすいという課題がありました。本記事では、PRオープンからマージまでの平均時間を約26時間から約11時間へと短縮した取り組みを紹介します。 目次 はじめに 目次 抱えていた課題 コンテキストの分断 レビューのボトルネック化 構造的に後回しになるレビュー 課題を解決したアプローチ 5名ずつの2グループ制 全体朝会+グループ朝会の二段構成 段階的にたどり着いた「もくもくレビュータイム」 Gatherを活用する理由 Findy Team+による指標の可視化と週次改善 コードレビューガイドラインとAIレビューの活用 効果と得られた知見 段階的な施策でリードタイムが半減する コンテキストの把握範囲を狭めることでレビュー速度が上がる 「仕組みだけ」では不十分、同期の時間が文化を変える メトリクスの可視化が「感覚」を「共通言語」に変える AIレビューは「人間のレビューの質」を高める おわりに 抱えていた課題 コンテキストの分断 マトリックス組織では、バックエンドエンジニアが複数のスクラムチームに分散して配置されます。WEARのバックエンドブロックでは約10名が1〜3名ずつ別々のチームに所属しており、隣のメンバーが何を開発しているかが見えにくい状態でした。 PRが作成されても、レビュアーにとってはまず「このPRの背景にある仕様は何か」を理解するところから始まります。コンテキストが共有されていないため、レビューの入口でつまずくことが頻繁に起きていました。 レビューのボトルネック化 WEARのバックエンドブロックでは品質担保のため、2名以上のApproveを必須としています。しかしコンテキストがない状態でのレビューは仕様理解から始まるため、1件あたりの負荷が大きくなります。 改善に取り組む前はレビュアーをランダムに2名アサインしていましたが、得意領域や所属チームがバラバラで、忙しさも人によって異なります。結果として、レビューが後回しになりやすく、PRオープンからマージまでに24時間を超えるケースが多々ありました。 構造的に後回しになるレビュー チーム全員がレビューの重要性は理解していました。しかし、自身のスクラムチームの開発タスクとレビュー依頼が常に競合する状態では、レビューは「割り込みタスク」として後回しにされがちです。 リモートワーク環境では、オフィスで自然に発生する「ちょっと見てほしい」という一声が生まれません。PRを出しても反応のないまま放置される状況が常態化していました。 これは個人の意識の問題ではなく、仕組みで解決すべき構造的な課題でした。 課題を解決したアプローチ 5名ずつの2グループ制 まず、10名を5名ずつの2グループに分けました。グループの編成にあたっては、以下の点を考慮しています。 同じマトリックスチームのメンバーを同一グループにまとめる 関連度の高いチーム(似た領域を触るチーム)やドメインが近い人を同じグループにする ベテラン社員が偏らないようにし、レビューや設計レビューの質にむらが出ないようにする 5名という規模は、全員の作業状況を把握できるギリギリのサイズです。この単位にすることで、「何の仕様に取り組んでいるか」が自然と共有される状態をつくりました。 各グループにはグループリーダーを立て、グループ単位でPDCAを自走できる体制にしています。リーダーがグループ内の課題を拾い、改善施策を回しています。そこで得られた知見はもう一方のグループにも共有し、チーム全体の底上げにつなげています。 全体朝会+グループ朝会の二段構成 毎日の朝会は、全体朝会(30分)とグループ朝会(30分)の二段構成で運用しています。 全体朝会(30分) では、バックエンドブロック全員が集まり、以下の内容を共有します。 小話やLT(チームの雑談・学びの共有) タスク共有(各メンバーの作業状況) 案件共有(お問い合わせ対応のアサインなど) 共有・相談(曜日ごとに担当者が議題を持ち寄る) グループ朝会(30分) では、各グループに分かれて以下を行います。 各スクラムチームから現在の作業状況を報告する チームメンバーのOpen PRを確認し、レビュー依頼をリマインドする 新規PRはPR作成者が画面共有しながらメンバーに内容を説明する 朝会後はそのまま「もくもくレビュータイム」としてレビューに取り組む(詳細は後述) 週1回、Findy Team+のチーム比較を確認し、1週間の振り返りと改善点を話し合う グループ朝会の司会は1週間交代で担当します。特定の誰かに運営が偏らないようにすることで、全員が主体的に関わる仕組みにしています。 ポイントは、グループ朝会で「未レビューのPR」を毎日確認する仕組みにしていることです。これにより、PRが誰の目にも触れずに放置されるという事態を構造的に防いでいます。 段階的にたどり着いた「もくもくレビュータイム」 実は、最初からレビュー専用時間を設けていたわけではありません。取り組みの初期はグループを分けて朝会でPRを確認するところから始めました。 それだけでもリードタイムは改善しましたが、新たな課題が見えてきました。朝会でPRの内容を共有しても、レビューに取り組む時間が仕組みとして確保されていなかったため、結局は各自の開発タスクが優先されがちでした。 そこで、グループ朝会の後にそのまま「もくもくレビュータイム」を設けることにしました。朝会が終わったらそのまま Gather (仮想オフィスツール)に残り、レビューに取り組みます。 もくもくレビュータイムの運用ルールは以下の通りです。 Gatherに集まり、各自が黙々とレビューする 必須でレビューしてほしい人がいる場合は、PR内でその人をメンションしておく メンションされたPRを優先的に確認し、メンションされた人のレビューは必須とする メンションは任意とし、各自の判断で行う(例:その機能に詳しい人へ仕様チェックを依頼したい場合など) この「朝会→もくもくレビュータイム」の流れを毎日のリズムとして定着させたことで、レビューが「空いた時間にやるタスク」から「毎日の習慣」に変わりました。 さらに、朝会後のレビュータイムとは別に、午後にも30分のもくもくレビュータイムを設けています。午前と午後の2回、同期的にレビューする接点をつくることで、1日を通してPRを素早くキャッチできるようになっています。 以下は、1日の流れを図にしたものです。 Gatherを活用する理由 もくもくレビュータイムにGatherの仮想オフィスを使っているのには明確な理由があります。 まず、レビュー中に聞きたいことがあればその場ですぐに声をかけられます。MTGをセットしたりSlackで非同期にやりとりしたりする必要がありません。さらに、他のメンバーが質問している内容も一緒に聞こえるので、自然と共通認識が形成されます。 リモートワークでは「ちょっと聞く」のハードルが高くなりがちですが、Gatherで同じ空間にいることで、オフィスの隣の席で気軽に質問するような感覚を再現できています。 Findy Team+による指標の可視化と週次改善 チームの開発パフォーマンスを Findy Team+ で継続的に計測しています。設定している目標値は以下の通りです。 PRオープン〜マージ:16時間以内 PRオープン〜1人目のレビュー:3時間以内 レビュー〜マージ:13時間以内 以下は実際にチームで確認しているレビューサマリの画面です。 週1回のグループ朝会で、2つのグループ間でリードタイムを比較し、「どこにボトルネックがあったか」を具体的に議論しています。以下はグループ間の比較画面です。 数値があることで「なんとなく遅い」ではなく「今週は1人目のレビューまでが遅かったのはなぜか」という建設的な振り返りができるようになりました。 Findy Team+の計測対象からの除外漏れがないかも毎週確認しています。具体的には、Dependabotによる自動PR、他部署の作業待ちが発生するPR、検証が必要でやむを得ずマージを保留するPRなど、チームのレビュープロセスの実態を反映しないものを除外しています。正確な数値を維持することで、指標の信頼性を保ち、チーム全体が同じデータを見て議論できる状態を担保しています。 グループ間の比較は健全な競争意識にもつながっています。「今週は相手グループの方がリードタイムを短縮できていた」という事実は、翌週の改善アクションを自発的に生み出す原動力になっています。この仕組みによって、改善が一時的な取り組みではなく、継続的に回り続けるサイクルとして定着しました。 コードレビューガイドラインとAIレビューの活用 レビュー観点を明文化したガイドラインを整備しました。以下の観点を体系的に定義し、レビュアーごとの品質のばらつきを低減しています。 Railsのベストプラクティス(RESTfulなAPI設計、Strong Parametersの適切な使用など) セキュリティ(SQLインジェクション対策、JWT認証、環境変数による機密情報の管理など) パフォーマンス(N+1クエリの検出、 nolock スコープによるロック回避、バッチ処理など) API設計(バージョニングの整合性、エラーレスポンスの統一フォーマットなど) テスト(RSpecのベストプラクティス、FactoryBotによる適切なテストデータ生成など) プロジェクト固有の規約(設計思想ドキュメントへの準拠、既存パターンとの一貫性など) 加えて、GitHub ActionsとClaude(Anthropic)を組み合わせたAIレビューの仕組みを導入しました。PRのコメントで @claude-review と呼びかけるだけで、上記ガイドラインに沿った自動レビューが実行されます。PRの差分を読み取り、インラインコメントと全体のまとめを日本語で返すため、人間のレビュアーが着手する前の一次スクリーニングとして機能しています。 実際のレビューでは、以下のフィードバックが返ってきます。 まとめコメント(抜粋) 🟡 Important N+1クエリ対策 : preload ではなく includes の使用を推奨 nolock スコープの使用 : 読み取り専用クエリでのパフォーマンス最適化 🟢 良い点 適切なバッチ処理: find_in_batches を使用してメモリ効率を考慮 充実したテストカバレッジ: 網羅的なテストケースで品質を担保 インラインコメント(抜粋) パラメータの型定義が既存のAPIと一貫していません。他のエンドポイントでは integer で定義されているため、一貫性を保つために型を変更することを推奨します。 注目すべきは、単に一般的なベストプラクティスを指摘するだけでなく、プロジェクト固有の設計思想ドキュメントや既存の実装パターンを踏まえた指摘をする点です。これは、AIレビューのプロンプトに「まずCLAUDE.mdと設計思想ドキュメントを読んでからレビューせよ」と指示しているためです。 また、PR作成前の段階でもClaude CodeやCursor、Codexなど、各メンバーがそれぞれのAIツールを使ってセルフレビューしています。AIのセルフレビュー → @claude-review を使った機械レビュー → 人間によるレビューという多段構成を取っています。これにより、人間のレビュアーが設計判断やビジネスロジックの妥当性に注力できる環境を整えています。 効果と得られた知見 段階的な施策でリードタイムが半減する 以下は、約2年間のリードタイム推移です。グループ制の導入(2024年4月)、生成AIによるPR数増加(2025年8月頃)、もくもくレビュータイム導入(2025年10月)の前後で変化が見て取れます。 各フェーズの平均時間は以下の通りです。 改善前(〜2024年3月) :約26時間 グループ制導入後(2024年4月〜) :約16時間まで短縮 生成AIによるコーディング普及後(2025年8月頃〜) :PR数が週4〜6件から週8〜12件へ約2倍に増加し、約18時間へ上昇 AIレビュー・もくもくレビュータイム導入後(2025年10月〜) :約11時間まで短縮 グループ制だけでも約10時間の改善がありましたが、生成AIの活用でPR数が約2倍に増えた際、一時的にリードタイムが上昇しました。そこにもくもくレビュータイムとAIレビューを組み合わせることで、PR数が増えた状態でもさらに短縮できています。 コンテキストの把握範囲を狭めることでレビュー速度が上がる チームを分けてレビューすることで、各メンバーが把握すべきコンテキストの範囲が大幅に狭まりました。10名全員の状況を追うのではなく、5名の動きだけ把握すればレビューに入れる状態をつくったことが、最も効果の大きかった施策です。 グループ朝会で毎日Open PRを確認する運用と組み合わせることで、「誰がどんなPRを出しているか」が常に頭に入っている状態になります。レビューに着手する際のコンテキストスイッチのコストが大幅に下がりました。 「仕組みだけ」では不十分、同期の時間が文化を変える グループ分けと朝会での情報共有だけでは、レビューのリードタイムは十分には改善しませんでした。転機となったのは「もくもくレビュータイム」の導入です。 情報を共有しても、レビューする「時間」が確保されていなければ結局後回しになります。午前と午後に同期的な接点を設け、「みんなが同じタイミングでレビューする」という習慣を作ったことで、レビューが日常のリズムの一部に変わりました。 重要なのは長い会議を増やすことではなく、短い同期時間を毎日の習慣として組み込むことです。 メトリクスの可視化が「感覚」を「共通言語」に変える Findy Team+の数値とグループ間比較により、改善が「個人の頑張り」ではなく「チームの仕組み」として回るようになりました。 特に週1回のFindy Team+チェックを定例化したことで、数値が悪化したときに早く気づき、翌週の改善アクションにつなげるサイクルが定着しています。ボトルネックを感覚ではなくファクトで議論できることが、継続的な改善を支えています。 AIレビューは「人間のレビューの質」を高める AIレビューの効果は、リードタイム短縮だけではありません。コーディング規約への準拠やN+1クエリの検出といった機械的に判断できる指摘をAIが担うことで、人間のレビュアーがそれらを一つひとつ確認する必要がなくなりました。その分、設計判断やビジネスロジックの妥当性といった、より本質的な観点へ集中できるようになっています。 また、PR作成者自身がAIツールでセルフレビューしてからPRを出すことで、レビュー時の指摘事項が減り、レビュー1件あたりの負荷が下がっています。結果として、レビューの「速度」と「質」を両立できる状態に近づいています。 おわりに レビューのリードタイム改善は、個人の意識改革ではなく仕組みの設計で実現できます。本記事で紹介した施策をまとめると、以下の4点に集約されます。 認知範囲の縮小 :グループを分けることで、把握すべきコンテキストを絞る 同期の接点の設計 :朝会でPRを共有し、もくもくレビュータイムで実行する。午前と午後に接点を分散させることで情報のキャッチアップを早める 指標の可視化 :Findy Team+で数値を計測し、週1回振り返る。数値で語れる文化をつくり、改善を仕組み化する AIによるレビュー品質の底上げ :AIレビューとセルフレビューで定型的な指摘を自動化し、人間は設計判断に集中する 私たちのチームも最初からうまくいったわけではありません。グループ分けだけでは足りず、レビュー専用時間の追加やFindy Team+での振り返りの定例化、AIレビューの導入など、段階的に改善を重ねてきました。結果として、PRオープンからマージまでの平均時間は約26時間から約11時間へと短縮しています。 マトリックス組織×リモートという環境は、コードレビューにとって不利な条件が揃いやすい構造です。しかし適切な単位でチームを分割し、同期と非同期のバランスを設計し、指標で振り返る仕組みを整えれば、質を落とさずに速度を上げることは十分に可能です。 約11時間まで短縮できましたが、改善の余地はまだあると考えています。AIレビューのプロンプトを磨いてレビュー精度を高めることや、AIレビューの品質向上を前提に2名Approveのルール自体を見直すことなど、取り組みたいテーマは尽きません。今後もチームの変化に合わせて仕組みをアップデートしていきます。 同様の課題を抱えるチームにとって、本記事が何かの参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
動画
該当するコンテンツが見つかりませんでした

















