2024 Accelerate State of DevOps Report 概説#1 『"LeanとDevOpsの科学"の「科学」とは何か?』

2024年10月23日、2024 DORA Accelerate State of DevOps Report、通称DORA Reportが公開されました。

2024 DORA Accelerate State of DevOps Report 表紙

このレポートは、ソフトウェア開発における運用と実践について、科学的な手法で調査・分析した結果をまとめたものです。 私は毎年このレポートを楽しみにしています。今年は10回目10年目の節目ということで、いつもより丁寧に読みました。

詳しい人でしたら、10年よりもっと長くないだろうか?と不思議がる方もおられるでしょう。

その辺の複雑な事情を含め、DORA Report 10年間の軌跡とその上に成り立つ最新レポートを解説したいと思います。全4回の予定です。

本記事ではv.2024.3をベースに解説します。なお、執筆時点で日本語版はまだリリースされていませんでした。また、正誤表を確認しなるべく最新の情報を参照するように努めました。
DORA Reportのライセンスは次の通りです。
“Accelerate State of DevOps 2024” by Google LLC is licensed under [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)

なお、DORA Report原文はGoogle Cloudのこちらのページからダウンロードできるので、ぜひ一次情報に触れてみてください。

DORAの知見を「実践」に活かすために

私の35年のエンジニア人生を振り返ると、かなりの時間「意味のない計測データ」の収集と加工に時間を費やしたと思います。なぜなら、(その時の)品質保証部門がプロダクトの「出荷」をなかなか認めてくれないからです。

いま思い出すと、私は被告側の弁護士のようであり、品質保証部門はまるで検察側のようでした。

裁判長は、事業部のトップです。

私はプロジェクトを終わらせるために、ありとあらゆるデータを集めて「そのプロダクトは動く。(きっと)問題ないはずだ!」を弁護する必要がありました。

DFD(data flow diagram)の生みの親であり、「ピープルウェア」「デッドライン」等の名著で有名なTom DeMarco(トム・デマルコ)というエンジニアの巨匠がいます。

Photo of Tom DeMarco by Hans-Rudolf Schulz

彼は、若かりし頃(1982年)の論文で次のような言葉を発しました。


「計測できないものは制御できない」 “You can’t control what you can’t measure.”

これは、私のような古いエンジニアにとっては、しばらくのあいだ呪言となりました。

しかし、2009年7月、IEEE Software誌7月8月合併号*1に、Tom DeMarco は衝撃的な記事を寄稿します。

タイトルは ”Software Engineering: An Idea Whose Time Has Come and Gone?(ソフトウェアエンジニアリング:その考えは、もう終わったことなのか?)”

“You can’t control what you can’t measure.”(計測できないものは制御できない) このセリフには本当の真実が含まれているのですが、私はこのセリフを使うことに違和感を覚えるようになりました。 (中略)例えば、過去40年間、私たちはソフトウェアプロジェクトを時間通り、予算通りに終わらせることができないことで自らを苦しめてきました。

この文章は当時、Tom DeMarco 自身が「測定できないものは制御できない」は誤りだったと認めた!ということで業界に衝撃が走りました。しかし、Tom DeMarco が本当に言いたかったことは、次の文章に含まれます。

しかし、先ほども申し上げたように、これは決して至上命題ではありませんでした。 もっと重要なのは、世界を変えるような、あるいは企業やビジネスのあり方を変えるようなソフトウェアを作るという「変革」です。

Tom DeMarcoが指摘したように、時代はさきほどのような「裁判ごっこ」よりも、もっと顧客との関係性を重視する方向に確実に変化してきました。それが、リーンであり、アジャイルでありDevOpsなのだと思います。

そんな中で登場してきたFour Keysですが、出会ったときの衝撃は今でも鮮明に覚えています。その指標があまりにもシンプルで、なおかつ説得力に満ちていたからです。

DORAの研究成果は決して一時的なトレンドではありません。Four Keysを単なる「流行り」と捉えるのは誤りです。しかし、その一方で、すべてを鵜呑みにする必要もありません。

2024年で10周年を迎えたDORA Reportには、この取り組みが成熟してきたことを示す重要なメッセージが随所に見られます。その代表例が"Applying insights from DORA"(DORAの知見を実践に活かすために)という章です。

一部を訳します。

私たちの調査結果は、皆様が独自の実験や仮説を立てる際の参考にしていただけます。チームや組織に最適なアプローチを見出すために、変更による影響を測定しながら実験を重ねることが重要です。それにより、私たちの調査結果の検証にもつながります。結果は組織によって異なることが想定されますので、ぜひ皆様の取り組みを共有していただき、その経験から互いに学び合えればと思います。

これは、DORAの取り組みが科学的だからこそ言えることであり、数々の困難があったなかで10年間継続してこれた理由でもあると思うのです。

科学とは何か?

DORA Reportから少し脱線するのですが、とても大事なことなので説明させてください。

書籍「LeanとDevOpsの科学[Accelerate] テクノロジーの戦略的活用が組織変革を加速する」(原題"Accelerate: The Science of Lean Software and DevOps: Building and Scaling High Performing Technology Organizations") が、DORAの研究ベースに書かれていることは広く知られているところです。

book.impress.co.jp

ところで、このタイトルの「科学」は何を指しているのでしょうか。

科学とは「なぜだろう?」という疑問に対して、実験と観察を重ねながら、誰でも同じ結果を得られる答えを見つけ出す取り組み、と言えます。天文学者であり小説家でもあった、20世紀屈指の科学者カール・セーガンは科学について次のような表現をしていました。

"Science is a way of thinking much more than it is a body of knowledge." Carl Sagan

科学は、思考の方法であり、知識の集積ではない。 カール・セーガン

例えば、あるカレー店の人気の秘密を科学的に考えてみましょう。

「このカレーが美味しいのはなぜか?」という問いに対して、「シェフの腕が良いから」という個人的な感想や、「創業100年の伝統の味だから」という言い伝えだけでは、科学的な説明とはいえません。

科学的なアプローチでは、次のような調査と実験を行います。

  • 100人のお客さんに同じ条件で食べてもらい、評価を集める
  • スパイスの配合を少しずつ変えて、味の変化を測定する
  • 調理時間や火加減を細かく記録し、最適な条件を探る
  • 異なる調理人が同じレシピで作っても、同じ味になるか確認する

このような過程を経て、「このスパイスの配合で、この温度で30分煮込むと、8割以上のお客さんが美味しいと感じるカレーができる」と言った、誰でも確認できる答えにたどり着けるのです。

開発の現場でも同じです。

「このやり方は効果的だ」という個人の経験や、「有名な企業がやっているから」という理由だけでは、本当にそれが正しいのかわかりません。

DORAの研究では、これらを科学的アプローチによってDevOpsの成功要因を明らかにしました。個人の経験や直感を超えて、数多くの組織のデータを分析し、客観的な証拠に基づいて「何が効果的なのか」を示したのです。これは単なる成功事例の集積ではなく、科学的リサーチによって検証された信頼性の高い知見なのです。

「科学的リサーチ」方法とは

科学的リサーチとは、現象を体系的に調査し、新しい発見や理論を導き出す手法です。観察から始まり、概念化、測定可能な変数への置き換え、モデル化という段階を経て、客観的なデータに基づいた結論を導きます。

向後, 千春. (2016). 18歳からの「大人の学び」基礎講座: 学ぶ, 書く, リサーチする, 生きる. 北大路書房. 図3-3に筆者が加筆

研究の流れは次の通りです。研究プロセスは次の4段階で進みます:

  1. 現象の観察
    例えば、「なぜある開発チームは他のチームより高い成果を上げているのか」という問いからDORA(DevOps Research and Assessment)チームの研究が始まりました。
  2. 概念の特定
    DORAの研究では、継続的デリバリー、継続的インテグレーション、自動化テストの実施などを成功要因として特定しました。
  3. 変数への変換
    概念を測定可能な形に変換します。例えば、デプロイ頻度、変更リードタイム、障害復旧時間、変更失敗率などの具体的指標として定義します。
  4. モデルの構築
    収集したデータを統計的に分析し、因果関係を明らかにします。DORAの研究では、自動化テストの実施が変更リードタイムを短縮することや、チームの独立性がサービスの信頼性向上につながることを実証しました。

この一連のプロセスを通じて、科学的リサーチは客観的な検証可能性、再現性、そしてデータに基づく実証性という特徴を持ちます。これらの特徴により、DORA Reportは他の研究者が検証・発展させられる形でDevOpsの成功要因を明らかにできました。

定量的データと統計分析がもたらす信頼性

DORA Reportで用いられる科学的アプローチに対して、「都合の良いデータ解釈をしているのではないか」という批判を目にすることがあります。

しかし、この批判は科学的リサーチの本質を十分に理解していないことから生じている可能性があります。科学的リサーチとは、単にデータを収集し結果を得ることではなく、現象を客観的に理解し、実証可能な方法で結果を導き出す「方法論」そのものです。

この考え方は、DORAの研究において次のように具体化されています:

  1. 思考の方法の適用
    DORAの研究では、特定の仮説(例えば「継続的デリバリーはパフォーマンス向上に寄与する」)を立て、それを証明または反証するために厳密な手法でデータを収集・分析しています。これは、科学的な「疑う」姿勢と「検証する」姿勢の両立です。
  2. 透明性と再現性
    測定方法や分析手順を厳密に文書化し、他の研究者が追試可能な形で公開しています。これは、科学が単なる知識の蓄積ではなく、「共有されるべきプロセス」であることを象徴しています。
  3. 実践への応用
    科学の思考方法をもとに導き出された成果が、現場での実践を通じて再び検証されています。たとえば、継続的デリバリーやテストの自動化が現実の組織で具体的な効果をもたらすことが示されています。

DORAの研究は10年にわたり、理論と実践の両面で成果を示してきました。カール・セーガンが述べたように、科学とは「知識の集積」ではなく「より良い問いを立て、より深く理解するための思考の道具」です。

DORAはこの科学的アプローチを用いて、LeanやDevOpsの成功要因を信頼性の高い方法論として確立し、現場の改善や組織変革に直接応用できる形で提供しています。毎年のクラスター分析結果の微細な変化も、このような継続的なデータ分析の重要性を示しているのです。

DORA Report 10年の変遷

今回の2024 DORA Reportでは、 "A decade with DORA"(DORAと共に過ごした10年)という章があります。DevOpsの起源から、State of DevOps Report誕生の背景、今日に至るまでの歴史が書かれています。

私は、その説明内容からこれまでの変遷を1枚の画にまとめてみました。

State of DevOps Reportの歴史

この画から分かるように「DORA Report 10年」とは 2014年版State of DevOps Reportから2024年版State of DevOps Reportまでを指します。2020年版はCOVID-19の影響で発行されていませんので、時間の経過を指す10年ではなく、この10冊が10年というわけです。

ですが、2013年版のState of DevOps Reportは10年の10冊には含まれていません。それはなぜなのか? 順を追って解説します。

State of DevOps Report のはじまり

2011年、Puppet Labsで働いていたAlanna BrownはDevOpsについてより深く理解するための調査を開始しました。この調査は、「'DevOps'的な働き方がITにおける新しいビジネスの方法として台頭している」ということを裏付ける助けとなりました。

itrevolution.com

この調査の成功をベースに、2012年に新たな調査を開始、2013年 Puppet LabsとGene Kim氏が率いるIT Revolution Pressが共同で、最初のState of DevOps Reportを発行しました。

書籍「LeanとDevOpsの科学[Accelerate] *2」には、次のような記載があります。

「State of DevOps Report』の初回は2014年版だが、研究自体はそれ以前に始まっていたという点に留意されたい。Puppet社のチームは、DevOpsという(まだそれほど知られていなかった)概念をより良く理解し、それが現場でどう採用されつつあるか、組織パフォーマンスの改善を組織がどう実感しているかを把握するための研究を始めており、2012年にこの研究への参画をGene Kimに求めた。

Gene KimはTripwire, Inc.の創業者兼CTOとして13年間務めたあと、IT Revolutionを創業し出版活動、DevOpsコミュニティへの貢献に尽力する人物でした。

itrevolution.com

さらに、優秀なキーマンが巻き込まれることになります。

そしてGene Kimがその後、Jez Humbleにも応援を仰ぎ、ともに調査に加わって全世界の組織から4,000件の回答を集め、分析した。この種の調査では最大規模である。

Jez Humble は、カルフォルニア大学バークレー校で教鞭も執っているDevOpsの研究者でした。

github.com

このような経緯を経て、2013 State of DevOps Reportはリリースされたのです。

なぜ2013年版はカウントされないのか?

一言で言えば「調査方法が違う」からです。

現在の統計的アプローチが取られたのは2014年以降となります。2013年当時のPuppet社は、ITインフラストラクチャの自動化を支援するソフトウェアを開発・提供している会社でした。

その中心的なプロダクトは Puppet Enterpriseであり、これはサーバーやクラウド環境、ネットワークデバイスなどの管理を効率化し、DevOpsやインフラ管理の自動化を推進するツールでした。

2013 State of Devops Report は、当時あまり知られていなかったDevOpsという概念を広め、Puppet Enterpriseの市場を開拓することが主な目的だったと思われます。実際、2013年版の調査による【主要な発見】は、やや意図的な印象を受けます。

【主要な発見】
  • DevOps導入状況
    • 回答者の63%がDevOpsを導入(2011年から26%増加)
    • 導入期間が長いほど、高パフォーマンス達成の可能性が5倍に上昇
  • 高パフォーマンスを実現する共通実践
    • バージョン管理システムの使用(89%)
    • コードデプロイの自動化(82%)
  • DevOpsスキルの需要
    • コーディング/スクリプティング(84%)
    • コミュニケーションスキル(60%)
    • プロセス再構築スキル(56%)

また、このレポートでは、後のDORAメトリクスの基礎となる4つの主要指標、デプロイ頻度・変更のリードタイム・変更の失敗率・復旧までの平均時間、いわゆるFour Keysが登場します。

しかし、現在の観点とは異なる分析結果でした。下記に、2013 State of DevOps Reportのパフォーマンス指標の解説と、グラフを転載します。

DevOpsの成熟度(未導入から12ヶ月以上前に導入)に基づいて、デプロイ頻度、変更のリードタイム、変更の失敗率、復旧までの平均時間という4つの主要なDevOpsパフォーマンス指標を分析しました。DevOpsの導入が成熟している組織は、まだDevOpsを導入していない組織と比較して、すべての指標において著しく高いパフォーマンスを示しました。

2013 State of DevOps Report, p5

ご覧の通り、4つの指標はそれぞれ、「DevOpsを導入しているか否か」で期間分類されているのです。

  1. Not Implemented(未導入)
  2. Currently Implementing(導入中)
  3. Implemented <12 Months(導入後12ヶ月未満)
  4. Implemented >12 Months(導入後12ヶ月以上)

そして、このレポートは回答者の多くがDevOpsに関心の高い層に偏っている可能性を検証しておらず、バイアスの制御が十分でないという問題も抱えていました。

Dr. Nicole Forsgren の参画

2014年、State of Devops Report に大きな転機が訪れます。

それが、当時ユタ州立大学ハンツマンビジネススクールの教授であったDr. Nicole Forsgren(ニコール・フォースグレン博士)の参画です。

彼女は、ITインパクト、ナレッジマネジメント、ユーザー体験の専門家でした。

itrevolution.com

「LeanとDevOpsの科学[Accelerate] 」に書かれた「謝辞」には、Nicole Forsgrenの次のようなコメントがあります。

私が初めて皆さんのところへお邪魔して、「ここは違っています」などと指摘させていただいたとき(そのときの私の口調、失礼じゃなかったですよね、ハンブルさん?)、皆さんは私を部屋から蹴り出したりしなかった。 おかげで私はその後、忍耐力と共感力を養い、冷めかけていたテクノロジーへの愛を再燃させることができた。また、「あともう1回だけ、分析やってみて!」が口癖であるキム氏の無尽蔵の熱意と気合いは、我々の仕事を堅牢で大変興味深いものにしてくれている。

この謝辞から読み取れるように、Dr. Nicole Forsgren は2014年以降のState of Devops Reportに科学的厳密性をもたらした重要な人物です。それまでの調査手法に対して建設的なフィードバックをし、その結果、2014年のレポートから、より体系的で科学的なリサーチがもたらされます。

そして、2014年から2017年までの間、State of Devops Report は次のメンバーによって調査・研究がなされました。

  1. Nicole Forsgren Velasquez(ニコール・フォースグレン・ベラスケス)
  2. Gene Kim(ジーン・キム)
  3. Jez Humble(ジェズ・ハンブル)
  4. Nigel Kersten(ナイジェル・カーステン)
    • Puppet LabsのCIO
  5. Alanna Brown(アラナ・ブラウン)
    • 2012年からState of Devops Report を担当する発案者

最初はFour Keysじゃなかったし、Eliteも居なかった

Dr. Nicole Forsgrenの参加は、リサーチプログラムに科学的な厳密さをもたらしました。

このことで、最初のFour Keysは統計学的有意(=確率的に偶然とは考えにくく、意味があると考えられる)が検証されるようになります。

このことで、Change Failure Rate(変更失敗率)は、他の3つの指標とは有意な相関が見られなかったため、ITパフォーマンスの定義から除外されています。

【2014 パフォーマンス指標】
指標 High Medium Low
Deployment Frequency 1日に複数回 週1回〜月1回 月1回〜6ヶ月に1回
Lead Time for Changes 数分単位 1日〜1週間 1週間〜6ヶ月
MTTR 分単位 時間単位 日単位

とはいえ、2014 State of DevOpsレポートは、ソフトウェアデリバリーのパフォーマンスと組織のパフォーマンスの関連性を明らかにし、


「高パフォーマンスのITチームを持つ上場企業は、低パフォーマンスのIT組織を持つ企業と比較して、3年間で市場価値の成長率が50%高かった」

ということが発見されています。(2014 Accelerate State of Devops Report)

2014 State of Devops Reportは、Nicole Forsgrenの「科学的リサーチ」によって次第にデータは説得力のあるものに変わっていきました。 ところが、ここにきて大きな壁が立ちふさがります。

それが、ソフトウェア界の巨匠Martin Fowler(マーティン・ファウラー)でした。

次回に続きます。


ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。興味を持っていただいた方はこちらのページからご応募お願いします。

herp.careers

*1:https://www.cs.uni.edu/~wallingf/teaching/172/resources/demarco-on-se.pdf

*2:Forsgren, N., Humble, J., & Kim, G. (2018). LeanとDevOpsの科学 [Accelerate]: テクノロジーの戦略的活用が組織変革を加速する. インプレス.

GitHubからエンジニアスキルを可視化する「スキル偏差値」を大幅リニューアルした話

こんにちは。

FindyでMLエンジニアをしているyusukeshimpo(@WebY76755963)です。

今回は直近で公開した「スキル偏差値ver.3」機能について、その内容や具体的な機械学習モデルの作成方法について紹介します。

Findyのスキル偏差値とは?

スキル偏差値の概要

まずはプロダクトの概要を説明いたします。 「スキル偏差値」は、Findyに登録されているユーザーのGitHubリポジトリ(※Open-access repositoryのみ)を解析し、 コミット量、OSSプロジェクトへの貢献度、他者からのコードの支持などに基づいて技術力をスコアリングする機能です。

GitHubの解析は機械学習技術を用いて実施しており、これまでに何度かアルゴリズムや学習データのアップデートを行ってきましたが、 今回は2017年のリリース以降で最も大幅なリニューアルを行なっており、大元のアルゴリズムや学習データの作り方自体をガラッと変更しています。

アップデートすることになった背景

アップデートの背景には、大きく分けて2つの理由が存在します。

  • リリースから数年が経過する中でユーザーの方からの要望が増えてきたから。
    • 特に「言語別」のスコアに対する要望が強く、言語別のスキル判定の精度改善を求めるニーズが強かったため。
  • 2023年以降、LLMやAI Agentが登場し、エンジニアリング(特にコーディング)の領域でも、それが当たり前に使われるようになってきたから。
    • 今後エンジニアに求められる「スキル」も環境に合わせて変わっていくことが予想される中で、よりサービス開発や運用に直結する能力を評価できるモデルに進化させていく必要があったため。

上記「ユーザーボイス」と「開発を取り巻く環境変化」を受けて、このタイミングで新たな「スキル偏差値」を作ることを決めました。

スキルの見える化する、スキル偏差値ver.3の詳細

今回のスキル偏差値の開発では次の3つの手順でスキルの見える化をしています。

  1. 学習データの作成
  2. ランキング学習
  3. 学習モデルによるスコアリング

この3つの手順について詳しく解説します。

1.学習データの作成

ランキング学習を支えるのは「質の高いデータ」です。

GitHubリポジトリから収集したデータを加工し、言語ごとに特徴量を整備することで、正確なモデル構築を目指しました。

1-1.使用言語ごとのデータ準備

GitHubリポジトリを次の主要言語ごとに分類しました。

  • Python
  • JavaScript
  • TypeScript
  • Go
  • Ruby
  • PHP

各言語ごとで重視される特徴量に違いがあり、言語ごとにデータを分ける方針にしました。

1-2.ペアワイズ形式のデータ構築

ランキング学習するために、今回は「ペアワイズ形式」のデータを用意しました。これは、2つのエンティティ間でどちらが優れているかを比較する形式です。

今回のケースではGitHubユーザー間のリポジトリ情報を比較し、相対的なスキル勝敗データを生成しました。また、ペアはバイアスが生まれないようにランダムでペアを作るようにしています。

1-3.勝敗アノテーションの付与

生成したペアデータに対して、上の図のようにスキルの「勝敗」をラベリングしました。

特にラベル基準をある程度明確化しておくと担当者間でのクロスレビューの際の議論がまとまりやすかったです。

2.ランキング学習

次に、上で用意した学習データに対して、次の2つのステップを経て、「ランキング学習」の手法を用いて特徴量間の比較します。

「ランキング学習」(Learning to Rank)は、複数ある事象の「順位」を目的変数とした場合に用いられる機械学習の手法で、身近な例としては「検索エンジン」などで使われています。

機械学習を用いた「ランキング学習」の実装にはいくつか方法がありますが、今回は上述したペアワイズなデータ間の「勝敗」を学習し、

2-1.ペアワイズデータの特徴量生成

2-2.機械学習モデルによる勝敗予測

以下、それぞれのステップを解説していきます。

2-1.ペアワイズデータの特徴量生成

ランキング学習するために、まずは各々のデータを機械が認識できる形にする必要があります。

具体的には、各データを「特徴化」し、ベクトル化した数値で比較できるようにしています。

当初はこの「特徴」抽出のプロセスについては、言語を問わずある程度一元化できると考えていましたが、 実験過程で、言語間の「特徴」に大きな違いが見られました。

詳細は社外秘情報にあたるため公開できませんが、一例として、ある言語では「Readmeなどのテキストの長さ」が重要な意味を持ちます。 別の言語だとボイラープレート内にReadme用のテキストが充実しているため「Readmeの長さ」を特徴として重視しない方が好ましい、という傾向が出ていました。

上記のように、言語ごとに利用者を取り巻く環境が大きく違う点も考慮し、現段階では「言語ごとに異なる特徴を採用する」という意思決定をしています。

*1

2-2.機械学習モデルによる勝敗予測

データを特徴化したら、機械学習モデルによるランキング学習をし、当該データ間の順位を予測します。

検討初期にはより複雑なモデルを利用することも検討しましたが、「言語ごとに個別のモデルを動かす」という仕様上の制約や、特徴抽出の工夫によりシンプルなモデルでも十分な精度が実現できたため採用しています。

上記のアルゴリズムにペアワイズの学習データとその勝敗を学習させ、勝敗の判定を行なっています。

モデルの精度についてはこちらも非公開ですが、検証方法としてはこちらも「複数人が判断した際の判断と同様の出力(=勝敗)を出せるか」を基準に評価しています。

リリース時点で、対象としている6言語はいずれも人間の判断を8割以上の精度で模倣できており、一定正確なジャッジができていると判断しています。

*2

3.モデルによるスコアリング

学習モデルを作成したら、その推論結果を元にユーザーのスキルをスコアリングしていきます。

学習したのは、ペアワイズデータの勝敗ですので、計算したいユーザーと他のユーザーとの勝敗をシミュレーションして、それを元にユーザーのスキルスコアを算出します。

このスコアを一般的に偏差値計算をする数式に当てはめ、最後に調整(言語ごとに異なる尺度を正規化するなど)をしたものを「スキル偏差値」としています。

実装で苦労した点と解決策

上記を実装する上で困難な点が多々ありました。

今回は次の苦労した点と解決としてどんなことをしたかも説明していきます。

  1. モデル精度の担保
  2. 言語ごとの特徴量

1.モデル精度の担保

アノテーションしたデータを使用しているので、モデルはアノテータに影響を受けます。

アノテータのバイアスを極力抑えて客観的に良いモデルを作成するための工夫が必要だったため、次の工夫をすることで精度改善を試みました。

アノテーションの工夫

データ作成時のアノテーションには次のような手順を導入しました。

  1. 言語経験者による判断: 経験者がアノテーションをすることで正確性を担保
  2. ラベル基準の明確化: ラベル基準を言語化してアノテータに共有
  3. クロスレビュー: 複数人がレビューすることでバイアスを極力抑える

例として、社内でRubyを日常的に利用する有識者へアノテーションの基準づくりを依頼しました。筆者は普段の業務でRubyを使用しないため、このように経験者から合意を得ることで、より良質な正解データを作成できました。

評価設計

アノテーションという定性的な評価を学習データにしているため、ただ単に評価関数の良し悪しで判断できません。

そんな中どのようにモデルの精度を評価したのかも非常に重要かつ難しいポイントでした。

そこで、アノテーションとモデルの精度を担保するために定性と定量の両方で確認しました。

  • 定量評価: 正解率や偏差値の分布を測定
  • 定性評価: 偏差値の妥当性を人間が確認

勝敗の正解率を確認したのち、Ratingや偏差値を実際に算出しますが、正規分布に基づいているか、ユーザーはこの数値で妥当かを定性的に評価しました。

これによるアノテーション結果の正当性を確保しつつ、モデルの方向性も正しいと確認できました。実際には次のサイクルでモデル評価とアウトプット評価をして都度アノテーションから見直すこともしました。

2.言語固有の特徴量設計

また、言語ごとに特徴量設計をするのも苦労したポイントです。同じ特徴量では表現できなかったため、言語ごとに最適な特徴量設計を見つける必要がありました。

言語特徴の言語化をしてデータ化

言語ごとにどんな特徴を持っているのかを言語化し異なる特徴を設計しました。

例えば、あるプログラミング言語ではボイラーテンプレートが充実しているため独自ロジックにスキルが出やすい傾向にあったり、ライブラリの利活用が盛んなプログラミング言語など特徴量が変わってくることがわかりました。

この特徴量の言語化のフローは大きく4つのポイントに分かれます。 まず、仮説の立案し、各言語の特性を調査し、スキルの違いを反映するための特徴を言語ごとに仮説立てを行います。その後実験を重ねて、仮説に基づいて特徴量を作成し、推論結果や評価指標を確認します。 確認した結果に違和感があれば、仮説を見直して新たな特徴量を追加・修正します。これらを繰り返し、最後に評価基準と実際の結果にズレがあれば、特徴量の設計を見直し、適正化という流れを繰り返しました。

最後に

以上がスキル偏差値ver.3の開発についてでした。参考にしていただければ幸いです。

また、弊社では機械学習エンジニア・データエンジニアなど一緒に働いてくれるメンバーを募集しております。

興味がある方は↓からご応募していただければと思います。

herp.careers

*1: 今回のリニューアルが6言語限定になったのも、上記の理由からモデルを言語別に作る必要があったためです

*2: 余談ですが、人間が判断した際も"意見が分かれる"ペアは10~20%程度存在するため、8割の精度というのはかなり妥当な水準だと考えています

Findy Team+のデータインポートのアプリケーションアーキテクチャを大公開!

こんにちは、ファインディでFindy Team+(以下Team+)を開発しているEND(@aiandrox)です。この記事はFindy Advent Calendar 2024 10日目の記事です。

adventar.org

Team+ではコード管理ツール・イシュー管理ツール・カレンダーなど、様々な性質の外部サービスと連携して、エンジニア組織における開発生産性の可視化・分析を行うためのデータを取得しています。

この分析を行うためには、外部サービスごとに異なるデータ構造やAPI仕様の差を吸収した統一的なデータ管理を行う必要があります。この課題を解決するため、異なるサービスのデータを統合し、単一のUIで一貫性を持って表示する仕組みを整えています。

この記事では、コード管理ツールのデータインポートをどのようなアーキテクチャで実現しているかを紹介します。

Team+と外部サービス差分の例

前提として、Team+では現在GitHub, GitLab, Bitbucket, Backlogから取得したコード管理系のデータを以下のように表示しています。

この画面の分析のために取得しているデータは、プルリクのステータス、コミット日、オープン日、最初のレビュー日、レビューステータス、マージ日です。

外部サービスごとの小さな差分の例として、プルリクのステータスがあります。Team+では「対応中」「クローズ」「マージ」の3種類がありますが、各サービスのAPIレスポンスの値は以下を返すようになっています。

GitHub GitLab Bitbucket Backlog
open, closed opened, closed, locked, merged OPEN, MERGED, DECLINED, SUPERSEDED Open, Closed, Merged

GitHubの場合、ステータスの値だけを参照してもクローズとマージの区別がつかないため、merged_atに値が入っているかどうかで判定しています。また、GitLabのlockedやBitbucketのSUPERSEDEDのようなイレギュラーなステータスは、他の値に丸めるようにしています。

全体のアプリケーションアーキテクチャ

全体の流れは、以下のようになっています。これらのインポート処理は、各サービスごとに分割して全8インスタンスで行い、それぞれのインスタンス内で組織ごとに20プロセスで並列して処理しています(2024年12月時点)。

主な役割
Client層 APIの仕様差を吸収し、レスポンスをRepresentationインスタンスとして返す
Importer層 エラーハンドリングをし、サービス独自テーブルに保存する
Transformer層 各サービスごとのデータ形式差分を吸収し、共通テーブルに保存する
API層 表示データのフロントエンド提供

GitHub, GitLabはClient層とImporter層の設計が少し違うため、この図では省略しています

このように4つの層と2種類のテーブルを使うことで、以下のようなメリットとデメリットがあります。

メリット

責務が明確になりコードの見通しがよい

この設計では、新しい外部サービスを追加する際はClient層とTransformer層、それぞれ単独で実装することができます。また、コードレビュー時も変更範囲が限定されるため、確認すべき箇所を絞りやすく、レビューの効率がよいです。

不具合が起きたときの原因がわかりやすくなる

例えば、外部サービスのAPIのエラーによってデータが取得できなかった場合、Importer層でエラーがキャッチされます。また、値がTeam+で扱うものとして想定外だった場合はTransformer層でエラーになります。

外部サービスのAPIを使っているため、こちらで対応できないエラーが起きることや想定されないデータが返ってくることは避けられませんが、それによる影響が最小限になるようにしています。

2種類のデータを扱うことで、柔軟性と速度を担保する

Team+には、サービス独自テーブルと共通テーブルの2種類があります。前者はAPIレスポンスの形に近い形で保存することを目的とし、後者はTeam+で扱いやすい形式で保存することを目的としています。これにより、フロントエンドからのリクエストに対しては、外部APIと独立して安定したデータソースとして迅速に提供することができます。

他にも、サービス独自テーブルに保存されたデータはTransformer層でエラーが発生しても再利用可能なため、外部APIを再度叩く必要がありません。

層ごとにスケールすることができる

現時点では行っていませんが、Import処理とTransform処理が独立しているため、必要に応じて片方のみスケールするという選択肢を持つことができます。

デメリット

層ごとの独立性が高いがゆえの複雑さがある

層ごとに責務が分離しているため、全体の流れを把握するのが大変です。特に新しいメンバーが参加した場合、どの処理がどの層で行われているかを理解するまでに学習コストがかかります。また、デバッグ時には層の間をまたぐデータフローを追う必要があり、状況によっては負担になることもあります。

リアルタイム性に欠ける

このアーキテクチャは、データの取得から変換、提供までを複数の層に分割しているため、一連の処理がリアルタイムで完結する用途には適していません。例えば、ユーザーがリアルタイムにデータを参照したい場合、現行のバッチ処理的なインポートでは対応が難しいです。

Team+でも、初回連携の際の一時的なデータ取得ではClient層しか利用していません。

一部のリソースのみインポートするといった処理が難しい

このアーキテクチャでは、すべてのデータのImport処理が完了した後にTransform処理を行っています。これが完了するまで画面上にデータを表示できないため、Import処理に時間がかからないリソースから逐次的に表示できるようにするなどの柔軟性は持たせづらいです。

各層について

Client層

この層では、外部サービスのAPIレスポンスや仕様がサービスごとに違うため、その差分を吸収することが大きな目的としています。

libディレクトリ配下に置いてGemのように独立して作用できるようにしつつ、アプリケーションで統一して扱えるようにしています。主に、以下のような処理を行っています。

  • 外部サービスのAPIにリクエストを行い、そのレスポンスをアプリケーションで扱いやすい形に成形加工するRepresentationクラスに格納する
  • リトライ処理を行う
  • 例外を発生させる

エラーに対しては、Rate limitのみリトライ処理を行っています。その他のエラーと、最大回数を上回ったエラーは例外を投げるようにしています。ここでは、主にレスポンスステータスを参照しています。

これらの処理には、外部サービス独自のGemは使わずスクラッチで実装しています。また、1ページごとに遅延実行をするようにしてメモリを圧迫しないように対策しています。

Importer層

Client層から取得したRepresentationインスタンスを使い、レコードごとのアソシエーションに応じて関連レコードと一緒に独自テーブルに保存します。

また、Clientインスタンスのような依存性を注入することで、Importerの単体テストを容易にしています。

class PullsImporter
  def self.call(client, repo_id:, repo_full_name:)
    new(client, repo_id:, repo_full_name:).call
  end

  def initialize(client, repo_id:, repo_full_name:)
    @client = client
    @repo_id = repo_id
    @repo_full_name = repo_full_name
    @user_finder = UserFinder.new
  end

  def call
    client.pulls(repo_full_name).each do |response|
      attributes = response.map do |representation|
        PullApiMapper.call(representation, repo_id:, user_finder:)
      end
      Source::Pull.import!(attributes)
    end
  end
end

Importer内では、FinderやApiMapperなどを定義し、それを用いてインポート処理を行っています。

関連レコードの取得はFinderクラスを作成し、これを介するようにしています。これにより、関連レコードを読み込むためのN+1を最小限に抑えることができるようにしています。

class UserFinder
  def find_by(uuid:)
    users_index_by_uuid[uuid]
  end

  private

  def users_index_by_uuid
    @users_index_by_uuid ||= Source::User.reload.index_by(&:uuid)
  end
end

ApiMapperは、Representationからサービス独自テーブルへの変換のためのattributes作成を行っています。関連レコード(この例ではuser)の外部キーを取得するためにFinderを渡しておき、レコード取得の処理はApiMapper内で行っています。

class PullApiMapper
  def call
    {
      repo_id:,
      user_id: user.id,
      title: representation.title,
      state: representation.state
    }
  end

  private

  attr_reader :representation, :repo_id, :user_finder

  def user
    @user ||= user_finder.find_by(uuid: representation.user.uuid)
  end
end

Transformer層

サービス独自のテーブルからレコードを取得し、各サービスのデータ構造の差分を吸収し、表示データ用のテーブルに保存します。

class PullConverter
  def self.call(duration)
    sources = Source::Pull.where(updated_at: duration)
    attributes = sources.map { |source| PullMapper.new.call(source) }
    View::Pull.import!(attributes)
  end
end

ここでもPullMapperが使われていますが、これはImporterで使われているApiMapperとは似ているようで少し違います。前者は、Representationクラスのインスタンスをテーブルに保存するためのもので、後者はモデルのレコードを共通テーブルに保存するためのものです。外部サービスのデータ形式の差分はここで吸収されることがほとんどです。

class PullMapper
  def call(source)
    {
      source_type: source.class.name,
      source_id: source.id,
      repo_id: source.repo.view_repo.id,
      user_id: source.user.view_user.id,
      title: source.title,
      status: to_view_status(source)
    }
  end

  private
  
  def to_view_status(source)
    return :merged if source.merged?
    return :closed if source.closed?

    :created
  end
end

API層

バックエンドから表示データ用のテーブルの値をフロントエンドに返します。ここでは、サービス独自テーブルにはアクセスせず、共通テーブルの値のみを使用します。

リアルタイムのデータ集計には独自のアーキテクチャを利用していますが、こちらに関してはまた別の機会にご紹介します。

おわりに

今回、Team+のデータインポート周りのアプリケーションアーキテクチャについて紹介しました。

このアーキテクチャは、最初からこの形だったわけではありません。複数の外部サービスと連携する中で、API仕様やデータ形式の違いに直面し、それらを克服するために少しずつ設計を見直してきました。また、例えば、初期にはエラー処理がClient層やImporter層に散在していたり、それぞれの層が密結合になっていたため、層ごとの役割分担を明確化しました。こうした試行錯誤を繰り返しながら、現在の仕組みに至っています。

今も、旧アーキテクチャの名残が残っている箇所があったり、データインポートの時間が長い組織があるといった伸びしろがあります。これからも、改善を重ねて、みなさんにとって価値のあるサービスを提供していきます!


ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。興味のある方は、ぜひカジュアル面談で話を聞きに来てください!

herp.careers

新しい技術領域へのチャレンジを促進!フロントエンドエンジニアのためのバックエンド勉強会を開催

こんにちは!ファインディでFindy Team+開発チームのEMをしている浜田です。 この記事はFindy Advent Calendar 2024 6日目の記事です。

adventar.org

今年の上旬、フロントエンジニア向けにバックエンド勉強会を開催しました。この記事ではバックエンド勉強会を開催した目的や内容、効果について紹介します。

バックエンド勉強会を開催した背景

私のチームが開発しているFindy Team+はWebアプリケーションとして提供しており、フロントエンドはReact/TypeScript、バックエンドはRuby on Railsを使用しています。

ファインディでは、エンジニア個人の志向に合わせて、特定の技術領域を深く理解することでバリューを発揮している方や複数の技術領域を幅広く扱ってバリューを発揮している方など様々な方がいます。 そのため、自分の得意領域以外への挑戦を推奨しています。

ただ、現在では1チームに6〜8名ほどのエンジニアで構成され、各自が得意領域を担当することで効率よく開発が進む状況です。 このような状況では、得意領域以外にチャレンジしたい気持ちがあったとしても、最初の一歩が踏み出しづらいものです。

そこで、今回はフロントエンドを主軸としているエンジニアがバックエンドに挑戦する一歩を後押しするためにバックエンド勉強会を実施しました。

バックエンド勉強会の概要

  • 期間: 2024年2月〜8月
  • 頻度: 毎週30分
  • 回数: 23回
  • 講師: 私
    • RubyもReact/TypeScriptも業務で使った経験あり
  • 参加者: 3名
    • バックエンドほぼ未経験、または数年触っていない。React/TypeScriptはバリバリ書いている
  • カリキュラム
    • RubyやRails、DBの基礎
    • 業務で使うコードを使ったライブコーディング

バックエンド勉強会の内容

バックエンド勉強会では、前半はRubyやRails、後半はDBについて学習をしました。 業務で使っているコードを使ってAPI開発の基礎知識を学ぶことで、簡単なAPIの作成や修正ができることを目指しました。

RubyやRailsの学習

VS Codeのプラグイン設定

まずはエディタを整備しました。 参加者は全員VS Codeを使っていたので次のプラグインをインストールしました。

Rails console / dbconsoleを使ってみる

開発環境には全員バックエンド環境も構築しているので環境構築は不要です。 RubyやSQLをサクッと試すことができるように、Rails console / dbconsoleの使い方を紹介しました。

railsguides.jp

ruby-lang.orgを読む

ruby-lang.orgの一部を読み合わせしました。 読み合わせした箇所をいくつか紹介します。

他言語からのRuby入門

普遍の真理

Rubyでは、nilとfalseを除くすべてのものは真と評価されます。 CやPythonを始めとする多くの言語では、0あるいはその他の値、空のリストなどは 偽と評価されます

JavaScriptを書いている人にとって0はFalsyですが、Rubyだと0はTruthyでありハマりやすいので強調しておきました。

不思議なメソッド名

Rubyでは、メソッド名の最後に疑問符(?)や感嘆符(!)が使われることがあります。

珍しいルールなのでこちらも詳しく説明しました。

?はbooleanを返すメソッドにつけることが多く理解しやすいです。

# 偶数かどうかを判定するメソッド
def odd?(n)
  n % 2 == 1
end

!は破壊的メソッドに付けることが多く、破壊的メソッドと同じ役割だが破壊的ではないメソッドがある場合には破壊的メソッド側に!が付けます。ただし、破壊的メソッドしかない場合は!をつけません。

# !あり・なしメソッドがあるパターン
str = "string"
str.upcase!
str # => "STRING", 元の値がstrが上書きされている

str = "string"
upcased_str = str.upcase
str # => "string", 元の値が上書きされていない
upcased_str # => "STRING", upcaseの結果がupcased_strに代入されている

# 同名の非破壊メソッドがないため、破壊的メソッドだけど!がつかない
str = "string"
str.replace "STRING"
str # => "STRING", 元の値が上書きされている

また、Railsだと例外を発生させるかどうかで!を付けたり付けなかったりすることがあります。

hoge.save # 保存に失敗した場合falseを返す
hoge.save! # 保存に失敗した場合例外を発生させる

「存在しなかった」メソッド

Rubyはメッセージに対応するメソッドを見つけられなかったとしても諦めません。 その場合は、見つけられなかったメソッド名と引数と共に、 method_missingメソッドを呼び出します。 method_missingメソッドはデフォルトではNameError例外を投げますが、 アプリケーションに合うように再定義することもできます。

Rubyではメソッドが見つからなかった場合の処理を簡単に上書きできます。 アプリケーションコードで多用することはないですが、ライブラリではよく使われている仕組みなので知っておくと良いです。

class Hoge; end
hoge = Hoge.new
hoge.fuga # undefined method `fuga' for an instance of Hoge (NoMethodError)

# method_missingを上書き
def hoge.method_missing(name, *args)
  puts "メソッドが見つかりませんでした: #{name}"
end
hoge.fuga # メソッドが見つかりませんでした: fuga

演算子は糖衣構文(シンタックスシュガー)

Rubyにおけるほとんどの演算子は糖衣構文です。 いくつかの優先順位規則にもとづいて、メソッド呼び出しを単に書き換えているだけです。 たとえば、Integerクラスの+メソッドを次のようにオーバーライドすることもできます。

class Integer
  # できるけれど、しないほうがいいでしょう
  def +(other)
    self - other
  end
end

Rubyでは"+"もメソッドです。面白い仕組みなので紹介しました。

メソッドなので次のような呼び方ができます。

1 + 2 # => 3
# 1(Integerのインスタンス)の+メソッドの引数に2を渡している
1.+(2) # => 3

Ruby 3.2 リファレンスマニュアル > オブジェクト

Ruby で扱える全ての値はオブジェクトです。

「全ての値はオブジェクトです」はRubyの特徴だと思うので説明しました。

クラスをインスタンス化したものがオブジェクトなのは理解しやすいですが、数値や文字列などのリテラルもオブジェクトであり、クラス自体もオブジェクトです。

全てオブジェクトなのでメソッド呼び出しや既存メソッドの挙動を上書きなどを行えます。

Ruby 3.2 リファレンスマニュアル > クラス/メソッドの定義

ここでは次の項目について説明しました。

  • クラス定義
  • モジュール定義
    • クラスとの違いがわかりづらいので説明
    • インスタンス化できず、共通の振る舞いを複数のクラスで共有したい場合などに使うことが多い
  • メソッド定義
    • 引数がなければ()を省略できる
  • 特異クラス定義 / 特異メソッド定義 / クラスメソッドの定義
    • Railsのモデルで多用するので説明

Ruby 3.2 リファレンスマニュアル > 制御構造

if文など基本的な制御構造やunlessなどRubyの特徴的なところを紹介しました。 また、forは他の言語では多用するがRubyではあまり使わずeachを使うことが多いということも説明しました。

Ruby 3.2 リファレンスマニュアル > 変数と定数

よく使うローカル変数、インスタンス変数、定数を説明しました。

Ruby 3.2 リファレンスマニュアル > リテラル

さらっと全体を眺めつつ「シンボル」「ハッシュ式」「ヒアドキュメント」「%記法」は初見だと戸惑うポイントなので詳しく説明しました。

ハッシュ式

キーとバリューのペアを保持するオブジェクト。 キーには数値・文字列・シンボルが使える。キーにはシンボルを使うことが一般的。

{ 1 => 2, 2 => 4, 3 => 6}
{ "a" => "A", "b" => "B", "c" => "C" }
{ :a => "A", :b => "B", :c => "C" }
{ a: "A", b: "B", c: "C" } # シンボルの場合、このような書き方ができる。この書き方が推奨。

ヒアドキュメント

複数行の文字列を表示するときに使う。

<<
<<-: 終端子にインデントをかける
<<~: インデントをいい感じに削ってくれる

Railsガイドの紹介

Railsガイドについてはこの勉強会では紹介だけ行いました。 Railsの機能が網羅されており、最新バージョンに追従しているとてもよいドキュメントなので、困ったらまずはRailsガイドを読むことをおすすめしました。

railsguides.jp

Railsの構成を説明

実際に開発しているバックエンドのディレクトリ構成を説明しました。

  • Gemfile / Gemfile.lock
    • package.jsonみたいにライブラリを管理しているもの
  • Dockerfile / Dockerfile.dev / compose.yml
    • .devは開発環境用
    • compose.ymlはdocker composeの設定ファイル
  • config
    • 各種設定ファイル
  • db
    • データベーススキーマやマイグレーションファイル
  • log
    • ログファイル
    • dockerで実行している場合、ここに出力されないのでdocker compose logs -f webで確認
  • lib
    • 本体とは切り離して使えるライブラリが格納されている
    • taskにコマンド実行するタスク(ジョブ)が格納されている
  • app
    • admin
      • 管理画面用のgem activeadminで使うクラス
    • controllers
      • MVCのC
    • enums
      • localeやメトリクスのenum情報
    • graphql
      • GraphQLのgem graphql-rubyのquery/mutation/typeなど
    • interactors
      • interactor gemを使ってCUDのビジネスロジックを実装
      • Controller -> Interactor -> Modelの依存関係
    • jobs
      • 非同期処理
    • mailers
      • メール送信処理
    • models
      • MVCのM
      • DBアクセスクラスが一般的だが、分析クラスや外部接続クラスなども入っている
    • views
      • MVCのV

バックエンドのライブコーディング

RubyやRailsの基礎的な部分は抑えたので、ここからは実際にバックエンドのコードを使ってライブコーディングを行いました。

既存のGraphQLのQueryにfieldを追加

フロントエンドを開発しているときにバックエンドを触りたくなる最も多いケースは、APIのインターフェースの変更だと思います。 そこで、既存のGraphQLのQueryにfieldを追加するというテーマでライブコーディングを行いました。

GraphQLの新規作成

次はよりがっつりバックエンドを触るケースとして、次の仕様を満たすGraphQLをライブコーディングしました。 このテーマでAPIを作成するときに必要となるDBのテーブル作成・Model作成・GraphQL作成の一連の流れを学びました。

  • userに紐づくメモを保存する機能
    • ユーザーごとにメモは複数作成可能
    • 1つのメモは100文字まで
    • 空のメモはNG
    • メモは1ユーザー10件まで
  • ユーザーIDに紐づくメモを取得できる
    • テキスト検索できる
  • メモIDに紐づくメモを取得できる
  • メモを新規登録できる
  • メモを更新できる
    • 自分のメモしか更新できない

DBの基礎

バックエンドの学習を進めるうち、データベースの知識を底上げする必要があると感じたため、ここからはデータベースについて学びました。

SQL実習

既存のテーブルを使って、お題を取得するSQLを書いてもらいました。

  • 単一テーブルのSELECT
  • JOINを使ったSELECT
  • 複数のテーブルをJOINするSELECT
  • INNER JOIN、LEFT JOINの使い分け
  • GROUP BY、HAVINGを使った集計

SLQ実習で書いたSQLをActiveRecordで書く

Railsで開発する場合、生のSQLを書かずActiveRecord経由でDBにアクセスするので、SQL実習で書いたSQLをActiveRecordで書いてもらいました。

N+1問題

N+1問題について説明し、RailsでN+1を回避するプラクティスを紹介しました。

  • preload / eager_load / includes
    • 結合が不要な場合はpreload、joinするときはeager_load(left outer join)
    • includesはpreload or eager_loadを自動判断する。
      • 意図せずクエリーが変わってしまうことがあるので使わない方が無難
      • 経験上、includesで発行するSQLが変わって困ったことはあってもincludes禁止で困ったことはない
    • Rails始めた人が絶対辿り着く記事
  • GraphQLではpreloadやeager_loadを使った先読みが適さないこともあるので注意

正規形

リレーショナルデータベースを使う場合、第3正規形までは必修だと思うので非正規形と第1〜3正規形を説明しました。

インデックス、実行計画

テーブル設計をするようになったらインデックスの検討も必須なので説明しました。

インデックスはとても良いスライドがあるのでこちらを共有しました。

speakerdeck.com

また、発行したSQLがどのインデックスを使っているのか判断する手法としてEXPLAINを使う方法を紹介しました。

トランザクションとロック

トランザクションは更新系の処理を実装する時に必須なのでRailsでトランザクションを使う方法を説明しました。

transaction do
  # この中の更新処理は1つでも失敗したら全部ロールバックされる
  # データの整合性を保つために更新系の実装では考慮が必要
end

Railsを使ってWebアプリケーションを作っている場合、明示的にロックを意識することは少ないのですが、概念として楽観的ロックと悲観的ロックについて軽く説明しました。

外部キー

外部キーについてもMySQLのドキュメントをなぞりながら軽く触れました。 特にON DELETEは便利なので活用していきたい機能の1つです。

dev.mysql.com

まとめ

バックエンド勉強会で実施した内容をざっくりではありますが紹介しました。

勉強会に参加したメンバーから「勉強会後からバックエンドのコードがスムーズに読めるようになった」などの感想をいただくことができました。さらに次の機能開発でバックエンドに挑戦するメンバーもいたので開催して本当に良かったと感じています。

次の画像は参加したメンバーのバックエンドのプルリクをFindy Team+で表示したものです。GraphQLのQueryやMutationの追加や、フィールドの追加・修正などを行なったことがわかります。

参加者のプルリク抜粋


ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。興味を持っていただいた方はこちらのページからご応募お願いします。

herp.careers

Nx活用術!モノレポ内のStorybookのパス設定自動化

ファインディ株式会社でフロントエンドのリードをしている新福(@puku0x)です。

この記事はFindy Advent Calendar 2024 4日目の記事です。

adventar.org

Nxはモノレポ管理の便利なユーティリティとして @nx/devkit を提供しています。

今回は @nx/devkit を利用したStorybookの設定の自動化についてご紹介します。

Nxについては以前の記事で紹介しておりますので、気になる方は是非ご覧ください。

tech.findy.co.jp

モノレポでStorybookをどのように管理するか?

皆さんはモノレポでStorybookを運用されたことはありますでしょうか?

概ねどちらかの方法を採用することになるかと思います。

前者はStorybookのデプロイ設定をシンプルにできますが、全プロジェクトのStorybookへのパスを記述する必要があります。後者は設定がやや複雑であり、プロジェクト毎にStorybookのデプロイ設定も必要です。

Findyのフロントエンドはモノレポで管理されており、フィーチャ単位に細分化された多数のプロジェクトを持つという性質と運用の難易度を考慮し、前者の手法を選びました。

どうやってパス設定を自動化するか?

単一のStorybookで集中管理する際に課題となるのは↓の部分でしょう。

// .storybook/main.js
const config = {
  addons: ['@storybook/addon-essentials'],
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
  stories:[ // 👈 ここ!👇
    '../apps/app1/src',
    '../libs/components/src',
    '../libs/app1/feature-a/src',
    '../libs/app1/feature-b/src',
    '../libs/app1/feature-c/src',
    // 以下、全プロジェクトのパスが続きます
    // :
  ]
};

export default config;

数個程度であれば十分に運用できそうですが、モノレポ上となると話が違ってきます。

ちなみに、Findyのフロントエンドではプロジェクト数は 70 個でした。
※他プロダクトでは100個近くになる場合もあります

「え?これ全部手動で管理するんですか!?」

さすがに手動での管理には限界がありますので、ここはNxの力を借りましょう。

使用するのは createProjectGraphAsync というユーティリティです。

createProjectGraphAsync | Nx

Nxは各プロジェクトの依存関係を保持しており、そこからStorybookのパスを算出できます。

最終的に次のようなものを目指します。

// .storybook/main.js
const config = {
  addons: [...],
  framework: {...},
  stories: getStories(),  // https://storybook.js.org/docs/configure#with-a-custom-implementation
  // 👇 こんな感じの配列を返して欲しい
  // [
  //   { titlePrefix: 'app1',  directory: '../apps/app1/feature-a/src' },
  //   { titlePrefix: 'app1-feature-a',  directory: '../libs/app1/feature-a/src' },
  //   { titlePrefix: 'app1-feature-b',  directory: '../libs/app1/feature-b/src' },
  //   { titlePrefix: 'app1-feature-c',  directory: '../libs/app1/feature-c/src' },
  //   ...
  // ]
};

export default config;

実装してみよう!

方針が決まったところで実装していきましょう。

事前の準備として、Storybookのパスを取得したいプロジェクトに tags を設定しておきましょう。Nxの @nx/enforce-module-boundaries ルールによる依存の制御を導入している場合は自然と設定されてあると思います。

// apps/app1/project.json
{
  "name": "app1",
  "tags": ["scope:app1"],
  ...
}
// libs/app1/feature-a/project.json
{
  "name": "app1-feature-a",
  "tags": ["scope:app1", "type:feature"],
  ...
}

nx.dev

@nx/devkitcreateProjectGraphAsync を利用して、各プロジェクトの name および sourceRoot を取得します。

// apps/app1/.storybook/main.js
import { createProjectGraphAsync } from '@nx/devkit';

const getStories = async () => {
  const graph = await createProjectGraphAsync();

  // Storybookが不要なプロジェクトは無視
  const ignoredProjects = ['app1-e2e', 'app1-utils'];

  return Object.keys(graph.nodes)
    .filter((key) => graph.nodes[key].data.tags?.includes('scope:app1'))  // 関連するStorybookの絞り込み
    .filter((key) => !ignoredProjects.includes(key))
    .map((key) => {
      const project = graph.nodes[key].data;
      return {
        titlePrefix: project.name,
        directory: `../../../${project.sourceRoot}`,
      };
    });
};

あとはこれを stories に渡せば完成です。

// apps/app1/.storybook/main.js
const config = {
  addons: ['@storybook/addon-essentials'],
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
  stories: getStories(),  // 👈 プロジェクトの追加・削除に応じて自動的に設定されます
};

export default config;

Storybookが表示されました!

Nxによる自動化のもう1つのメリットは、↓のように titlePrefix にプロジェクト名を関連付けることで、フィーチャ毎の分類がより明確になる点だと思います。

const project = graph.nodes[key].data;
return {
  titlePrefix: project.name,
  directory: `../../../${project.sourceRoot}`,
};

開発メンバーからは、

「モノレポ構造とStoryの構造がリンクすることで画面の使い勝手が非常に良い」
「検索したときにStory名が同じでもどの階層にいるか判断して目的にたどり着ける」

といったフィードバックを受けることができました👏 検索性の向上に一役買えましたね!

今回のサンプルは次のリポジトリから動作を確認できます。是非お試しください。

github.com

まとめ

この記事では @nx/devkit を利用したStorybookの設定の自動化についてご紹介しました。

Nxの機能を活用すれば「モノレポにプロジェクトを追加した後のStorybookの設定が漏れていた!」といった事とは無縁になるでしょう。

検証した時点では、@storybook/test-runnerstories をAsync Functionで渡すパターンとの相性がまだ悪いようでした。今後の更新に期待したいですね。

今回はStorybookとの組み合わせでしたが、同じ仕組みを使ってGraphQL Codegenの設定自動化も可能であると確認しています。また別の機会にご紹介できればと思います。

それではまた次回!


ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。興味を持っていただいた方はこちらのページからご応募お願いします。

herp.careers

【エンジニアの日常】エンジニア達の人生を変えた一冊 Part3

【エンジニアの日常】エンジニア達の人生を変えた一冊 Part2に続き、エンジニア達の人生を変えた一冊をご紹介いたします。

今回はPart3としまして、Findy Freelanceの開発チームメンバーから紹介します。

人生を変えた一冊

マスタリングTCP/IP―入門編

主にバックエンド開発と開発チームのリーダーを担当している中坪です。

私が紹介する「マスタリングTCP/IP―入門編」は通信プロトコルのTCP/IPの基礎について解説している書籍です。

私が最初にこの本を読んだのは、新卒入社した会社で、システムエンジニアとして働き始めた頃です。 当時、Webやスマホなどのアプリケーション開発部署への配属を希望していました。 しかし、実際にはネットワーク機器の設定、導入を業務とする部署に配属となりました。

最初はネットワークという分野に興味を持てず、仕事をする上での必要な知識も足りず、苦労しました。 そんなときに、先輩に勧められて読んだのがこの本です。当時、私が読んだのは第3版です。

本を読み進めながら、業務でPCとルータやファイアウォールを接続し、疎通確認をしたり、Wiresharkを使ってパケットの中身を確認する作業をしました。 本に記載されているプロトコルの仕様と、実際に目の前で行われている通信の挙動を結びつけることができました。

いくつか具体的な例を出すと次のようなことです。

  • IPアドレスとMACアドレスの役割
  • パケットのカプセル化の仕組み
  • 代表的なプロトコルがTCP/UDPのどちらをベースにしており、何番ポートを使っているか
  • デフォルトゲートウェイの役目やルーティングの挙動

そこから、徐々にネットワークに興味を持つようになりました。 目論見どおりに通信を制御できたときは、達成感を感じることができました。

今振り返ると、最初興味がなかったのは知識がないからであって、理解することで後から興味が湧いてくることを体験しました。 また、基礎を学ぶ大切さや、本を読むことと実践を組み合わせることで、学習が加速することも学びました。 エンジニアとしての原体験をもたらしてくれた一冊です。

新しい技術や役割に挑戦する際の姿勢に影響を与えてくれていると感じています。 自分がなにか新しいことを任せる立場になったときも、このときの経験を活かしていきたいと思っています。

今はネットワーク関連の仕事をしていませんが、 本ブログを書くにあたり、久々に最新の第6版を購入して読んでみました。 現在はWeb開発をしているので、その立場から見たときのおすすめの章は次の通りです。

  • 1章 ネットワーク基礎知識
  • 4章 IPプロトコル
  • 5章 IPに関連する技術
  • 8章 アプリケーションプロトコル
  • 9章 セキュリティ

ネットワークの分野でもIPv6やHTTP/3などの変化がありますが、TCP/IPはこの先も当分は通信の基盤として使われると考えています。 そのため、本書にある基礎知識は長い期間有効であり、 ネットワークを専門としないエンジニアであっても1度は読んでおいて損はないと思います。

ハッカーと画家 コンピュータ時代の創造者たち

インフラ・バックエンドエンジニア兼、Embedded SREの久木田です。

著者のPaul Grahamによって書かれたエッセイ集です。コンピュータ時代の革新を担うハッカーたちのものの考え方について書かれています。

各章は独立して書かれているので、どの章から読むことができます。

私がこの本を読んだのは大学1年のときです。もう10年以上前になります。

情報系の学科に入学してはじめてプログラミングにふれた自分としては、今後プログラマとして食べていけるのか、やっていけるのかを非常に悩んでいた時期でした。2011年当時は、プログラマ35年定年説やIT業界はブラックな環境が多いというネガティブな情報が目立っていたように感じ、その影響を受けていました。

そんなときに出会ったのがこの本でした。この本に書かれている「ハッカー」にすごく憧れて、勉強を続けていくこともできて、いまの職につけていると思っています。

私が一番好きな章は第16章の「素晴らしきハッカー」です。

良いハッカーとはどのようなことを好んでいるのか、何を大切にしているのかについて書かれています。良いハッカーとはプログラミングを本当に愛していて、コードを書くことを楽しんでいると書かれています。他にもどういった要素がハッカー足らしめているかを書かれているので興味を持って詳しく知りたいと思った方はぜひこの章を読んでいただきたいと思います。

また、良いハッカーを見分けるには同じプロジェクトで一緒に仕事をすることで初めてわかると書かれていて、当時はそういうものなのかと思って読んでいましたが、今は確かにそうかも知れないと思っています。エンジニアを採用する立場であったこともあるのですが、面接時にわからなかった特定の分野に関する知見の深さを同じチームで一緒に働くことで気づき、その人の凄さを初めて知ることが有りました。

この章のすべてが好きなのですが、特に好きなのは次の一節です。

何かをうまくやるためには、それを愛していなければならない。ハッキングがあなたがやりたくてたまらないことである限りは、それがうまくできるようになる可能性が高いだろう。14歳のときに感じた、プログラミングに対するセンス・オブ・ワンダー1を忘れないようにしよう。今の仕事で脳みそが腐っていってるんじゃないかと心配しているとしたら、たぶん腐っているよ。

大学での勉強や研究室配属後の取り組みを通して感じた、プログラミングの面白さやWebサーバ・ネットワークの仕組みを知ったときの感動が原体験となって私を形作っています。

初版が2005年2007年2と古い本なので、エピソードはコンピュータ黎明期の話が多かったりしますが、ハッカーのマインドに関する説明などは今でも通じる部分は多いかと思います。ハッカーの考え方を理解したい人やコンピュータを扱う世界にいる人、飛び込もうとしている人には特におすすめしたい一冊です。

UNIXという考え方

バックエンド開発を担当している金丸です。

この本はUNIXというOSの背後にある基本的な考え方を知ることができる一冊です。 UNIX自体の利用方法やコマンドについての説明はほとんどなく、UNIXがどのような思想に基づいて作られたかが説明されています。

本書と出会ったのは、新卒入社した会社で、情シスとしてFreeBSDを利用したサーバー管理の業務に従事しているタイミングでした。 当時の私は初めてのCLIに四苦八苦しており、ファイルをコピーするシェルスクリプトを作成するのにも苦戦していました。 「なんでコピー完了したことを教えてくれないんだろう」と同僚に話していたところ、この本を薦められました。

当初は疑問の答えを求めて読み進めていましたが、疑問の答えだけでなく、システムをどのように設計すべきかの指針も学ぶことができました。 プログラミング経験がなかった当時の私にとって、UNIXの考え方は初めて自分が理解できる内容で納得感のあるものでした。

この本は「定理」という形でUNIXの思想を説明しています。

9つの定理を紹介していますが、私の中で特に印象に残ったのは次の3つです。

  • 定理1: スモール・イズ・ビューティフル
  • 定理2: 一つのプログラムには一つのことをうまくやらせる
  • 定理3: できるだけ早く試作を作成する

定理1と2ではUNIXというソフトウェアの大前提となる部分で、互いを補完している関係にあります。 この定理により、シンプルなコマンドを自由に組み合わせて処理を行うことができます。

例: ls, awk, sort コマンドを組み合わせて、ディレクトリ内のファイルを名前順に並べて表示するシェルスクリプト

$ ls -l | awk '{print $9}' | sort

スクリプトで利用されている ls コマンドはディレクトリが空の場合、何も表示せずプロンプトに戻ります。 これにより、次に組み合わせるコマンドに必要な情報だけを渡すことができ、コマンド同士がスムーズに連携できるようになっています。

$ ls
$

この設計思想を知ったとき「なるほど!」と、非常に納得感がありました。 不要なメッセージを出さないことで、コマンドの組み合わせが直感的で柔軟にできるという点にUNIXの考え方への感銘を受けました。

上記の考え方を通じ、疑問と思っていた cp コマンドの役割はコピーすることであり、その機能のみをもつことが、定理2の「一つのプログラムには一つのことをうまくやらせる」に即していると理解しました。 合わせて、完了メッセージが必要であれば、コマンドを組み合わせて出力するのがUNIXらしい考え方だと解釈しました。

定理3では、プロトタイプを活用した開発の重要性を説いています。

この定理で紹介された次の一節が特に印象に残っています。

製品の完成後に百万のユーザーから背を向けられるより、少数から批判を受けるほうがはるかにいい。

当時の私は自分の仕事が批判されたように捉えてしまっていたため、プロトタイプを社内レビューで見せることに抵抗がありました。 ですが、この定理を読んで、自己本位の開発になっていることに気が付き、これではダメだと衝撃を受けました。 それ以来、プロダクトを誰のために作っているのかという意識を持つようになりました。

現在でも、機能の根幹となる部分から優先的に開発し、早い段階からPdMに都度確認してもらいながら実装を進めるスタイルを取っています。 実際の画面を確認していただくことで必要な情報が欠けていたことに気づくこともあり、細かく試作することでより良いプロダクトを作ることができると感じています。

紹介される定理はいずれもシステム設計の指針を示しており「設計の思想とは何か」を本書を通じて学ぶことができます。

設計を担当される方はもちろん、設計の指針となる考え方を学びたい方にもおすすめの一冊です。

まとめ

いかがでしたでしょうか?

偶然ですが、今回はどれもメンバーの初期キャリアに影響を与えた本の紹介となりました。 このブログを読んでくださった方で、そのような本がある方も多いのではないでしょうか。 久々に読み返してみると、原点に立ち返ったり、新たな気づきを得ることができるかもしれません。

ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。興味を持っていただいた方はこちらのページからご応募お願いします。

herp.careers


  1. ここでいうセンス・オブ・ワンダーとはプログラミングに触れたときに感じた感覚や感動を意味していると私は解釈しています。
  2. 正しくは2005年でした。はてぶのコメントでご指摘があり気が付きました。ありがとうございます!

Nx活用術!Larger runnerの動的設定でGitHub Actionsのコスパ改善!

ファインディ株式会社でフロントエンドのリードをしている新福(@puku0x)です。

皆さん、GitHub ActionsのLarger runnerはご存知でしょうか?

高性能なマシンを使ってCIを実行できる一方、変更の少ない場合や計算負荷の低いCIではコストパフォーマンスが悪くなってしまいがちですよね?🤷‍♂️

この記事では、Nxの機能を利用してLarger runnerを動的に切り替える方法をご紹介します。

Nxについては以前の記事で紹介しておりますので、気になる方は是非ご覧ください。

tech.findy.co.jp

Larger runner(より大きなランナー)

Larger runnerは、「GitHub Teamプラン」または「GitHub Enterprise Cloudプラン」の場合に利用可能です。

docs.github.com

プライベートリポジトリ用の通常のランナー(GitHub-hosted runner)は、Linuxマシンの場合、CPUは2コアとなりますが、Larger runnerでは4コアや8コア、16コアなどより高いスペックのマシンを選択できます。

Larger runnerの例

最近はArmベースのCPUも利用できるようになり、x64ベースのCPUを使う場合よりもコストを抑えられるようになりました。積極的に使っていきたいですね。

課題

Larger runnerは強力ですが、その分コストがかかります。

コードの変更が少ない場合や、CI全体が数分で終わってしまうような場合では、せっかくの高いスペックも宝の持ち腐れとなってしまうでしょう。

負荷が高い時だけLarger runnerのスペックを上げるにはどうすれば良いか? というのが今回の課題となります。

現在のGitHub Actionsには、変更の規模や負荷に応じて動的にランナーを切り替える機能が標準で備わっていないため、自分で組まなくてはいけません。

解決策

単純に変更されたファイル数や行数をカウントしても実際の影響範囲とは乖離があるため、より高度な制御が必要となります。

✨そこで、Nxの登場です。✨

Nxはモノレポ内のプロジェクトの依存関係を Project Graph として保持しており、コードの変更から影響範囲を割り出すことが可能です。

次のコマンドを実行すると、影響されるプロジェクトをJSON形式で取得できます。

npx nx show projects --affected --json

show - CLI command | Nx

あとは実行結果をパースしてLarger runner切り替えの条件を組めば実現できそうです。

ワークフローの例を示します。

on:
  pull_request:

jobs:
  check:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    outputs:
      runs_on: ${{ steps.output.outputs.runs_on }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - uses: nrwl/nx-set-shas@v4
        with:
          main-branch-name: ${{ github.base_ref }}
      - run: npm ci
      - name: Get affected projects
        id: get_affected_projects
        # 1. 影響されるプロジェクト数を算出
        run: |
          length=$(npx nx show projects --affected --json | jq '. | length')
          echo "length=$length" >> "$GITHUB_OUTPUT"
      - name: Output
        id: output
        # 2. 影響されるプロジェクト数に応じたLarger runner名をセット
        run: |
          if [ ${{ steps.get_affected_projects.outputs.length }} -gt 20 ]; then
            echo "runs_on=arm64-4-core-ubuntu-22.04" >> $GITHUB_OUTPUT
          else
            echo "runs_on=arm64-2-core-ubuntu-22.04" >> $GITHUB_OUTPUT
          fi

  build:
    needs: check
    # 3. 指定されたLarger runnerで実行
    runs-on: ${{ needs.check.outputs.runs_on }}
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - uses: nrwl/nx-set-shas@v4
        with:
          main-branch-name: ${{ github.base_ref }}
      - run: npm ci
      - run: npx nx affected --target=build

actions/checkoutactions/cache は適宜最適化しましょう(1分ほど速くなる余地あり)

ワークフローを実行すると、まず影響されるプロジェクト数が算出されます。

後続のジョブでは、その結果を元に runs-on: ${{ needs.check.outputs.runs_on }} で利用するLarger runnerを設定します。

やりたかったことが実現できていますね!🎉

負荷の高い時は高スペックのLarger runnerが動くため、CI時間の短縮が見込めます。負荷が低い時は、Larger runnerのスペックを落としてコストを節約できます。

結果

直近100回のCI結果を元に、4コアマシン固定の場合と2〜4コア可変の場合でコストを計算してみました。

Before(4コア固定) After(2〜4コア可変)
$6.53 $4.79

※ArmベースのCPUを使用する想定でコストを算出しています
※直近100回中、約1割が高負荷なCI(約12分)、残りが低負荷なCI(約5分)でした

Larger runnerを動的に切り替える方法を採用することにより、コストを3割ほど削減できました。

以前の状態と比較してコストパフォーマンスが改善されたと思います。

まとめ

この記事では、Nxの機能を活用してLarger runnerを動的に設定することで、コストパフォーマンスを改善する方法を紹介しました。

今回は runs-on の切り替えのみ紹介しましたが、他にもNx CLIの --parallel オプションの動的設定など応用は様々です。

皆さんの参考となりましたら幸いです。


ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。興味を持っていただいた方はこちらのページからご応募お願いします。

herp.careers