こんにちは、2024年新卒入社、開発部の岡塚です。現在は主に求人飲食店ドットコムなどの開発に携わっています。
弊社の新卒エンジニア研修ではWeb開発技術の基礎からシステムの設計・開発までがカリキュラムとして組み込まれています。今回はその中でもシステムの設計・開発を学ぶ「システム設計研修」で私が開発を担当したテーマであった「変更リードタイム可視化機能」について紹介します。弊社の新卒研修については、以下の記事で詳細に紹介されています。
背景
弊社では開発生産性向上活動の一環としてFour Keysを用いた生産性計測が行われています。生産性可視化の取り組みについては、過去の記事で詳細に紹介されているのでよろしければご覧ください。
上記記事の「まとめ」のセクションでも触れられていますがFour Keys計測は継続的・自動的に集計する仕組みが用意されておらず、集計作業に工数がかかっていることが課題となっていました。そのため今年のシステム設計研修では、変更リードタイムの自動集計システムが題材の一つとして選ばれ、今回私がそれに取り組みました。
以下に作ったシステムの画像を貼ります。元々はシェルスクリプトとGoogleスプレッドシートを用いて集計を手作業で行なっていたものを、今回の開発でシステム化しています。
既存のスプレッドシート:


今回作成したシステム:


設計方針
今回のシステムを開発するにあたり、以下の方針で設計・実装を行いました。
Ruby on Railsでバックエンドと、フロントエンドのグラフ以外の部分を実装しています。
- 内製の案件管理システムにweb APIの追加
- 変更リードタイムの計算に必要となるFirst Commitを取得する際にGitHub GraphQL APIを使用するため認証にあたってGitHub Appを使用し、GitHubと内製の案件管理システムからデータ取得を行うバッチの実装
- Chart.jsを利用してフロントエンド側のグラフ部分を作成
これは以下の様な制約があったことから上記の方針を採用しました。
- 新卒研修で作成したシステムの運用や保守にはあまり労力を割けない観点から、既存のインフラ上で構築を行いたい
- 新卒研修の性質上、一般的なweb開発としてアプリケーションのレベルでの実装を行いたい
以下でデータ取得についてシーケンス図で簡単に記載した上で、各項目について説明していきます。
データ取得について
変更リードタイムを取得するための情報源が案件管理システムとGitHubであるため、両方から情報の取得を行なっています。
そして、
- 案件起票日や担当者などのデータは案件管理システムから取得する必要がある
- 一方で案件のfirst commitのみGitHubから取得する必要がある
という理由のため、GitHub GraphQL APIにアクセスしつつ、案件管理システムのテーブルのカラムの一部分の同期をする動作を行う形になっています。
処理方式について定期的に差分同期(一定の時間の範囲内で作成・更新されたデータに限定した同期)を行っており、初回実行時のみ全量同期(データ全体の同期)を行なっているのですが、この理由については後述します。
web APIの追加
同期処理によるデータ取得を行うため、案件管理システム側にweb APIを作成しました。(単純なweb APIのため、詳細は割愛します)
GitHub Appを使用した認証・データ取得処理の実装
変更リードタイムシステムは要件管理システム用のdbとは別のインフラで運用されることになるため、新たに変更リードタイム集計のためのテーブルを作成し、案件管理システムとGitHubから取得した情報をこのテーブルで管理するようにしました。
GitHub GraphQL API経由でfirst commitの日付を取得する過程でGitHubの認証が必要になるため、認証方法を選択する必要がありました。今回はGitHub Appを使用しています。各種選択肢がある中で一般的に個人利用ならPersonal access token(PAT)がその手軽さから使われることが多いと思うのですが、
- Personal access tokenの場合認証情報が個人に紐づく形になる
- 期間制限なしでPersonal access tokenを利用することが非推奨とされている
という懸念点がありました。そのため長期運用の観点から、懸念点の両方に対応できるGitHub Appを使用して認証を行う形にしました。
GitHubの認証をRubyで行う方法としては、専用のライブラリの導入なしで利用する形を取りました。OctokitというGitHub公式のapiクライアントを使うのが一般的ですがそれを使わなくても認証自体は可能で、また今回はライブラリを導入したときの使用箇所がこの機能に限定されるためです。以下の様な実装で認証を行うことができます。
private # Faraday経由でGitHubへのコネクションを取得 # # @param path [String] # @param options [Hash] # @return [Faraday::Connection] def faraday_connection_github(path, options = {}) jwt = generate_jwt( Settings.app_id, Settings.private_key, ) access_token = access_token(Settings.installation_id, jwt) Faraday.new( url: github_url(path, options), headers: { 'Authorization' => "bearer #{access_token}" }, ) end # app_idとprivate_keyからJWTを生成する # # @param app_id [Integer] # @param private_key [String] # @return [String] def generate_jwt(app_id, private_key) private_key = OpenSSL::PKey::RSA.new(private_key) payload = { iat: Time.zone.now.to_i, # 発行時間 exp: Time.zone.now.to_i + dummy_duration, # 有効期限 iss: app_id, # GitHub Appのapp_id } JWT.encode(payload, private_key, 'RS256') end # apiからGitHubのアクセストークンを取得する # # @param installation_id [Integer] # @param jwt [String] # @return [String] def access_token(installation_id, jwt) url = github_url("/api/v3/app/installations/#{installation_id}/access_tokens") response = Faraday.post(url) do |req| req.headers['Authorization'] = "Bearer #{jwt}" req.headers['Accept'] = 'application/vnd.github.v3+json' end JSON.parse(response.body)['token'] end end
上記の認証用の実装をした上で、案件管理システム、GitHubからのデータの同期用のメソッドをクラスメソッドとしてそれぞれモデルに追加しておきます。
データの同期というものを単純に考えた場合、データ全体の同期を定期的に実行しよう、という考えがありうると思います。ただしその場合、時間が経つにつれてデータ量が少しずつ増えていくため、次第にシステムにかかる負荷が青天井に高まってしまうという懸念がありました。そのため、
- システムの稼働開始初回のみデータ全体の同期を時間をかけて実行する
- 具体的には、クラスメソッドを呼び出すスクリプトを用意してFargate上で実行する
- 以降は定期的に差分同期を行うことで実行時間を短く抑えられるようにする
- 具体的には、同期の成功・不成功を管理するためのログテーブルを用意した上で前回の成功日時〜現在、までの分の差分同期を行うスクリプトをSidekiqのworkerとして作成し、定時実行する
という形をとっています。
Chart.jsをメインに利用してフロントエンド側の画面を作成
フロントエンド側のグラフはChart.jsを用いて実装しました。
大枠の仕組みは既存の機能で実装できたものの、冒頭の画像で示したヒストグラムのパーセンタイルの垂直線の表示の実装が難しかった点でした。
パーセンタイルの垂直線をヒストグラムの各階級幅内に表示するような機能をしたかったものの、そのロジックがChart.js本体や既存の公開されているライブラリではうまく実現できない状態でしたが、Chart.jsのインターフェースを利用することでプラグインを自作することが可能だったため、それを用いることで実装を行いました。
具体的には以下のようにafterDrawに渡したコールバックで、グラフ本体の描画完了時にアノテーション用のロジックを呼び出しています。
const plugin = { id: 'percentile-annotation', afterDraw: (chart: Chart) => { // ループを回す for (const [percentileKey, percentileValue] of Object.entries( percentiles, )) {/* 手続き的にパーセンタイルの垂直線を描画する */} }, } const leadTimesChart = new Chart(ctx, { plugins: [plugin], data: { // 省略 }, options: { // 省略 }, })
学んだこと
まず、完成させることができてよかったです!
今回の研修では、学び・反省になったことが何点かありました。
仕様を理想の目標からの逆算で決めるべきだった
今回のシステム設計研修にあたってはMUSTで求められている仕様というものが少なくWANTの仕様が多かったため、開発を進めながらどこまでやるかを決めて、研修終了までの時間を考慮してWANTに随時対応する、という形で進めることを計画していました。
実際、研修終了までに時間の余裕があると判断し追加でWANTの機能を開発していったのですが、実は仕様面での考慮不足があった、ということが複数発生して期限に間に合わなさそうになってしまっていました。最初の仕様を固める段階で、実際に実装に必要となる工数は考えずまず理想的に何を作りたいのかを依頼側とすり合わせる、その後に現実的な実現の可能性を考慮して難しい部分を削っていくという形の方が良かったかな、と思いました。つまり、まずは時間ファーストでなく理想ファーストで設計を行ってその次に時間などの現実的な制約を考える方が計画の立て方として筋が良かったかなと思いました。
仕様変更についてのレビュワーとの共有
仕様面での考慮不足による変更が発生したタイミングで変更内容をレビュワーに伝えきれておらず、手戻りが発生する場面がありました。仕様調整のやり取りを一元管理することでレビュワーから参照できるようにしたり、こちらから積極的にコミュニケーションして情報の共有を行っていく、などを行う必要があったと反省しました。
エッジケース・異常系に対する考慮
- リードタイムが数ヶ月以上のスパンにまで長くなる案件が発生する
- パーセンタイル計算のアルゴリズムの性質上、実際のリードタイムとしては存在しない数値が指し示されることがある
- ネットワークエラーは何が返されるか不明なため任意のエラーをキャッチするべきだが、特定の型のみを捕捉していてエラーハンドリングが適切にできていなかったことに後から気づいた
といったエッジケースなどへの配慮が抜けていた部分がありました。
学生のときに趣味の一環で開発を行っていた時にはほとんど意識することがなかった部分だったため、このようなところへの配慮が必要だということはかなり学びになりました...
専門用語(ドメインの用語)についてのすり合わせ
リードタイムの定義については休日をどう扱うかなどいくつか考慮すべき点があり、設計の時点で細かく詰めきれていなかったためにレビュアーからの指摘で考慮漏れに気づくということがありました。また、パーセンタイルについては統計学の用語で必ずしも明確な定義があるものではなく(例: 取ることのできる補完のアルゴリズムが複数あり、それ次第で同じデータに対してでも数値が変わってくる)、既存のリードタイム計測の仕組みとの数値のずれが後になってから判明して修正するということがありました。このように専門的な用語・ドメインに関係した用語についてはおざなりにせずに、しっかりと定義を把握しておくことが大切だなと痛感しました。
まとめ
今回、研修テーマとして弊社開発部の生産性可視化システムを開発しました。生産性可視化の工数削減に貢献できていたら幸いです。
Railsを含むweb開発の最低限基本的な部分を設計から実装まで一通り触れて、かなり学びになったと思っています!今後、得た経験を活かして頑張っていきたいです。