10XでProduct Fridayという社内発表会を運営している話

この記事は🎄10X プロダクトアドベントカレンダー2023 の17日目の記事です。

16日目の昨日はkazk1018さんによる「Data as a Product」について考える でした。

こんにちは、10XでProduct Managerをやっておりますkeiです。本日は10Xで行っているProduct Fridayという取り組みについてご紹介します。

Product Fridayとは

もともと「開発共有会」という名の開発で共有した知識により全体のパフォーマンスがあがるものを定期的に共有できる場があり、エンジニアリング本部(SWEやQAを中心とした組織)により運営されていました。それをこの4月からプロダクト本部(PdMやDesigner, データプロダクト)と合同で運営することとなり、趣旨は引き継ぎつつ職種を超えて(主に開発に関わる)知見を共有すること、コミュニケーションを促進すること、発信カルチャーを醸成することを目的に開催しています。

  • 頻度は隔週金曜の16-17時
  • 1枠15分くらいで最大3名の発表(短くても可)、形式は自由
  • 立候補制(ローテとかではない)
  • Audienceの参加は自由、おやつ代の補助あり

Happy Friday的なカジュアルな意味合いも込めてProduct Fridayと命名しました。4/28に第1回を開催してからもうすぐ開催される12/22まで延べ15回、計45の発表がありました。

実際にあった発表例

  • UIとUXの話をしよう - デザイナー採用面接でのWSをやってみる -
    • FigJamを使ってその場で仮想サービスへのツッコミと質疑をやってみるというインタラクティブな発表で、大変盛り上がりました
  • PdM×デザイナコラボでモブ業務フロー一緒にやったらとても良かった話
    • PdM x Designerでラジオ風に業務フローを作ってみる(再現)という斬新な発表で、質問もおハガキ紹介という徹底ぶりでした
  • お届けチームのダッシュボードを作った話
    • パートナー毎のシステムメトリクスや生産性、リリースした機能がどう使われているか?開発スプリントの可視化まで、いかに「定期的に見られる」ダッシュボードを構築するかという観点で非常に参考になりました

やってて良いと感じること

知見の共有

まずは趣旨どおり、10Xに在籍する各種メンバーから知見を共有してもらえるのが良きポイントで、アンケートでも「SWE以外の方の発表を聞けるのが新しい気づきがあってよい」や「他のチームの知見がたくさん聞けて学びが多い」といった感想が集まっています。

コミュニケーションの促進

また、新入社員のメンバーには自己紹介LTをしてもらっており、As-One Mtgという全社定例での自己紹介もありつつ(5分)、より深い自己紹介をプロダクトに関わるメンバーにできることで業務上話す際にもアイスブレイクに繋がっているようです。

運営の流れと難しさ

運営は私を含む3名で行っており、週にかける時間は30分程度というイメージです。開催〜次回までの運営の流れでいうとざっくりこんな感じです。

  • 事前準備
    • 発表者への準備リマインド
  • 当日
    • BGMを流す(Youtube)
    • 録画をする
    • Product FridayのSlackチャンネルでワイワイを促す
    • 積極的に質問をする
    • おわりにアンケートを案内する
  • 次回に向けて
    • 開催翌週の火曜に30分の運営定例を実施
    • KPTしたり次回までのToDoを確認する

難しさとして感じるのは大きく2点で、これからも課題だなと感じています。

1. コンテンツの安定提供

コンテンツを安定的に提供するためにはコンスタントに発表者が集うことが必要なわけですが、立候補にも波がありコンテンツが少なくなってきたときは運営が声掛けを頑張っています。

普段の業務の中でも「これProduct Fridayで発表してみよう」「これはProduct Fridayで聞いてみたい!」みたいな会話が運営を介さずとも自然に発生するような状態が理想かなと思っています。

2. 発表しやすい空気づくり

回を重ねていくと発表のリピーターも出てくるため、発表者のUXは大変重要です。当日の運営としてもチャンネルを盛り上げたり、なるべく質問をしたりするものの、リアクションが見えにくかったという声もあったり課題に感じています。

運営だけでなく空気は参加者みんなで作るものなので、なるべくビデオOnにしたりリアクションを送ったり(10XではGoogle Meetを使っています)、気軽に発表できる空気をつくっていきたいです。

おわりに

最後までお読みいただきありがとうございました!プロダクト組織に所属されている方の参考になれば幸いです。

明日18日目はむらなかさんによる「データ分析はプロダクトの機能の一部である」の予定です!お楽しみに!

SREとして入社し1年たつので振り返り

こんにちは。SREの栗原です。 この記事は10Xアドベントカレンダーの15日目の記事です。

私がSREとして2022年の10月に入社し1年が経ちました。 この1年間でやってきたことについて書いていきます。 現在SREチームは採用募集中です。この記事を見てスキルがマッチしていたり興味が湧いた方は是非カジュアル面談をしましょう!

SRE(Site Reliability Engineer) / 株式会社10X

  • 入社初期の取り組み
  • 自動化と効率化への取り組み
    • Terraform moduleへのresource追加
    • Redashやめる
    • Kubernetes yamlのコピペ運用をやめる
    • サービスアカウントキーの発行方法の見直し
  • 育休の取得
  • その後の取り組み
    • Deny policiesの導入
    • Terraform Planの権限修正
  • これから
続きを読む

GitHubの監査ログを定期的にexportして保存する

こんにちは。セキュリティチームでソフトウェアエンジニアをしてる@sota1235です。

この記事は10Xアドベントカレンダーの13日目の記事です。

www.notion.so

昨日の記事はSuzuki Ryotaさんのお届けチームでオーナーシップを持っていくぞでした!

今回はGitHubの監査ログを定期的にexportし、保存する仕組みを作った話をします。

  • 監査ログとは
  • GitHubの監査ログ
    • GitHubの監査ログは永久には保存されない
    • 監査ログの出力方法
    • この記事の本題
  • 監査ログ出力の仕組み
    • ざっくり要件
    • 技術選定
      • ログの保存場所
      • ログの取得処理
      • ログの取得・保存処理はGitHub Actionsで行う
    • 全体像
      • 1. BigQueryに最新データを取得しに行く
      • 2. 監査ログを取得する
      • 3. 監査ログを保存する
    • 権限管理
      • 監査ログに含まれるデータについて考える
      • 具体的にどこに制限をつけるか
    • 今後の活用方法
  • 最後に
  • 明日は
続きを読む

Relay Proxyを活用してLaunchDarklyを導入する

Relay Proxyを活用してLaunchDarklyを導入する

はじめに

この記事は 10X アドベントカレンダー2023 11日目の記事です。

前日の記事は id:hisaichi5518 さんによる「“10xを創る”開発チーム文化とはなにか 〜お届けチーム編〜」でした。


こんにちは、今年の6月に10Xに入社して今はお届けチームでソフトウェアエンジニアをしているgenkey6です。

この記事では、お届けチームで直近取り組んでいるfeature flag管理サービスのLaunchDarkly導入に関する話をお届けします。

10XではServer Sideの開発言語としてDartを採用していますが、feature flag管理サービスを導入するにあたってServer Side DartのSDKやライブラリを公式で提供しているサービスがなく苦戦しました。

最終的に採用したLaunchDarklyでは、SDKこそ存在しないもののRelay Proxyの活用という選択肢をとることで公式がサポートする方法でServer Sideからサービスを利用することができました。

LaunchDarklyがSDKでサポートしている開発言語は非常に充実しているためこうした対応が必要になるケースは稀だと思いますが、まだ見ぬ未来のLaunchDarklyユーザーに向けて、導入にあたって検討した内容やハマったポイント等をまとめておきます。

ニッチな内容ですが最後までぜひお付き合いください!

導入の背景

大変ありがたいことに、10Xが提供しているStailerのプラットフォームを活用してネットスーパー/ネットドラッグストア事業を運営されるパートナーの数は日々増えており、併せてお届けチームで開発している小売事業者向けのスタッフアプリや管理画面を利用するユーザーも増加しています。

これらのアプリケーションの特徴として、ユーザーである現場スタッフの方々の業務時間中はほぼ常時アプリケーションを使用している状態になるため、高い信頼性が求められるという点があります。

また、開発している機能に占めるCUJ (Critical User Journey) の割合が大きいため、特定の機能に障害が発生すると業務のブロッカーとなる上、最悪の場合はエンドユーザーであるネットスーパー/ネットドラッグストアのお客様のサービス利用体験にも影響が出てしまいます。ゆえに、毎回のリリースに対して慎重な姿勢を取らざるを得ないという状態にありました。

一方で、現場のオペレーション効率化にアプリケーションが寄与できる余地はまだまだ残されているため、なるべく細かくリリースを行なってインクリメンタルに改善を行なっていきたいというニーズも存在します。

こうした背景から、信頼性の担保と開発速度の向上を両立する手段として、feature flagを活用してリリースの影響範囲を抑えつつ障害発生時の復旧時間を短縮することを目指した検討が始まりました。

LaunchDarklyについて

前提として、feature flagの仕組み自体は単純なものなので、これまでの開発でも各所で利用していました。

具体的にはアプリケーションの実装内にflagをハードコーディングする方法や、環境変数 (もしくはそれに準ずる設定ファイル) を用いて機能のon/offを切り替える方法が現在進行形で利用されています。

こうした素朴な実装でもやりたいことは部分的に実現できますが、

  • 影響範囲を抑えるために一部のユーザーに限定したリリースを行うための仕組みが欲しい (単純なユーザーidによる指定に加えて、ユーザーが所属するパートナーや店舗といった単位も想定)
  • 障害発生時の復旧時間を可能な限り圧縮するために、flagの値を更新する際はデプロイを不要としたい
  • これらの仕組みを自前で実装・保守するコストはできるだけ払いたくない

といった理由により、はじめからSaaSを導入できないか検討しました。

ここでは世の中に数多く存在するfeature flag管理サービスの詳細な比較 *1 は行いませんが、LaunchDarklyを採用した理由については簡単に触れておきます。

調査を進めていくと、冒頭で述べたようにServer Side Dartで利用できるSDKを公式にサポートしているサービスが存在しないという壁にぶつかりました。

サービスによってはSDK自体は必ずしも必要ではなく、用意されているエンドポイントにリクエストを送るだけで済むようなものも存在しましたが、flagの値をなるべくリアルタイムに更新したいことを考えると何かしらstreamingな形でサービスとやり取りを行う仕組みは欲しいところです。

そんな中で、LaunchDarklyではRelay Proxyというコンポーネントが間に立ってサービス本体とのstreaming通信を肩代わりしつつスケーラブルなアーキテクチャを用意してくれる、かつこのRelay Proxyは公式がサポートするOSSとして提供されていることから、最有力な選択肢として残りました。

加えて、同時に導入を進める予定のモバイルアプリで利用できるFlutter向けのClient SDKが存在する点や、既に開発の中で利用していた各種ツールとのインテグレーションが充実している点などから活用イメージがつきやすかったことが決め手となり、採用に踏み切りました。

Relay Proxyについて

Relay Proxyとは

LaunchDarklyは公式ドキュメントが非常に充実していますが、Relay Proxyについては例えば以下のページにまとまっています。

docs.launchdarkly.com

また、以下ではSDKが存在しない言語でLaunchDarklyを利用する際に選択できるオプションの1つとしてRelay Proxyが紹介されています。

docs.launchdarkly.com

これらのページに書かれている内容を総合すると、

  • Relay ProxyはGoで実装されたマイクロサービスで、LaunchDarklyのstreaming APIと通信を行ってflagの値を保持する
  • ユーザーはLaunchDarklyと直接通信する代わりにRelay Proxyにリクエストを送るようにすることで、LaunchDarkly側に大量のリクエストが飛んでしまうのを防げる
  • SDKのない言語でLaunchDarklyを利用する方法は他にも存在し、例えば既存のSDKのラッパーを実装する方法や自前でSDKを実装する方法 *2 が紹介されている

といったことが分かります。

Relay Proxyは元々は負荷分散を想定した仕組みですが、言語非依存かつ独立してスケール可能なコンポーネントを実装コストを抑えつつ用意できるため、今回のユースケースにぴったりと当てはまるというわけです。

Relay Proxyの設定を決める

さて、Relay Proxyが目的に適していることが分かったので、続いてはRelay Proxyの設定について考えていきます。

この項の記述は主に公式ドキュメントの以下のページで触れられている内容に基づいているので、実際に導入を検討される場合は一度目を通すことをおすすめします。 (なお、Relay Proxyのバージョンは執筆時点で社内で利用されているv8.2.0を前提とします)

docs.launchdarkly.com docs.launchdarkly.com

最初に決めるべきはRelay Proxyのmodeです。Relay Proxyには proxy modeとdaemon modeという2つのmodeが存在します。両者の大きな違いは、LaunchDarklyと通信して手に入れたflagの値をどこに保存するかです。

proxy modeではflagの値をインメモリなキャッシュとして保持するのに対し、daemon modeではpersistent storeと呼ばれるデータベースに保存します。 (persistent storeの実装にはRedisやDynamoDBが用いられます)

ドキュメントの記述によると、We generally recommend configuring your SDKs for proxy mode. Daemon mode is a workaround for environments where normal operation is not possible. とのことなので、今回はproxy modeを採用することにしました。*3

他に試せていない内容として、proxy modeとpersistent storeを組み合わせて使うケースも存在します。具体的には、LaunchDarklyのBig Segmentsという機能を利用する場合はpersistent storeを利用することが推奨されていますが、今回のユースケースには当てはまらなかったため検討対象から外しました。

もう1つのポイントは、Relay Proxyのスケール数です。(StailerのインフラではKubernetesを採用しているため、ここでのスケール数とはRelay Proxyをデプロイする際のPodのreplica数を指します)

公式ドキュメントのScaling guidelinesには、We recommend you provision the Relay Proxy the same way you would an HTTPS proxy, and plan for at least twice the number of concurrent connections you expect to see. と記載されていたため、Relay ProxyにアクセスするServerコンポーネントに対するHTTP Proxyで設定しているreplica数に揃える形で設定することにしました。

また、オートスケールの設定については最初はリクエスト数が少ない箇所から運用を開始することを想定して、一旦は見送っています。

今回の取り組みと並行して、DatadogのメトリクスによるHPA (Horizontal Pod Autoscaler) の設定を可能にする仕組みをSREチームに用意してもらったので、今後のリクエスト数の実績値を元に導入を検討していく予定です。

Relay Proxyをデプロイする

設定の方針が決まったので、続いてはRelay Proxyを実際にデプロイするステップです。

以下のページで複数のデプロイ方式が紹介されていますが、社内で既にHelmを活用していたためそちらを使うことにしました。

docs.launchdarkly.com

公式のHelm chartが以下のrepositoryで管理されているため、repository内のドキュメントを参考にしつつvalues.yamlの中身を埋めていきます。

github.com

各種configurationの項目の意味合いについては以下にまとまっているため適宜参照しつつ、

以下のファイルで記述されているデフォルト値を更新する形でvalues.yamlを記述すればOKです。

10Xではこうして用意した設定に基づいて、Helmfileなどの仕組みを活用しつつKubernetesのマニフェストを生成してデプロイしています。

Relay Proxyを監視する

Relay Proxyがダウンしてしまうとアプリケーション側でLaunchDarklyのfeature flagの値に依存している箇所は全てデフォルト値にfallbackされてしまうため、運用開始後の監視は厚めに設定しています。

監視内容の設計にあたっては公式ドキュメントの以下のページが参考になりました。

docs.launchdarkly.com

上記を参考にしつつ、10Xでは次に挙げるような項目を監視しています。

  • Podの死活監視
    • (デプロイ時の設定で無効化していなければ) PodのlivenessProbe/readinessProbeがRelay Proxyの /status エンドポイントにリクエストを送るように設定されているため、これをhealth checkとして利用しています
  • LaunchDarklyとの接続が正常かどうかの監視
    • /status エンドポイントのresponseにはLaunchDarklyとのstreaming通信が正常に行われているかどうかを示す情報が含まれます (JSONPathで書くと $.environments.NameOfEnvironment.status の部分)
    • これをDatadogのSynthetics Testを用いて定期的に確認することで、通信が途絶えた場合にアラートを出すようにしています
  • Relay Proxyのパフォーマンス
  • Relay Proxyにリクエストを行うClientがflagの評価に失敗した際に出力するログ
    • 必ずしも全てのケースで緊急の対応が必要なわけではありませんが、flag評価に失敗してfallback値が使われたことは検知したいため、Cloud Loggingのlog-based alertsを用いた通知を行っています

Relay Proxyにリクエストする

ここまででRelay Proxyを利用する準備が整ったため、いよいよServer SideからRelay ProxyにリクエストできるようにDartでClientを実装していきます。

大まかな方針としてはflag評価用のエンドポイントに対して、Contexts *4 の情報を付与してリクエストするだけのシンプルなものになります。

以下で、実装時にハマった点をいくつか取り上げておきます。

  • undocumentedな仕様が多い
    • requestで渡すべきContextの型、特に複数のContextを組み合わせるMulti-contextsの場合の情報が記載されておらず、最終的にRelay Proxy本体とテストの実装を読んで理解しました
    • また、responseの型についても明示されておらず、手元でAPIを呼び出してresponseの内容を直接確認する必要がありました
  • 同じくDartで実装されているFlutter向けのSDKと型を共有できそうで出来ない
    • Contextの型 (上記で確認したもの) が実は違います
    • packageをそのまま追加するとFlutterまで依存に入ってきてしまうため、実際に参照する型定義だけをコピペする必要がありました
    • 最初はClient実装に関わる型定義部分はほとんど流用できるか?と考えてチャレンジしてみましたが、結果的に正しく使い回せたのは LDValue というflagの値に関する型定義のみでした

これより詳細な実装の中身に関してはここでは立ち入りませんが、以下のrepositoryにて社内で利用しているものとほぼ同じ実装を行っているので、気になった方はご参照ください。

github.com

今後に向けて

以上、Relay Proxyを活用してSDKのない言語でLaunchDarklyを導入するまでの道のりについて書いてきました。

導入してからまだ日は浅いですが、チーム内では既に以下のようなユースケースでLaunchDarklyを活用し始めています。

  • 細かな調整が求められるUX改善タスクにおけるExperimental Flag
  • 比較的長期にわたって行われるリアーキテクチャプロジェクトにおけるMigration Flag

また、将来的には次のようなユースケースにも活用の幅を広げていきたいと考えています。

  • 特定の機能を限られたユーザーに提供する際のPermission Flag
  • 外部サービスに依存している箇所のKill Switch
  • Firebase Remote Configを利用してA/Bテストを実施しているケースの代替

加えて、今回の記事では触れられませんでしたが、LaunchDarklyの運用面の整備として以下のような取り組みも並行して進めています。

  • flagの負債化を防ぐ仕組み
  • ソフトウェアエンジニア以外がflagの値を安全に変更できるような仕組み
  • 複数チームで利用する際のガバナンスの仕組み (flagの命名規則やflagに付与するTagsのルールなど)

活用が進んでいった暁にはこれらのテーマでもまた記事を書いてみたいです。

終わりに

この記事では信頼性の担保と開発速度の向上を両立する手段としてのfeature flagについて紹介しましたが、直近では同様のゴールに対して次のような施策も検証されています。

  • リリースの安全性を高めるProgressive Deliveryの仕組み
  • モバイルアプリにおけるhotfixリリースを容易にするためのOTAの仕組み

10Xでは、こうした取り組みを一緒に進めてくれるソフトウェアエンジニア、SREの仲間を募集しています!

open.talentio.com open.talentio.com


最後に、筆者が所属しているお届けチームの取り組みについて、他のメンバーが書いた以下の記事も併せてご覧ください。

続く明日の記事もお届けチーム所属のsuzukiさんによる「お届けチームでオーナーシップを持っていくぞ」です。お楽しみに!

*1:先日Findyさんが主催で開催されたイベントの次の登壇資料に網羅的にまとまっているため、サービス選定に悩まれている方は参考にされると良いと思います: https://speakerdeck.com/gunta/uxno-ding-dian-devcycle-ni-chan-rizhao-kumadenodao-nori

*2:ただし、いずれも公式としてはサポート外ですよという注意書きがされています

*3:じゃあいつdaemon modeを利用すべきなの?かというと、同じドキュメントで Daemon mode requires that you configure server-side LaunchDarkly SDKs to communicate directly with they Relay Proxy's persistent data store. We recommend this configuration when you're using LaunchDarkly with PHP or in a serverless environment. と書かれています

*4:「ユーザー」を拡張した概念で、リクエストを行った主体が、事前に設定した条件に合致するかどうかを判断するためのkey-value形式の属性

“10xを創る”開発チームの文化とはなにか 〜お届けチーム編〜

この記事は 10X アドベントカレンダー2023 の10日目(12/10)の記事です。9日目(12/9)の昨日は、 id:takanamito さんによる「grpc-dartのInterceptorを使う」でした。

10Xのお届けチーム エンジニアリングマネージャー(以下, EM)の id:hisaichi5518です。10Xではソフトウェアエンジニアとしてサーバサイド、Android、iOS、Flutterと色々やってきて、今は「人を動かして、ことを成す」を目指してEMとして活動しています。

今回は、自分が担当しているお届けチームについて「どんな雰囲気なのか」「どういうチームを目指しているのか」「具体的な活動」について書いていこうかなと思います。書き終わって気付いたんですが、今年のテーマは"開発・プロダクト"らしく、ちょっとテーマに沿ってない感があるけどまあいいかってなりました。人生ってそんなもんですよね

お届けチームって?

10Xは、”10xを創る”というミッションを持っていますが、お届けチームは、Stailer*1において"パートナーが、かんたんに届けられる仕組みを提供する"をミッションに持つチームです。このミッションを達成することが10xを創ることとも言えると思います。

イメージしやすいようにどういう機能の開発をしているのか具体の話をすると、店舗スタッフが商品を取ったことを記録するといった機能や配達スタッフが配達時にどの順番で届けるかといった機能などを提供するアプリの開発をしているチームです。

また、お届けチームは執筆時点で、EM1名、SWE4名、QA3名、Designer1名、PdM1名の計10名で構成されています。

お届けチームの雰囲気「楽しく働く」

カジュアル面談をしていると10Xはあんまり雑談がない雰囲気と思われていたりしますが、お届けチームのSlackチャンネルでは、こんな改善した!やこうしたほうがいいかも?や気になった技術ブログの話みたいな仕事に関連する話はもちろん、アニメの話や今日のお昼ごはんの話といった仕事に関係ない雑談もよくしています。他にもSlackのHuddleで作業している人同士で繋げたり雑談をすることもあります。

CIが爆速に!!

スクラムイベントの改善の提案

やいていき!

これは、チームメンバーの根幹に"楽しく働こう!"があると思っていて、それがチームの雰囲気にも出ていると自分は思っています。

お届けチームはどういうチームを目指すのか

もちろん楽しく働くのは大事ですが、それだけではお届けチームの"パートナーが、かんたんに届けられる仕組みを提供する"というミッションは達成出来ません。23年4月からお届けチームは組成*2されたのですが、その時からお届けチームはどういうチームを目指すのかを定義しています。

10Xでは、10X Valuesという価値観を定義していて、それを元に作成されています。

1. 中長期的な観点を持った活動が継続的に行われている

なぜ目指すのか

ある機能を期日に間に合わせるように作るというのは短期的には非常に重要ですが、期日に合わせようと無理をした実装になりバグの温床になったり、作った本人しかわからない機能になったり、後から色々とツラミが出てきます。そうならないためには、中長期的な観点を持って実装面や機能面はどうあるべきかを考え、それを言語化し、一歩ずつ実現する必要があると考えています。

2. チームが自分たちで判断し実行している

なぜ目指すのか

お届けチームは、経営や他事業部から言われたことをただやるチームではなく自分たちで判断して実行するチームでありたいと思っています。そのほうがメンバーがやることに対して自分ごと化出来て成果が出せると考えています。

3. チームで成果を出し、チームで評価される

なぜ目指すのか

“10xを創る”という大きな成果を出すには、1人1人が頑張って成果を出してその総和で勝負するのではなく、チームで成果を出すことで個々の総和をチームで超える必要があります。そのためには、メンバー同士が助けを求め、助け合うことで大きな成果が出せると考えています。

具体的なお届けチームの活動の例

お届けチームがどういうチームを目指すのかはわかったと思うので、それを踏まえて具体的にどういう活動をしているのかの例をこのセクションでは記載します。これは、EMの自分だけがやっているのではなく、メンバーが行っていることです。

理想の姿とギャップをドキュメントにして、チームで取り組む

中長期的な観点を持った活動は、まず理想の姿を定義して現状とのギャップを埋めるために着実に進めることが重要です。そして、一歩ずつ進めるためには1人で実施するのではなく周りを巻き込み、チームとして取り組む必要があります。そのためにはドキュメントを作成して、他の人も理解できるようにする必要があります。

最近作成されたドキュメントは以下のようなものがあります。(一部伏せ字にしています)

  • feature flagの削除を忘れないようにするための運用
  • お届けチームがオーナーシップを持つコンポーネントの監視・オブザーバビリティのあるべきを考える
  • 滞留しているプルリクエストを消化するためにやること
  • 満足のいく引き継ぎってなに?
  • ******* Syncerの現在地と目指しているところと未来
  • 総量ピックへのpick-packモジュール導入

現場リサーチ

お届けチームでは、ネットスーパーを運営する実店舗にお伺いしオペレーションを観察することを現場リサーチと呼んでいます。この現場リサーチを行うことで、”パートナーが、かんたんに届けられる仕組みを提供する"を実現するための課題やあるべき姿を考え、チームで何をやるべきかを判断し実行できるようにしています。

現場リサーチについては、お届けチームのPdMであるkoichi-mさんが12/9に公開した より良いオペレーションのためのプロダクトとは?お届けチームが探索する「現場リサーチ」〜プロローグ〜|こういち / koichi-m にも記載されているので、読んでみてください。

目標共有会

10Xでは個人の目標設定を半期ごとに行っています。目標自体は一覧として共有されるのですが、それだけだと目標設定時に何を考えたのか、どういう思いがあるのかなどがわかりません。

チームで成果を出すには、各個人がどういうことを考え目標を設定したのかや助けてほしいことなどをチームで共有し、職種関係なく助け合える状態になることが必要があると考え、お届けチームでは目標共有会を行っています。

目標共有会では、以下のようなことをチームで話しました。

  • この半期で達成したい目標
  • 実は持っている裏テーマ
  • メンバーに知っておいてほしいこと
  • 職種限らず、達成するために助けてほしいこと
  • メンバーからの質問/感想タイム

まとめ

お届けチームの雰囲気や目指したい形、具体的に何をやっているのかを紹介しました。

ここでは書ききれなかった「ミッションの”かんたん”って何?どういう挑戦がある?」「お届けチームは具体どういう開発をしているの?」「これらを実現するためにEMはどう振る舞っているの?」などもっと詳しく知りたい人は、XのDM/リプライまたはカジュアル面談フォームからどうぞ。

明日は、お届けチームメンバーの id:genkey66 さんによる「Relay Proxyを活用してLaunchDarklyを導入する」です。楽しみですね!

関連

*1:ネットスーパー・ネットドラッグストアの立ち上げと成長を支援するサービスです

*2:ちなみに組成された経緯は、ドメインベースの開発体制への移行 - 10X Product Blogで読めます。

Kubernetes のインスタンスコストを 0.6x した話

10X の Kubernetes おじさん兼娘ちゃん好き好きおじさんこと SRE の @tapih です。

この記事は 10X プロダクトアドベントカレンダー2023 の 8 日目の記事です。昨日は PdM の @enaminnn さんの記事でした。

note.com

本記事では、 2023 年 1 月頃に行っていたインフラコスト削減の施策についての話をご紹介します。

施策を行った背景

弊社が提供している Stailer では、サービスローンチ当初から Kubernetes を採用しています。

自分が入社した 2021 年 5 月時点では数パートナー企業にのみサービス展開をしておりました。ありがたいことにそこから多くの企業様にご導入いただき、短期間で十数パートナーまでサービスを展開することができました (導入の状況は Culture Deck をチェック!)。

一方で、ありがたくないことにインフラコストは増える一方です。サービスが拡大しているのでコストが増えるのは当然ではあるのですが、絶対値で見たときに「ここまでかからないだろう…」という水準に差し掛かっている感覚がチーム内に漂っていました。

では、問題を解消しようとすると、アプリからインフラまで様々なレイヤに手を入れる必要があり、結構大変な印象でした。しかし、一連の施策によってコストカット以外の様々なメリットが得られる期待もあります。このような施策を “10x 案件” と弊社内で呼んだりするのですが、育休復帰後の私は「10x するぞ!(コストは下げるけどね)」と意を決して施策をスタートすることとしました。

Goals / Non Goals

Goal はまず当たり前ですが、 Kubernetes のインスタンスコストを削減することです。その How として Node pool の作り直しが Must であったため、その部分の設計を行うことも当初のゴールと設定しました。また、低リスクで拾える他の改善ポイントがあればついでに拾うこととしました。

一方で、 Cloud Run / GKE Autopilot への移行といった Kubernetes クラスタに閉じない施策は工数や影響範囲が大きく、問題が顕在化した状態で不確実性の高い施策をするのは避けたい、という観点から一旦 Non Goal としました。また、 Kubernetes 以外のコストで大きいものの削減に関しては、他チームに移譲していたり、弊社でコントロールしきれない部分があったため、これも Non Goal としています。

Node pool の命名規則

Node pool を作り直そうとしたところ、 Pod の Node Selector で cloud.google.com/gke-nodepool ラベルを参照していました。このラベルは GKE 側で自動で設定されるものであり、仮に新しい Node pool を作ると別の値が自動で設定されます。そして、この新しい Node pool に Pod をスケジュールさせるには Node Selector の設定を変える必要があります。Node Selector の変更はエディタの一括置換で行えないこともないですが、 Kubernetes マニフェストは現状様々なレポジトリに散在しており、作業は割りと面倒です。 Node pool を作り直すオペレーション自体は今後も発生しうるため、 SRE / SWE の責務を分離できるような状態にしておくのがよさそうでした。

クラスタ上で動いているすべてのワークロードを整理した結果、以下の 2 つの Label / Taint を設定することとしました。

  • 10x.co.jp/owned-by:
    • system (For GCP)
    • platform (For SRE)
    • services (For SWE)
  • 10x.co.jp/instance-type:
    • highmem
    • highcpu
    • standard

また、このタイミングで Spot VM の稼働を開始し、非同期ワーカーの一部の Pod を Spot VM 上で動かすこととしました。以上から Node pool の命名は <10x.co.jp/owned-by>-<10x.co.jp/instance-type>-<cloud.google.com/gke-spot>-YYYYMMDD のようにしました (上限は 39 文字)。なお、 Node Auto-Provisioning のような自動化の機能もありますが、自動化により別の問題が発生する不確実性を排除したかったことから採用を見送っています。 Node pool を分割しすぎると待機リソースが増えてしまうため、このルールに従いつつも必要最小限の Node pool のみを作成しました。

Pod の移行

その後、マニフェストを変更して移行を進めます。この作業は、理想的には各チームに移行を依頼したかったのですが、自分でゴリッと設定していくことにしました。

現在は、ドメインに対する理解をより深めるためにドメインベースの開発体制に移行が進んでおり、各ワークロードに対する Ownership がある程度定まっています。しかし、施策を行った当時は、パートナー企業グループ単位でのチーム分割が行われており、各チーム内で理解が十分でないワークロードがありました。その状況の中では、①それでも各チームに頼む、②チーム移行を待つ、③自分でやってしまう、といったアクションが考えられ、 SRE チームとしては別の施策のブロッカーを解消したかったため、各チームの OnCall-er に軽く共有だけしつつ、③の自分でやってしまうを選択しました (そして後思いの外大変でプチ後悔します)。

Node pool に付与したラベルを指定して移行を行います。

...
spec:
    affinity:
      nodeAffinity:
        requiredDuringSchedulingIgnoredDuringExecution:
          nodeSelectorTerms:
          - matchExpressions:
            - key: 10x.co.jp/owned-by
              operator: In
              values:
              - services
            - key: 10x.co.jp/instance-type
              operator: In
              values:
              - highcpu
    tolerations:
      - key: 10x.co.jp/owned-by
        operator: Equal
        value: services
        effect: NoSchedule
...

AOT コンパイル

以上はよりインフラに近い施策でしたが、よりアプリケーションに近い施策として AOT コンパイル化を行いました。

弊社ではサーバサイドでも Dart を採用しています。 Dart (Native) は実行モードとして JIT / AOT コンパイルモードの 2 つをサポートしており、ざっくり前者は開発用途、後者は本番用途での利用が推奨されております。

しかし、実態は JIT モードで動いている Pod が多く存在しました。 gRPC サーバのような比較的長い時間動き続ける Pod に関しては JIT コンパイラの最適化が働きやすく CPU 効率はむしろ AOT モードよりも良かったのですが、その他に以下のような問題が発生していました。

  • メモリ使用量がそもそも多い
  • メモリ使用量が不安定
  • 最適化の効きづらい CronJob の実行時間が増大
  • Pod の起動時間が遅い

AOT モードへの移行は簡単で、 dart compile exe を実行するだけです。このタイミングで Multi-stage build 対応していない Dockerfile の変更も行いました。

移行しても動くだろうと思いつつ、若干自信がなかったので、検証をはさみながら移行を進めたところ、 AOT モードに移行すると動かない CronJob があったことがハマりどころでした。コード内にファイルパスを自動で解決する処理があり、この値が AOT / JIT で違う値を返していたことが原因でした。対策としてはシンプルで、パスを外から渡すようにコードを書き換えました。

final scriptPath =
        io.Platform.script.path.replaceFirst(RegExp(r'pkg/stailer.*'), '');

リソース設定

AOT コンパイルモードに移行した後、 Node pool の移行に合わせてリソース設定によるコストの最適化を行いました。弊社の Kubernetes 上で動いている主なワークロードは以下の通りです。

  • Deployment: gRPC
  • Deployment: 非同期ワーカー
  • CronJob

お客様からのリクエストを直接受ける数種類の gRPC サーバに関しては低負荷時の CPU throttling を避けるために limit を設定していません (詳細はこちらの Issue や様々なブログで言及されてるで割愛)。その他に関しては現状 QoS が Guaranteed になるように設定しています。

特に CronJob に関しては以下の要素に応じて、設定値にどのくらい余裕をもたせるかの判断を行いました。

  • 実行頻度
  • 並列実行数
  • 失敗し続けたときのクリティカル度合い
  • 実装 (特に DB 周り)

振り返り

コスト削減を入り口にスタートしましたが、振り返ってみるとひたすら基本的な設定をし続けている期間でした。

欲を言えば、ドメイン体制での Ownership 移譲が先にできているともう少しよい進め方ができていたと感じています。その他にもデプロイの仕組みが十分整備されていなかったり、マニフェストが散在していたりと、あれが先にできてたらもう少し楽なのに…と思うことが多々ありました。そのような状況でも SRE チームのインセンティブと照らし合わせて、地道な火消しにまず振り切ったことは良かったポイントでした。また、ドメイン体制移行後は、各チームメンバーが自分のした設定を土台にリソース設定を都度ブラッシュアップしてくれており、体制移行をスムーズにすることに役立ったと実感しております。

インスタンスコスト削減

インスタンスコストは 4 割減 (0.6x) となりました。また、インスタンスコストが安定したことで確定利用割引を利用しやすくなりました。最近、他のチームメンバーが割引を有効にするタスクを推進してくれたことで、さらなるコスト削減効果が得られました。

cost
インスタンスコスト削減

CronJob 実行時間の短縮

CronJob 実行時間が短縮されました。

execution time
CronJob 実行時間の短縮

リソース使用の安定化

リソース設定以前は CPU request が設定されていなかったり、メモリ設定が不適切な Pod が多く存在しており、 Noisy Neighbor の影響と思われる現象が発生しておりました。その数をちゃんと計測しているわけではないので主観が入りますが、施策実施後はクラスタ全般がより安定したことを感じています。

今後について

マニフェストレポジトリの統一と自動化

以上により、既存の Pod のマニフェストは適切に設定がなされるようになりました。一方で、新規に追加されるマニフェストについては適切な設定がなされる保証はありません。

マニフェストが追加される頻度はそこまで高くないため、現状は SRE による CODEOWNER レビューによってこれを担保しています。今後は、マニフェストレポジトリの統一と Open Policy Agent (OPA) の導入によって自動化を進められれば、と考えています。

Re-architecture の機運

以上により、 Kubernetes 周りに顕在化していた問題が概ね解消され、運用コストが削減されました。また、その後ドメイン体制移行によって運用面の各チームへの移譲が進んだこと、様々な施策によって Toil が削減されたこと、ローンチが一段落したことも相まって、SRE チームとしてより将来に目を向けられる体制になってきたことを感じています。

現在は Re-architecture に対する機運が高まっていることを感じます。その中でいよいよ Kubernetes クラスタをリプレースしたり、はたまた Cloud Run に移行する話などが上がってくるかもしれませんが、今回の施策を行ったことでそのような議論にもじっくり腰を据えて向き合えようになりました。

Wrap up

弊社では、今後のさらなる 10x 案件に向けて採用を行っています。弊チームは SLO の策定等、スタートアップの事業フェーズながらも SRE プラクティスに向き合える環境を作ってきました。弊社に興味がある方はカジュアル面談へのご応募をお待ちしております!

明日は SWE お買い物チームの @takanamito さんが記事を公開する予定です。お楽しみに!

モノリス解体に向けたパッケージ構成

CTOのishkawaです。

この記事は10X アドベントカレンダー2023の3日目の記事です。

先日、サーバーサイドのメンバーを中心として、コードをどのように分割管理していくか話すオフサイトを実施しました。オフラインで1日中話していたこともあり、話題は色々な方向に進んだのですが、その中でもモノリス解体にトピックを絞ってシェアしたいと思います(他の話は他のメンバーが書いてくれるはず!)。

前提

10Xには4つの開発チームがあります(お買い物チーム、お会計チーム、お届けチーム、マスターデータチーム)。今年の4月にチーム分割が始まり、コードやデータのオーナーシップも各チームにアサインしてきました。

この組織移行によってオーナーシップ分割は進んでいるものの、依然としてモノリスのパッケージ(Dartのパッケージ)は大きいままで、ほとんどのコードがそこにある状態でした。領域ごとのディレクトリ分割は進んでいるものの、パッケージまで分かれる筋道が見えていませんでした。

課題

正直、ディレクトリ分割/オーナーシップ分割が進んでいけば、モノリスパッケージでもまだやっていけなくはないかなあという状況ではありました。しかし、以下のような課題があるのは事実なので、先手を打ってモノリスを解体する段取りを進めることにしました。

  • 業務領域を跨いた処理に対する制限が弱く、意図しない結合が生まれやすい。
  • 開発中のエディタによる解析、テスト実行、コード生成に掛かる時間が伸びている。
  • 名前空間が大きいため、クラスや関数に素朴な名前が使いづらい。
  • 依存パッケージやユーティリティが肥大化しやすい。

1つ目の課題が特に重要なので、ここではそれについて説明します。

業務領域を跨いだ処理に対する制限

Stailerにはネットスーパーの幅広い業務を取り扱うシステムが含まれています。注文管理、決済、店舗でのピッキング、配達といったものです。

各種業務はある程度の独立性がありますが、互いに関連しているものもあります。例えば、お客様からの注文が入っても、店舗でのピッキングすることができなければ欠品となるため、お客様の注文管理と店舗のピッキング管理の2つの業務は関連していると言えます。

この関連を素朴に実装する場合、注文管理でもピッキング管理でも注文データを更新することになりますが、これでは注文データは共有データとなり、管理が難しくなります。

一方、注文を取り扱うモジュールを定義し、データの変更をモジュール内に限定し、外からは指示を出すコマンドを定義すると、注文データの変更は注文モジュールに閉じることになります。

モノリスのパッケージではすべてが同じパッケージに含まれるため、前者の方式で簡単に実装できてしまいます。これは極端な例なので「そうはならんやろ」と思えるものかもしれませんが、いずれにせよ「どの機能がどの機能を利用して良いのか」の統制はコードベースが大きくなるほど難しくなります。ArchUnitやLintなどの方法で統制をかけることもできますが、業務領域レベルの大元の分割ではパッケージの分離の方が有効と考えました。

パッケージが分かれている場合、そもそも注文データへのアクセスのインターフェースも公開しない限りは使用できないため、自然と後者の方式を取ることになります。

我々のアプローチ

以下のようなパッケージ群を構成することに決めました。

  • stailer_app: mainのパッケージ。アプリ向けgRPCサーバーやcronジョブなど。
  • stailer_module: 業務を扱うモジュールのパッケージ。注文管理やピッキング管理など。
  • stailer_common: Stailerで汎用的に使うパッケージ。税額計算など。
  • general: Stailerに依存しない一般的なパッケージ。Firestoreクライアントなど。

元々 stailer_appstailer_module のすべてが1つのパッケージとなっていった所から、各種業務ロジックを stailer_module に切り出していくことと、エントリポイントであるstailer_app が別々のパッケージとして切り出していくことが主な決定です。

stailer_moduleは他のモジュールに依存してもOKで、公開インターフェースを通じて領域跨ぎの業務を完遂する想定です。なお、モジュールの循環依存は許容しないものとしています。

パッケージの公開インターフェース

Dartのパッケージの公開インターフェースは、以下のように決まります。

  • lib/src以下の実装ファイルはデフォルトでは公開されない。
  • lib/*.dartで実装ファイルをexportすると、当該ファイルのインターフェースが公開される。

ファイルの構造を図に起こすと、以下のようになります。

package_root
├── pubspec.yaml
└── lib
    ├── some_export.dart <-- lib/src以下のファイルを参照してexportする。
    └── src
        └── ...          <-- lib/src以下に実装ファイルを入れる。

前述の通り、ある業務を扱うモジュールの内部実装は他の業務を扱うモジュールからは不可視としようとしています。そのため、パッケージがexportするものは以下の3種類と決めました。

  • コマンド: モジュールの変更を加えるインターフェース。stailer_moduleから使用される。
  • クエリ: モジュールのデータを取得するインターフェース。stailer_moduleから使用される。
  • gRPCサービス: gRPCサービス。stailer_appから使用される。

exportするものを絞ることにより、モジュール間のコミュニケーションは意図したもののみとなり、不適切な結合を生みにくい構造となりました。

実現への筋道

先日まさに「この形を目指すぞ!」ということになったので、実現はまだこれからです(上手くいかないこともあるかも?)。実現に向けては、以下のようなステップを踏むことを想定しています。

  1. モノリス内にモジュールディレクトリをつくり、関連業務のコードをまとめる。
  2. モジュールディレクトリを跨ぐ処理の呼び出しに制限をかけ、疎にしていく。
  3. モノリス内のモジュールディレクトリへの依存がないモジュールディレクトリをパッケージ化する。

図にすると以下のようなイメージです。まずはモノリスパッケージからスタートします。

次に、モノリスパッケージ内にモジュールディレクトリを業務ごとに作って、分類していきます。

モジュールCはモノリス内へのモジュールへの依存がないため、パッケージにします。

モジュールAとモジュールBもモノリス内への依存がなくなったので、パッケージにします。

結果として、モノリスにひとまとまりになっていたものがパッケージ分割されました。

終わりに

今回のパッケージ分割ではパッケージという境界はできるものの、実体としてDartのメソッド呼び出しに過ぎません。裏を返せば、この方式は各モジュールが同一言語(Dart)で実装されていることや、モジュールがアプリケーションにバンドル可能という前提に依存しています。

将来的にはこの前提を取り払い、各モジュールの直接呼び出しからメッセージベースの連携に変えたり、はたまた各モジュールをサービス化したりするのかもしれません。いずれにせよ、今進めているような分離は必要なので、まずはこれをやり切ろうというのが我々の現在地です。

こうした取り組みは続けていくので、いつかまたこのブログで状況を報告できればと思います。


10X アドベントカレンダー2023はクリスマスの12月25日まで続きます。

明日はtakimoさんによる記事です!お楽しみに!