LUUPのサーバーチームの現状と課題
こんにちは、t-kurimuraです。DataEngineeringチーム所属ですが、最近はすっかりサーバーチームの業務に邁進しております。
さて、アドベントカレンダーの時期を前に、LUUPの サーバーチームが「どんな感じ」で「なにをしているのか」 をバックログの例を含め説明した上で、今後の課題や展望などをお伝えしたいと思います。
開発範囲
サーバーサイドチームの主な開発範囲は主に2つです。
- Cloud Functions を利用したバックエンド開発
- Web管理画面のためのフロントエンド開発
2は文字通りフロントエンド開発ですが、サーバーサイドと一気通貫して開発する場面も多く、便宜的にサーバーサイドチームで開発しています。Cloud Functionsのバックエンド開発も管理画面のWebフロント開発もTypeScriptを利用しているため、横断開発や状況・知見の共有をしやすいことも理由の1つです。
メンバー
10名弱のエンジニアが在籍しています。ただ、フルタイムで稼働されている方は多くなく、総合した開発キャパシティでは3、4名程度のチームです。
バックエンド・Webフロントをそれぞれ専門的に開発することが多いですが、人によっては横断して開発する場面もあります。
開発体制
リモートで週に1回の1時間のミーティングをしています。一般に日次でおこなう" 朝会 "や" Daily Scram "を週次で行っているイメージです。
そのミーティングでタスクの状況を共有し、困り事や依頼事項を相談しています。新規タスクがあればその概要をチームリーダーの自分から説明します。タスクの大小や粒度は担当される方の周辺機能の仕様理解や経験に合わせています。
タスク管理はGithub Issueです。詳細な仕様にはNotionやFigmaも多く利用されています。週次のミーティングで技術的な課題や設計方針の議論も話し、ToDoレベルまで落とし込めたものは機能バックログと同様にIssue化されます。
CIは、ローカルでPrettierのフォーマッター、Github ActionsでESLintとテストが実行されます。CDもGitHub Actionsで実装され各人が開発環境やPreview環境にデプロイできます。ローカルや開発環境で動作確認を行い、影響範囲次第で専任のQAチームが詳細に検証をします。コードレビューののち週数回のリリースをします。
具体的な開発内容
ユーザー向けアプリのバックエンド開発
LUUPのエンドユーザーの皆様に向け提供するiOS・Androidアプリが最も開発が盛んなため、その周辺機能が多い傾向にあります。アプリ向けの「ライド開始」や「ライド終了」を始めとした重要機能はモバイルアプリに対しAPIで提供しているため、それに関連した機能追加、変更や定期実行処理などがあります。
SendGridやFirebaseのMessaging、FirestoreのDocument作成・変更時のトリガーを利用しているため外部APIを利用した実装も少なくありません。
例)
- 決済処理に、特定条件でxxxのクーポンパターンを適用
- xxx(故障状態)な車両があるポートを表示するために定期的に計算
- ライド終了時のxxxの処理を非同期で実行し高速化
Web管理画面の機能開発
社内向けツールのWeb管理画面ですが、そのユーザーは多岐にわたります。
- ユーザーの皆様からの問い合わせ対応などを行うカスタマーサポートチーム
- 街を巡回して故障車両の回収やバッテリー交換をするオペレーションチーム
- オーナー様からお借りした場所を、車両のポート(貸し借りを行える場所)として設定する営業チーム
- 車両の新規登録や故障状況を把握するハードウェアチーム
- キャンペーン時にクーポンの登録や設定をするマーケティングチーム
そのため汎用的なコンポーネントを開発した上で、類似した機能や横展開できるような構成が求められます。その中でポートやエリアの設定など「地図」の機能に関連したコンポーネント充実も図っています。
例)
- ユーザーが利用しているアプリのOS環境をユーザーページに表示
- 巡回チームの現在地に応じてxxxを地図上に表示
- xxxを管理画面で設定可能にしポート管理を効率化
オペレーション向け機能開発
上述したようにLUUPのアプリケーションのユーザーには社内のチームも含まれます。
巡回チームやハードウェアチームは、車両の点検などを行う場合に鍵の施錠・解錠を行います。この操作は車両に搭載されているIoTモジュールに制御されるため、Cloud Functionsのアプリケーションを経由してコマンドを送信する実装などをすることがあります。これら機能やコマンド送信部分は専門性の高いIoTチームが担当したり協業することも多いです。
カスタマーサポート向けでは、ZendeskのAPIを利用した連携などを充実させています。LUUPは「移動」というサービスの性質上、ユーザーが時間的にシビアな利用をしていることも多いため、頂戴した問い合わせを瞬時に対応することを目指しています。カスタマーサポートチーム内の体制ももちろんですが、電話受信時に素早くお客様の状況を把握するためにZendeskのトリガー機能やRestAPI機能を利用した機能で情報連携・確認などを強化しています。
また、より多くのお客様にLuupのサービスをご利用いただくためにはいざ利用しようとしたとき利用可能な車両が必要です。車両の最適配置の計算はData Scienceチームで継続的に取り組まれていますが、その情報を巡回を担うチームに連携するための機能はサーバーチームの実装で実現しています。
例)
- 施錠コマンドの送信処理部をxxxで冗長化し安定を図る
- Zendeskでのチケット作成時にxxxの情報を取得しコンソール上に表示する
- xxxの状態に応じてポート表示を変更することで特定の車両がライドされやすくする
代表的な技術スタック
Firestore
FirestoreはGoogleが提供するNoSQLクラウドデータベースです。LuupではFirebaseのサービスを多く利用していますが、その1つはFirestoreです。
さまざまな特徴はありますが、Web SDKやiOS・Android向けのSDKが提供されており、クライアントから直接参照・更新ができる「サーバーレス開発」を可能にしています。LUUPも特に2020年のサービス開始当初はこれにより少人数で高速な機能開発が可能となり事業を支えました。
ユーザーが増えてくるにつれてRate Limitでエラーが発生する懸念などが元々ありましたが、2022/10のアップデートで書き込みや同時接続数の制限はなくなったこともあり可用性は高いと感じています。
The following database limits no longer apply:
- Maximum writes per second per database: 10,000
- Maximum concurrent connections for mobile/web clients per database: 1,000,000
Cloud Functions
LUUPのサーバーサイドチームの開発で、EC2やGCEのようなVM(仮想マシン)の利用は限定的で、ユーザー向けアプリや管理画面に関連する機能のバックエンドの多くはCloud Functionsを利用しています。
一般のREST APIのような外部HTTPのトリガーの他に、Firestoreの更新トリガーやGoogle Analytics、Cloud Storage、Pub/Subのイベントトリガーなど外部イベントでのトリガーを幅広く利用しています。
インテグレーションテストは専用のFirebase Test SDK for Cloud Functionsを利用し、Firestoreのエミュレーターを起動する環境が整備されています。
やはり、インフラの管理の考慮が小さく済む点で助かっています。以前はCold Startに起因し実行時間が問題でしたが、昨年登場したMinimum instanceの設定で解消し、Cloud Functions自体に起因した遅延はメトリクス上確認されていません。
今後はより細かな設定が可能な第2世代のCloud Functionsへの移行も検討中です。
TypeScript
冒頭でも触れましたが、バックエンド・WebフロントともにTypeScriptで実装されています。
バックエンドは、ControllerがFirebase SDK for Cloud Functionsに依存する形で実装されています。Tastabilityを担保するためにレイヤーを分離し、ユニットテストはJestやSinon.JSを利用して依存をMockすることで検証をしています。また、FirestoreのParse処理をzodで行いNoSQLのFirestoreのデータをより安全に扱えるよう進めています。
Webフロントは、NuxtJSで実装されています。最近では、多用するUIをcomponentを積極的に共通化したり、validatorjsで値の整合性担保の実装の改善がされています。
主な課題・今後の展望
Firestoreのデータをより型安全にしていく
現状、Firestoreのデータの更新操作は以下5つのアプリケーションから行われています。それぞの独自のコードで書かれているため意図せず別の型を入れてしまえたり、null/non-nullの扱いも統一できない課題がありました。
- [バックエンド]
- Cloud Functions
- [フロントエンド]
- ユーザー向け
- iOSアプリ
- Androidアプリs
- 社内向け
- 巡回チーム向け管理アプリ
- Web管理画面
- ユーザー向け
Firestoreの場合、Cloud Firestore セキュリティー ルールでデータの整合性を担保できます。ただ、これはバックエンド向けのFirebase Admin SDKは対象にならず、RDBのような厳格かつ統一的なルールにはなりません。その名の通り "セキュリティー" を保証する側面のほうが強いと考えています。
そのため、データの一貫性を担保していくべく、FirestoreデータのI/OをなるべくCloud Functionsに集約していく予定です。モバイルアプリ向けには特に更新系アクセスはCloud FunctionsでAPIの提供を進め、Web管理画面とバックエンドではコード資産の共有などでの解消を検討しています。
外部APIとの連携
現状潤沢な開発リソースがあるわけではなく、今の事業フェーズではさまざまな機能を内製してメンテナンスしていくことはまだ難しいと考えています。一方で、上述のように社内に多様なチーム・業務があるためより効率的なオペレーションのため社内ツールに必要な機能は少なくありません。
そのため適材適所、機能提供を他のサービスに依存させています。例えば、オペレーションに関連する一部機能はSlackBotでの操作を可能にしています。UIの実装を省き、権限もチャネルで管理できます。小さく作って一定フィットさせたうえで、どうしても足りない要件が出てくれば大きく内製しています。
今後、ポート獲得のための営業やオーナー様とのコミュニケーションツール、Map visualization、Airflowを始めとしたデータの活用基盤との連携もさらに加速していく必要があります。また、より多くの方に認知されるためのマーケティングに関連したAPI連携も重要だと考えています。
開発体制強化
今の開発チームはフルリモートかつパートタイムの業務委託の方の割合が多い状況です。この場合、プロダクトマネージャーやチームリーダーがタスクを定義したほうが高速にまわる可能性が高いと考えています。
ただ、これはタスクを定義する人間のスキルが解決策の質の上限になり得ます。技術的・プロダクト面双方の課題発見や解決策の質は、専門的かつ経験のあるエンジニアメンバーのアイデアにより向上することが多いと思います。そのためには他チームのオペレーション理解など事業面でのキャッチアップが重要ですが、短い稼働のなかでは短期的な時間効率の問題に加え心理的なハードルもあると思っています。
局所的に重要な実装をするメンバーが多く助かっていることが多い反面、より多くの時間を使ってもらうメンバーの割合を増やすことでリファクタリングや技術改善を含め、長期的なプロダクト改善への組織力を強化したいと考えています。
最後に
LUUPは電動アシスト自転車と電動キックボードのシェアリングサービスです。一見、単純な仕組みに見えるかもしれませんが、背景には多様なチームの業務があります。それらをいかにソフトウェアが支えチーム同士を連動させていくかには、他にない面白さがあると思います。
サービス立ち上げ期のサーバーサイドの技術的な負債はこの2022年に少なからず解消できてきたと感じています。一方で、ビジネス的にも開発組織的にも技術的にも改善すべき課題はまだまだ山積しています。この難しさや大変さを一緒に楽しみ改善してくれる方をお待ちしております!
Discussion