Diverse developer blog

株式会社Diverse(ダイバース) 開発者ブログです。

Diverseが開発生産性を計測する理由

こんにちは、Diverse Developer Blogです。今回は、Diverseの開発組織の生産性と計測結果をどのように活用しているかをご紹介します。

リポジトリごとの開発生産性ダッシュボード

最初に計測中の開発生産性ダッシュボードを公開します。このダッシュボードはGoogleスプレッドシートで作成しており、データは自作したGitコマンドで集計しています。指標や計測の詳細は後述します。

計測対象は、開発が最も盛んで売上の高い弊社のサービス「YYC(https://www.yyc.co.jp)」の3つのリポジトリです。なお、Serverリポジトリは課題解決を優先して実施中なので、ClientやInfraにはない指標を追加しています。

  • Serverリポジトリ:Perl製のサーバー(API/batchなど)でWebクライアントと管理画面も含む
  • Clientリポジトリ:Flutter製のiOS/Androidのコード
  • Infraリポジトリ:AWSのリソースを管理するTerraformのコード

■ Serverリポジトリの開発生産性ダッシュボード

server_development_productivity

■ Clientリポジトリの開発生産性ダッシュボード

client(iOS, Android)_development_productivity

※ デプロイはAppleやGoogleのコンソールに次回バージョンをアップロードしたタイミング
※ ClientのRevertはデプロイ前のQAで発覚した不具合をrevertするため、本番で発生した変更障害の数と同じではありません

■ Infraリポジトリの開発生産性ダッシュボード

infra_development_productivity

なぜ計測するのか

計測する理由は2つあります。ひとつ目は社内的な理由で「開発組織を改善する手がかり」を見つけることです。

弊社では、改善の第一歩は現状を正確に把握することだと考えています。弊社のサービスは20年以上稼働しており、組織や技術に課題があります。これらの課題をできるだけ正確に把握するために開発生産性の計測を始めました。

ふたつ目は社外的な理由で「エンジニア採用時にイメージしやすい情報を使って開発組織の現状を伝えたい」からです。

弊社の採用ピッチ資料*1は全職種向けの会社紹介になっています。エンジニア採用の面接時は採用ピッチ資料に加えて、利用中の言語やフレームワークの話や、プロダクトのコードやタスクの一部を共有して開発組織の現状を紹介しています。つまり、質的な内容だけで開発組織を説明していました。ここに量的な情報も加えて開発組織をさらにイメージしやすい状態にして採用広報を進めたく、開発生産性の計測を始めました。

デプロイの頻度の計測

開発生産性で有名な指標は「デプロイの頻度」「変更のリードタイム 」「変更障害率」「サービス復元時間」の4つです。これらはFour keys*2と呼ばれています。弊社では、計測が簡単で特に重視される「デプロイの頻度」の計測から始めました。

計測はGitで作成したコマンドを各リポジトリで実行して月初にデータを集計しています。デプロイ頻度の計測方法を説明する前に弊社のデプロイフローを説明します。フローは以下のとおりです。

  1. mainブランチからreleaseブランチに向けてPRを作成する
  2. releaseブランチにPRをマージする
  3. CI/CDが本番に変更をデプロイする

つまり、releaseブランチへのPRマージ数が「デプロイの頻度」と同じになります。このマージ数を月間で集計するGitコマンドは以下のとおりです。これで毎月どれだけのデプロイがあったのか数値で把握できます。

# releaseのマージコミット数だけ対象(--since=で対象開始日を指定)
git log --merges --date=format:%Y-%m --pretty=format:'%ad %s' --since="2022-01-01" release \
| awk '{print $1}' \
| sort \
| uniq -c \
| awk '{print $2","$1}' \
| sed 's/^ *\([0-9]*\) /\1,/'

# 実行結果はCSV形式の日付とマージ数となります
2022-01,32
2022-02,27
2022-03,40
<省略>
2023-07,77

d/d/dの計測

次に計測した指標は、d/d/d (deploys/ a day / a developer)です。d/d/dは、1日あたりのデプロイ回数を開発者数で割った数値で0.1以上だと、健全な開発組織と判断する指標*3です。祝日と週末を除いた営業日数を月ごとに算出しておき、現在の開発者数*4と当月のデプロイ数を計測してd/d/dを算出します。

最近はd/d/dを計測する企業も増えましたが、d/d/dを知らないと数値を見ても開発組織の状態をイメージしづらいと思います。そこで弊社では「デプロイ数のデイリー平均(23/01~前月)」も算出して日々の開発をイメージしやすい数値でも表示しています。

Serverは1日3回ほど、Infraは1日0.8回なので2日1度ぐらいの頻度でデプロイすると言えます。一方、審査が発生するClient(iOS,Android)は少し解説が必要です。Clientは7月分の計測終了時点でデプロイ数のデイリー平均が0.27回となっています。

ClientリポジトリはreleasブランチにPRをマージすると、CI/CDでAppleやGoogleのコンソールに次回のバージョンをアップロードします。弊社のiOSとAndroidはこのタイミングをデプロイとカウントします。そして、弊社のスクラムは1スプリントを5日(土日祝日を除く1週間)にしています。つまり、5日に1度はデプロイする頻度(0.27回 x 5日 = 1.35デプロイ/週)だと言えます。

変更障害率とRevert数の計測

Four keysの「変更障害率」はRevertのコミット数を代用して計測しています。コミットメッセージの先頭にRevertやrevertのメッセージが含まれるコミットが対象です。弊社では、デプロイの変更に問題があった場合、 git revert して復旧作業を行います。Revertのコミット数を計測するGitコマンドは以下のとおりです。

# releaseのRevertコミット数だけ対象(--since=で対象開始日を指定)
# 補足: 極稀にRevertを打ち消すRevertコミットがあるため、コミットメッセージでRevertやrevertが重複するコミットはperlワンライナーで対象外にした
git log --grep="Revert\|revert" --date=format:%Y-%m --pretty=format:'%ad %s' --no-merges --since="2022-01-01" release \
| perl -ne 'print if /^(\d{4}-\d{2}) (Revert|revert)/i && (()= ($_ =~ /(Revert|revert)/ig)) < 2;' \
| awk '{print $1}' \
| sort \
| uniq -c \
| awk '{print $2","$1}' \
| sed 's/^ *\([0-9]*\) /\1,/'

# 実行結果はCSV形式の日付とRevertコミット数となります
2022-01,3
2022-02,1
2022-03,5
<省略>
2023-06,2
2023-07,6

提供するサービス内容によって水準は代わりますが、Four keysでは、最も高いパフォーマンスレベルのエリートで変更障害率は15%以下です*5。弊社のServerでは10%台になる時期もありましたが、単体テストの増加やWeb側にe2eテストを導入して改善を試みています。直近数ヶ月はデプロイ頻度が増加しても、変更障害率が10%をこえない状態を維持しています。

なお、Client(iOS, Android)のみ git revert の運用方法が他リポジトリとは異なります。本番リリース前のQAで発覚した不具合に git revert を行うため、本番で発生した変更障害と同じではありません。

廃止/削除のPRマージ率の計測

最後に弊社独自の指標である「廃止/削除のPRマージ率」を説明します。「廃止/削除のPRマージ率」とは、PRタイトルに廃止または削除の文字があるPRをmainブランチにマージした数(廃止/削除PRマージ)を、mainブランチにマージしたPRの数(PRマージ)で割った数値です。

廃止/削除PRマージ ÷ PRマージ = 廃止/削除のPRマージ率

この指標は社内のエンジニアの「廃止や削除のPRでデプロイ数が増加したのであって、ユーザーへの価値提供は増えてないのでは?」という意見から生まれました。不要な機能やコードが減って開発効率が上がるのは、基本的には良いことです。しかし、デプロイの量だけではなく質にも注目すべきと考えて計測を始めました。

Serverのダッシュボードを見ると、23年3月から6月は「廃止/削除のPRマージ率」が20%以上です。これはサービス内で利用するポイントシステムの抜本的な改善に着手したのが原因です。20年以上サービスを運営すると、現在のニーズに合わない機能や古いポイントシステムが残ったりします。そこで機能の廃止やコードの削除を改善と同時に実施しました。そして、7月にポイントシステムの改善がほぼ完了したため、現在の「廃止/削除のPRマージ率」は11%まで低下しました。

このようにポイントシステムの改善という明確な方針があって「廃止/削除のPRマージ率」が上昇するのは妥当な結果です。改善に必要な廃止や削除だったと説明できれば、チーム内の認識のズレが少なくなり、より良い振り返りがチームで行なえます。

「廃止/削除のPRマージ率」はどの程度が適切な数値でしょうか。5%以下だと機能やコードを継ぎ足してばかりな可能性がありそうです。逆に30%以上だと、コードや機能の質に問題があったり、開発環境の改善を優先し過ぎなどの振り返りができそうです。この数値の適切な水準は模索中ですが、計測し続けると見える改善点もあると考えています。

まとめ

弊社の開発生産性の指標と活用事例を紹介しました。開発組織を数値で振り返り、社内外にイメージしやすい形で公開できると、開発組織に対する認識のズレが少なくなり、より良いコミュニケーションのきっかけになります。

だだし、弊社はこれらの指標の変動を人事評価に使ったり、目標にしたりはしません。数値の改善が目的ではなく、数値が示す現状から何を改善すべきかを知る手がかりに過ぎないと考えているからです。

読者のみなさんは弊社の開発生産性から、どのような課題や改善が見えますか?もし、何か気づいた方は以下の採用ページからカジュアル面談へ!様々な意見をお待ちしています。

diverse-inc.co.jp

YAPC::Kyoto 2023に参加して学んだこと

こんにちは、Diverse developer blogです。

3月19日(日)に開催されたYAPC::Kyoto 2023に弊社CTOがリモートで登壇し、若手社員1名が現地参加しました。今回は、現地参加した社員の体験レポートです。

yapcjapan.org

YAPCに初参加

京都観光も満喫して帰ってきましたが「ブログを書くまでがYAPC」との事なので、まだYAPC終わっておりません。

Perl歴2年弱でYAPCは今回が初参加でした。参加経緯は会社からスポンサー特典により配布されたチケットがある&交通費もサポートしてくれると声を掛けていただき、得るものも多そうだと参戦!

当日はスタッフ含め300人前後の人が参加されたようで、会場はお祭りのような雰囲気でワクワク。知り合いがいない中での参加で、コミュニケーションはあまり取れなかったのが心残りですが、ひたすらセッションやトークを聴いて周るだけでも楽しかったです。

また、出展ブースで他社の方から直に話を聞けたのも良い機会でした。

個人的に印象深かったセッションやトーク

売上と開発環境を同時に改善するために既存のPerl Web アプリケーションをどのようにリプレイスするか

弊社CTOが登壇!発表はリモートからとなりましたが、京都の会場から聞きました。

売上と開発環境を同時に改善するためにPerl Webアプリケーションをどのようにリプレイスするか - Speaker Deck

入門 障害対応「サービス運用はTry::Catchの繰り返しだよ、ワトソン君」

障害対応はエンジニアをしていたら逃がれられないので、本編の気をつけている話は勉強になりました。playbookはいい考えで、熟練メンバーの思考は気になるところですね。

入門障害対応 - Speaker Deck

ソフトウェアエンジニアリングサバイバルガイド: 廃墟を直す、廃墟を出る、廃墟を壊す、あるいは廃墟に暮らす、廃墟に死す

過去の携わったプロジェクトやシステムを振り返って「廃墟」に該当するものや思い当たるものがあってとても興味深い内容でした。「技術的負債」をはたして正しく認識できていただろうかと考えさせられました。ぜひ、内容は公開されているスライドを参照ください。

そして何より@moznionさんのプレゼンが面白い。全体的にトークが上手で話に惹き込まれました。

ソフトウェアエンジニアリングサバイバルガイド: 廃墟を直す、廃墟を出る、廃墟を壊す、あるいは廃墟に暮らす、廃墟に死す - Google スライド

他に

全体的に今が旬のChatGPTが話題に上がるセッションも多かった気がします。「春のエンジニアぶつかり稽古 2023」では、Perlで書いたコードをChatGPTに「良いところ、悪いところ」について評価させたり。ランチセッション「今出川FM公開収録」ではまさにトークテーマがChatGPT。豪華なお弁当をいただきながら楽しく聞くことができました。

YAPC::Kyotoのアーカイブを後日youtubeで公開していただけるようで、ベストトーク賞「あの日ハッカーに憧れた自分が、「ハッカーの呪縛」から解き放たれるまで - Speaker Deck」は今回見れなかったので公開を楽しみに待ちたいと思います。他にも並行して聞けなかったトークも沢山あるのでありがたいです。

最後に

今回参加できて良かったです!

リモート参加も会場や時間を問わずメリットも沢山ありますが、オフラインも参加しないとわからない会場の雰囲気というか熱気がありますね。参加者の皆さんとても楽しそうでした。

次回はYAPC::Hiroshimaということで、そちらの続報も待ちたいと思います。 運営、スタッフ、参加者の皆様お疲れさまでした!素敵なイベントをありがとうございました。

AWS JumpStart 2023 設計編に参加して学んだこと

はじめに

こんにちは。 Diverse developer blogです。

2023年3月8日(水)〜9日(木)にオンラインで開催されたAWS JumpStart 2023設計編に参加してきたので、その体験レポートについて書いてみました。

AWS JumpStart とは

AWS JumpStartは新卒やAWS初学者のエンジニアを対象とした、AWSによる実践的な研修プログラムです。

この研修では一般的なリファレンスアーキテクチャAWSのコアサービスの概要その選定基準について理解することをゴールとして設定されていました。

期間は3月8〜9日の2日間で、1日目に講義やハンズオンなどの個人ワークがメインで、2日目には5,6人のグループになって与えられたプロダクトの要件にしたがってアーキテクチャを検討するというプログラムになっていて、参加者は総勢500名を超えていました。

参加時点の筆者のスキルレベル

  • Webコーディング(HTML, CSS) 5年以上
  • Reactを使ったアプリケーションの開発 1年以上2年未満

現時点の筆者のスキルレベルはざっくりと上記のようになっていて、業務ではフロントエンドがメインでインフラ部分は触る機会がなく個人開発では何も考えずにバックエンドはFirebase、ホスティングサービスはVercelに任せるなどしていて、深くその部分のアーキテクチャについて検討したことがありませんでした。

なので研修に対してついていけるか若干不安でしたが、事前に共有された動画があったおかげで問題なく理解することができました。

研修の流れ

1. 講義

講義では事前学習の内容に加えて、アーキテクチャを検討する上でのポイントやそれに応じたAWSのサービスについて話してくれました。

可用性や拡張性、コストの最適化などの検討項目について説明し、それを解決するAWSサービスについて紹介してくれていました。

2. ハンズオン

1である程度AWSやアーキテクチャ設計に関する知識を蓄えた状態で、実際にAWSコンソールを触ってToDoアプリの構築をしました。

AWSのコンソールを触る機会もあまりなかったので具体的にどうやって構築するのか?という部分がこの時間でできるようになりました。

3. クイズ

クイズでは「こんなプロジェクトがあった際にどのAWSサービスを利用するか」といった架空のプロジェクトを想定したアーキテクチャの選定に関する問題を出題して、Slackのリアクションやテキストで答えていくという流れで行いました。

事前学習や1の講義学んだことを理解しているか、また補足することで理解を深めることができました。

クイズの内容とは関係ありませんが、クイズの回答をSlackのリアクションで答えていくという流れでリアクションが数十秒で400件を超えるのをみてるのが個人的に楽しかったです。笑

4. アーキテクチャ課題

ECサイトの開発メンバーという設定で参加者5,6人くらいで1グループになりアーキテクチャを検討するといった内容でした。

ECサイトという要件に加えて以下の項目も考慮することが求められました。

  • 可用性
  • スケーラビリティ
  • パフォーマンス

検討は1日目の夕方から2日目の夕方くらいまで行い、最後にランダムで選ばれた3グループが発表しAWSの方からフィードバックをもらうというものでした。

わからないことがあれば都度質問ができる状態だったので自分の分からない点を説明してもらいながら、理解を深めることができました。

最終的に自分達のグループは以下のようなアーキテクチャ図になりました。

また、ある程度AWSを触っている方も参加者の中にはいたので発表やそのフィードバックも新しい気づきになりました。

参加してみて

AWS JumpStartに参加してみて、AWSまったく分からないという状態からちょっとわかる状態になりました。

また、実際にDiverseのプロダクトではどんなアーキテクチャが採用されているのかに興味が持てたので良い機会になりました!

Diverseはエンジニアを募集中です

Diverseは2022年7月に会社のビジョン(パーパスとミッション)をアップデートし、現在第二創業期を迎えています。少しでも興味のある方は、以下の採用ページからカジュアル面談へよろしくお願いいたします。

diverse-inc.co.jp

YAPC::Kyoto 2023にシルバースポンサーとして協賛します

こんにちは。 Diverse developer blogです。

Diverseは2023年3月19日(日)に開催される YAPC::Kyoto 2023 にシルバースポンサーとして協賛いたしました。

yapcjapan.org

YAPC::Kyoto 2023 とは

YAPCはYet Another Perl Conferenceの略で、Perlを軸としたITに関わる全ての人のためのカンファレンスです。 Perlだけにとどまらない技術者たちが、好きな技術の話をし交流するカンファレンスで、技術者であれば誰でも楽しめるお祭りです!

DiverseはプロダクトのサーバーサイドをPerlで開発しており、当日は弊社CTOの須藤が登壇します。

https://yapcjapan.org/2023kyoto/timetable.html#talk-123

Diverseはエンジニアを募集中です

Diverseは2022年7月に会社のビジョン(パーパスとミッション)をアップデートし、現在第二創業期を迎えています。少しでも興味のある方は、以下の採用ページからカジュアル面談へよろしくお願いいたします。

diverse-inc.co.jp

FlutterKaigi 2022にシルバースポンサーとして協賛しました

こんにちは。 Diverse developer blogです。

Diverseは、2022年11月16日(水)~18日(金)にオンライン開催された FlutterKaigi 2022 にシルバースポンサーとして協賛いたしました。

flutterkaigi.jp

FlutterKaigi 2022 とは

FlutterKaigiは、Flutter/Dartをメインに扱う日本の技術カンファレンスです。Flutterエンジニアの有志による実行委員会が、Flutter/Dartの知見や情報の共有、コミュニケーションを目的に開催しています。

FlutterKaigiは今年で開催2回目となりました。 当日の発表内容はYoutubeのライブから閲覧可能です。

FlutterKaigi - YouTube

Diverseはエンジニアを募集中です

Diverseは2022年7月に会社のビジョン(パーパスとミッション)をアップデートし、現在第二創業期を迎えています。少しでも興味のある方は、以下の採用ページからカジュアル面談へよろしくお願いいたします。

diverse-inc.co.jp

後編:歴史ある婚活サービスyoubrideをリプレイスしようとしていた話

Diverseの須藤(id:kurotyann) です。

今回はリプレイスの結果の後編を共有します。

主にフェーズ1〜2の技術的な知見について書きました。

なお、「前編:歴史ある婚活サービスyoubrideをリプレイスしようとしていた話 - Diverse developer blog」でもお伝えしたとおり、このリプレイスは今年3月に経営判断により終了しました。残念ながらバックエンドのNestJSはプロダクションにデプロイされることなく終了しました。NestJSは多数の会社でプロダクション運用の実績があり、採用する価値のあるフレームワークです。弊社も本番へデプロイして、さらなる知見を得てコミュニティに還元したかったのですが、それは次回のプロジェクトへ持ち越しとなりました。

このブログでは本番デプロイは叶わなかったものの、それでもニーズがありそうな知見に絞って成果を報告したいと思います。

TypeScriptを主軸にした構成

このリプレイスを開始した昨年10月に弊社の開発言語をTypeScriptとDartに統一する方針を定めました。弊社のエンジニア組織の体制を考慮したとき、この二つの言語への投資が最もユーザーのニーズに応えやすい体制になると考えました。詳細は、以下の記事を参照してください。

このリプレイスは、TypeScriptとDartのメリットを実感するためのプロジェクトとして開始した一面もあります。DartはモバイルアプリをFlutterで開発中のため、TypeScriptはモバイル以外を担当する言語となります。今回はフロントエンドにNext.jsを、バックエンドにNestJSを採用しました。どちらもTypeScriptをフルサポートするフレームワークで、フロントからバックエンドまで同じ言語で、かつ各種npmパッケージ(ESlintやPrettierなど)の恩恵を受けながら開発できます。

リプレイスは前編で説明したように、利用規約などの静的なページから移行を開始しました。既存の構成に、Next.jsやNestJSをAWS ECS(Fargate)で追加し、ELBのルーティングを修正するのがリプレイスの主な開発フローです。赤枠が今回のリプレイスで追加修正されたリソースです。

リプレイスで追加修正されたリソース(赤枠)

Next.jsは静的なページをSSGで生成し、生成したファイルのキャッシュをNginxが担当するシンプルな構成にとどまりました。この後編ではTypeScriptで統一した開発環境や、NestJSやPrismaなどのニーズの多そうな知見に絞って紹介したいと思います。

1. Monorepo構成

TypeScriptで統一したので、リポジトリもモノリポ構成にしました。リポジトリのディレクトリ構成を簡単にした図がこちらです。

.
├── api
│   ├── infra
│   ├── initdb.d
│   ├── prisma
│   ├── src
│   └── test
├── mobile
│   └── api-client
├── openapitools.json
├── renovate.json5
└── web
    ├── cypress
    ├── infra
    ├── nginx
    ├── public
    └── src
        └── api-client

apiがNestJS、webはNext.jsのディレクトリです。mobileのディレクトリは、OpenAPI Generatorで自動生成したAPIクライアントのディレクトリです。別リポジトリで開発中のFlutterの pubspec.yaml から参照しています。モバイルだけでなく、webにもOpenAPI GeneratorでAPIクライアントコードをsrcの直下に自動生成しています。

apiとwebにあるinfraディレクトリには、CI(GitHub Actions)でECS(Fargate)へデプロイするときに参照する task-definition.jsonDockerfile を環境別に配置しています。TerraformとFlutterはリプレイス前から別リポジトリで管理中なので、このモノリポに含めません。

課題

今回は、NxやlernaやTurborepoなどのモノリポ管理ツールは利用しませんでした。npm の --prefix や、各ディレクトリに配置した Makefile にコマンドを集約して、make -C ../api command_name でコマンド実行を楽にしていました。

モノリポ管理ツールは、npmの共通パッケージを集約してビルドキャッシュできるなど利点はあるものの、今回のリプレイスに必須ではないと判断して見送りました。次回で挑戦します。

2. NestJSはDDDベースのModule分割

NestJSはModuleごとに機能を隔離するアーキテクチャをとり、Moduleごとの複雑さを管理しやすくする設計をサポートしています。しかし、機能の分割単位を誤ると、Module同士の依存関係が増えてコードが密結合になり、開発が大変になります。弊社のModuleは以下のDDD構成です。

/**
 * プレゼンテーション (Controller)
 *  ↓
 * サービス (Service)
 *  ↓
 * ドメイン (Domain)
 *  ↑
 * インフラ (Repository)
 */
api/src/modules
└── auth
    ├── auth.controller.ts
    ├── auth.module.ts
    ├── domain/
    ├── dto/
    ├── repository/
    └── service/

弊社ではModuleはドメイン単位で分割して、Module同士が依存する関係を避けるようにしました。つまり、Moduleから他のModuleを呼び出すのは原則禁止です。モジュール内に影響を閉じ込める設計を優先します。その代わり、Module間で似たような処理が重複するのは許容しました。例えば以下のとき、重複を許容します。

  • リポジトリ層で、userテーブルの呼び出しが多数のモジュールに記載される。
    • 例:authやuser
  • ドメイン層で、似たようなモデルや処理が記載される。
    • 例:authやuser

しかし例外として、どうしても共通Moduleとして切り出す方が効率的である場合は common/ に切り出します。なお、この共通モジュールはプレゼンテーション層(controller)を持ちません。そして、ドメイン知識を含まないもの、共通利用が確実な小さな処理は、utils/ に切り出しました。最後に、環境変数やORM(Prisma)など、あらゆる箇所から呼ばれるModuleを @Global にしてlibs/ に配置します。

api
 └── src
     ├── libs
     │   ├── config
     │   ├── prisma
     │   └── sentry
     ├── modules
     │   ├── auth
     │   ├── common
     │   ├── health-check
     │   └── user
     └── utils
         └── validator
             └── class-validator.ts

課題

このModuleとディレクトリ構成で開発した期間がまだ数ヶ月なため、大きな課題はありませんでした。ただ、リプレイスを進めてコードが増えると、この設計でも辛みがでるかもしれません。そこは次回のプロジェクトで試行錯誤したいところです。

3. テスト時のPrismaのseedデータのリセット

開発時のテストデータや、テストケースごとのseedデータのリセット方法です。DBの初期化とユーザー作成は docker compose up 時に docker-entrypoint-initdb.d/init.sh で可能です。

// api/initdb.d/init.sh

#!/bin/bash

mysql -u root -ppassword < "/docker-entrypoint-initdb.d/0_setup/database.sql"

テーブル作成とseedデータのリセットもPrismaで簡単にできます。api/prisma/seed.ts に投入データを非同期で書きます。

// api/prisma/seed.ts

import { PrismaClient } from '@prisma/client'
import { createTestUser } from './seeds/user'

const prisma = new PrismaClient()

async function main() {
  // test user
  await createTestUser(prisma)
}

main().catch(e => {
  console.error(e)
  process.exit(1)
})

// api/prisma/seeds/user.ts

import { PrismaClient } from '@prisma/client'

export const createTestUser = async (prisma: PrismaClient): Promise<void> => {
  await prisma.user.upsert({
    where: {
      id: 1,
    },
    update: {},
    create: {
      id: 1,
      email: 'test_user01@local.youbride.jp',
      gender: 1,
      birth: new Date('1980-12-24'),
      status: 1,
      created_at: new Date(),
      updated_at: new Date(),
    },
  })
}

あとはDBコンテナが起動中に docker compose run --rm nestjs npx prisma migrate reset --force すれば、DBがテストデータの初期状態にリセットされます。ローカルで開発中に何度でもやり直しが効くので便利です。

これを単体テストやE2Eテストをjestで走らせたとき、テストケースごとにやりたい場合 beforeAll などで、 execPromisify('npm run prisma:reset') を呼び、無理やりDBをテストデータの初期状態にリセットしていました。

import { exec } from 'child_process'
import util from 'util'

const execPromisify = util.promisify(exec)

/**
 * prisma:reset は以下を実行する
 *  1. DBテーブルを削除
 *  2. DBテーブルを作成(migration)
 *  3. DBテーブルにシードデータを投入(seed)
 *
 * @see api/prisma/migrations
 * @see api/prisma/seeds
 * @see api/prisma/seed.ts
 * @see https://www.prisma.io/docs/guides/database/seed-database#integrated-seeding-with-prisma-migrate
 */
export const resetDatabaseTables = async () => {
  // package.json の scripts に 
  // "prisma:reset": "npx prisma migrate reset --force" がある前提
  await execPromisify('npm run prisma:reset')
}

// auth-signup.repository.spec.ts

import { resetDatabaseTables } from '@/../test/utils/prisma/prisma-util'

beforeAll(async () => {
  await resetDatabaseTables()
  const moduleFixture: TestingModule = await Test.createTestingModule({
    imports: [ConfigModule, PrismaModule],
    providers: [AuthSignupRepository],
  }).compile()

  repository = moduleFixture.get<AuthSignupRepository>(AuthSignupRepository)
})

課題

この対応でテストケースごとにDBの状態を気にすることなく、テストコードが書けます。しかし、参照するDBが一つなのでテストを直列で実行(jest --runInBand)しないと実行順やタイミングでテストが失敗します。 さらに、直列なのでテストケースの増加に比例してテスト時間も増えるし、npm run prisma:reset も毎度2~3秒かかります。

ぱっと思いつく解決策として...

  • テスト時に参照するDBを複数にする
  • DBの状態に依存しないテストケースと、そうでないケースを別ディレクトリに分ける
  • DB接続は諦めてテストデータをモックにする

などがありそうです。テストを書くモチベーションに直結するので、次回のプロジェクトでは良い解決策を見つけます。

ELBのルーティングルール管理

TypeScriptやDartの話だけでなく、リプレイスの苦労も共有します。一番苦労したのは新旧画面のルーティングです。

段階的に画面をリプレイスする場合、画面のパスを新旧で別にして共存させるか、パスの転送先を新旧で切り替えるかだと思います。弊社では後者を選びました。

ELBのルーティング

課題

このとき、新画面のルールを旧画面のルールより優先するようにTerraformで書く必要があります。つまり、aws_lb_listener_rulepriority を期待するルールの昇順に並び替えないといけません。しかし、terraform applyしたとき、もし既存ルールの優先度と追加したルールが重複すると、applyが中断されてルーティングが崩れた状態でaws上にリスナールールが構築されます。

追加ルールと、既存ルールの priority が重複しないか確認しながら、terraform apply するよりも、awsコンソールから手動でルールを追加後、優先度が変わったルールを terraform import する方が安全だと判断し、フローを変更しました。

  • ① 新ルールを既存のルールの優先度と被らないように追加する
resource "aws_lb_listener_rule" "example-replace" {
  listener_arn = aws_alb_listener.production.arn
  priority     = local.replace-base-priority + 2 // 次のルールは重複を避けて3にする
  • ② 追加したルール数に合わせて、XXXX-rule-countを変更する
locals {
  base-priority         = 1
  base-rule-count       = 1
  replace-rule-count    = 3
  replace-base-priority = local.base-priority + local.base-rule-count
  prod-base-priority    = local.replace-base-priority + local.replace-rule-count
}
  • ③ PRレビューはするがmasterには一旦マージしない
    1. ①と②の修正をレビューしてもらう
    2. マージすると優先度の重複で失敗する可能性があるのでマージはしない
  • ④ AWSコンソールで対応する環境のリスナールールに ① のルールを追加する
  • ⑤ ルールが正常に切り替わったらterraform import でルールを追加する
  • terraform plan で差分なければPRをmasterにマージする

awsコンソールで新ルールを手動追加しつつ、terraformのローカル変数でルールの重複を避ける作戦です。あまり賢い解決策ではないので、ここも良い解決方法を模索したいです。

最後に:成果をつなげていく

これまでDiverseはPerl、Ruby、Dartでの開発経験はあるものの、TypeScriptでの開発経験や歴史がまだ浅い会社です。リプレイス中は、毎週火曜日に弊社で技術知見共有会(Tech-DLN)を開き、TypeScriptをはじめリプレイスで得た知見を社内に共有していました。リプレイスを機会に社内での技術共有が進みはじめたので、その推進力を次のプロジェクトへと繋げます。

リプレイスは終了しましたが、次のプロジェクトでもTypeScriptとDartでの開発を続ける予定です。このような開発体制に興味のある方、もっと良いやり方があるなど、カジュアルに話したいことがある方は是非お話しましょう!

前編:歴史ある婚活サービスyoubrideをリプレイスしようとしていた話

Diverseの須藤(id:kurotyann) です。

昨年10月から婚活サービスのyoubrideのリプレイスを開始しました。

しかし、このリプレイスは今年の3月に経営判断により終了しました。

このリプレイスを最後までやり切りたかったですが、Diverseの今後の成長を考慮した判断となります。5ヶ月間という短期間ですが、得られた知見は多く、Techブログに結果を残すことにしました。

今回は、以下の2部構成でリプレイスの結果を共有し、このブログは前編になります。

世の中のプロジェクトは成功か失敗のどちらかに区別されます。しかし、今回のリプレイスは成功とも失敗とも言い切れないまま終了しました。ある意味で珍しい事例です。この成果が社内だけなく、サービスのリプレイスを検討したり、実施中の社外の方々への参考になると幸いです。

老舗サービスの変遷と構成

婚活サービスのyoubrideは、スマホがないガラケー時代からあるサービスです。運営会社が変わりながら、2007年2月にyoubrideというサービス名になりました。開発言語もJavaからPHPへと変わり、2013年ごろのリニューアルと同時にPHPからPerlへ変更されています。さらに、モバイルクライアントは、2019年にFlutter+gRPCの構成へリプレイスされています。詳細は過去のTechブログをご覧ください。

現在のyoubrideの構成を簡略化した図がこちらです。今回のリプレイスに関係するリソースに絞って構成を書いてます。

youbrideの構成図(簡略化)

構成図を説明すると、SledgeというPerlのWebフレームワークが、PCとスマホ向けのWebページを別々にレンダリングしつつ、一部のREST APIを担当しています。そして、一部のWebページはWebViewを経由してFlutterで参照しています。gRPCはモバイルアプリのAPIであり、Flutterからのみ呼び出されます。他にもサービスの管理画面や各種バッチ処理が、PerlやRubyで稼働しています。

なぜリプレイスするのか

リプレイスを決めた理由は「サービスの改善速度を低下させる技術的負債を解消するため」です。これを解決すると、youbrideを起点に会社(経営/組織/技術)を改善することに繋がっていくと考えました。

youbrideの技術的負債は、まずはPerlへの依存です。Perlから得られるメリットは年々少なくなっています。さらに残念ながら、Perlを使ってサービスを改善したい人が社内外で減少しています。最も改善すべきドメイン部分が全てPerlなため、改善する人手が足りず、改善コストが上がってしまいます。

もう一つの負債は、Webページがモバイル、フロント、サーバーで密結合している点です。あるWebページのデザイン変更のとき、モバイルアプリ、PCのWeb、スマホのWebと3つの領域に影響が出ます。もちろん、これは場合によってはメリットです。サービス共通で表示する静的なページ(例:利用規約、会社概要など)は変更コストが低く、変更漏れの心配も最小限にできます。

しかし、youbrideは静的ページ以外でも共通で利用中のWebページが複数あります。特に課題なのは、新規登録のオンボーディングページです。各プラットフォームのUIコンポーネントで体験を最適化したいのに共通であるため、各プラットフォームの変更が大変でした。理想は、モバイルはFlutterでUIをレンダリング、Webはレスポンジブにレンダリングして、新規登録に必要なAPIをサーバーに用意してリクエストです。

近い将来は、以下の構成へ段階的にリプレイスしようと計画していました。なお、開発言語はTypeScriptとDartへ統一する計画です。

リプレイス後の理想の構成図

TypeScriptとDartへ統一する背景は過去の記事をご覧ください。

リプレイスチームを作る

リプレイス専任のチームを作りました。このチームのメンバーは、youbrideを改善中の現行チームからは編成しませんでした。理由は現在のサービス改善速度を下げずに、リプレイスを進めるためです。

チームメンバーはCTO室から3名と、業務委託から3名の合計6名で進めました。業務委託は週1~2の稼働で技術的なアドバイスを担当し、リプレイスを行うのはCTO室の3名です。

フロント、バックエンド、インフラと全領域を3名で担当するため、TypeScriptやDartで統一したのは間違いではなかったと思います。工数の見積もりやタスクのアサインなどが、少し楽になったと私がPMをしていて感じました。

リニューアルではなくリプレイスです

リプレイスの基本方針を以下にまとめました。

  1. 画面デザインやAPI仕様は「基本的に現行のまま」とする
  2. ただし、「ニーズに合わない仕様やデザインまで移行しない」
  3. 変更は「ユーザーのニーズを損なわず、工数削減できる変更なのか」を考える

つまり、リニューアルではないことを前提にリプレイスを進めようとしました。仕様を一度に変更するとデグレが起きて失敗する可能性が高いです。また仕様調査を十分にしたとしても、次はどう仕様変更するかを考える調整コストが発生します。

リプレイス担当とサービス改善担当のチームを別にしたのは、リプレイスに注力するためです。ユーザーのUX改善するリニューアルなどは、リプレイスの後にしました。

もう一つ、この方針にした理由は、デザイナーリソースが足りず、リプレイス専任でデザイナーをアサインできなかったのもあります。リプレイスの対象画面の新デザインまでエンジニアに負担させると、作業負担が重すぎると判断しました。よって、画面デザインはそのまま、ただし容易に変更可能だったり、廃止可能な部分は「ユーザーのニーズを損なわない範囲」で対応するとしました。

段階的にリプレイスする

リプレイスは段階方式で実施しました。一気に変更は加えず、リプレイスをフェーズとステップの段階にわけ、各フェーズの変更を本番へデプロイします。youbrideは歴史あるサービスで機能も豊富です。プラットフォームもモバイルアプリからWeb(PC/スマホ)まであります。一気に変更して広範囲に影響が及び、安全なデプロイが困難になる事態を避けようとしました。

さらに、段階的な本番デプロイで成果を確認しやすくしたかったのも理由の一つです。フェーズごとの変更に不具合がないか、フェーズの終盤で変更を本番へデプロイして確認します。フェーズは安全にデプロイできる範囲で分割しています。フェーズの対象範囲が大きすぎる場合は、さらにステップに分割しました。実際に作成したリプレイスのロードマップはこちらです。

youbrideリプレイスロードマップ

このロードマップは1枚でリプレイスの計画を伝える資料です。左から右へ進み、フェーズ0からフェーズ7へと進みます。フェーズ0はリプレイス前の状態です。youbrideはプロダクトチームがサービスの改善を担当しています。フェーズ1から、リプレイスを専任で担当するCTO室がリプレイス作業を開始します。フェーズ3までCTO室は関わり、フェーズ4以降は既存チームに移譲できる体制にまで改善する計画をつくりました。フェーズ4以降の期日は、フェーズ1~2の成果で決める予定でした。

フェーズ1では、静的ページ(会社概要や利用規約など)のリプレイスのために以下を実施しました。

  • Next.jsやNestJSのツールをセットアップ
  • Terraformで必要なAWSのリソースをセットアップ
  • CI/CDやドキュメントの整備
  • 静的ページを本番へデプロイする

AWS ELB(主にALB)のリスナールールを使い、新旧画面のパスを切り替えながらリプレイスします。失敗したとしても、ユーザーへの影響が限定的なページで試しながら、新しい構成へ安全にデプロイできる環境をフェーズ1で整えました。

実際にはフェーズ1は予定より早く完了し、次のフェーズ2へ進んでいました。フェーズ2に認証処理を選択したのは、以下が理由です。

  • 認証を突破しないと、他の主機能へのアクセスが技術的に困難なこと
  • モバイルアプリの登録やログインはWebViewであり、改善要望が多かったこと
  • Flutterから、Perlでレンダリングされた画面の依存を無くし、モバイル側の開発速度を改善したかったこと

なお、このフェーズ1〜2の技術的な知見は後編にて説明します。

Notionへの移行

リプレイスの開始と同時にドキュメントシステムをDocBaseからNotionへ移行しました。Notionへの移行もリプレイスに必要な準備でした。リプレイス中に、youbrideの仕様を新たにドキュメントにまとめ直すことにしたのです。

10年以上つづくyoubrideの仕様を把握している社員はおらず、その仕様がまとまったドキュメントも十分にはありません。DocBaseには利用価値のあるドキュメントなのか、微妙なページが多数存在しました。これらもサービス改善速度を低下させる一つの要因でした。

リプレイスのまとめ方はフェーズやステップごとにドキュメントを分け、ページ内の構造をテンプレにしました。さらに、今後の新規参入者も考慮してオンボーディング資料もまとめていました。

そして、プロダクトチームに現在も有効なドキュメントをDocBaseからNotionへ移行してもらいました。ドキュメントが多いため、すべての移行が順調に進む訳ではないですが、今後の仕様をどう書き残すか一定の方向性ができました。システムだけでなくドキュメントも試行錯誤しながら、改善するサイクルが出来てきました。

Notionへの移行は、別記事でもまとめているのでご覧ください。記事では今年の1月となっていますが、リプレイス専任チームは先行してNotionを使って移行準備を進めていました。

リプレイスは計画的に

ここまで「リプレイスの理由と、リプレイスをどのように進めたのか」を紹介しました。youbrideのリプレイスは一人では不可能でした。複数のチームとメンバーへの説明と理解が必要です。事前の計画は必須でした。

リプレイスは計画を立て社内に共有後、実行します。想定とズレた部分は適宜修正しつつ、ゴールに向かって進む。書いたり言ったりするのは簡単ですが、実際に実行すると本当に大変です。このとき計画がないと軸がなく、メンバーが混乱してチームがバラバラになります。

多くの場合、リプレイスしたいサービスは歴史があり、機能も豊富で仕様も複雑でしょう。計画を立てることが大変な作業で、とりあえず見切り発車したくなります。それでも計画は誰かが事前にたてる必要があります。なぜなら、絶対にすべては思うようにいかないからです。そして、繰り返しになりますが、一人ではできないからです。

このリプレイスも計画どおりに進まず、経営判断での停止で終了しましたが、計画がなければ経営判断の前に終わっていたでしょう。リプレイス完遂という形で成果は共有できませんが、得た知見は今後の会社の成長にとって価値あるものでした。後編では、その成果を書きました。