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形式の属性