Upgrade to Pro — share decks privately, control downloads, hide ads and more …

OpenFeatureと自動生成を活用したフィーチャーフラグの宣言的集約管理

 OpenFeatureと自動生成を活用したフィーチャーフラグの宣言的集約管理

CloudNative Days Summer 2024 の登壇資料

https://event.cloudnativedays.jp/cnds2024/talks/2274

---
近年、トランクベース開発やAB テスト、カナリアリリースへの利用などでフィーチャーフラグを活用するケースが増えてきました。また、フィーチャーフラグAPIの標準化を目指す OpenFeature から先日ついに Web SDK v1 がリリースされるなど、徐々にその熱の高まりを感じます。しかしながら、OpenFeature を採用した開発事例はまだ少なく、活用方法などの知見が不足していると感じています。

本セッションではフィーチャーフラグの種類分けや OpenFeature の概要から、具体的な実装にいたるまでを詳しく解説します。さらに今回、自動生成を活用することによってアプリケーションコードとリモート間でのフラグ情報の集約管理を実現したため、その方法についてもお話しします。セッションを通じてフィーチャーフラグの導入イメージに加えて、できるだけ疲弊せずにこれらを運用していくためのアイデアを得ることができます。

Shota Iwami

June 13, 2024
Tweet

More Decks by Shota Iwami

Other Decks in Technology

Transcript

  1. OpenFeatureと 自 動 生 成を活 用 した フィーチャーフラグの宣 言 的集約管理

    株式会社サイバーエージェント 岩 見 彰太 GitHub:@BIwashi X: @B_Sardine CloudNative Days Summer 2 0 24
  2. 自己 紹介 岩 見 彰太 / Iwamin 株式会社サイバーエージェント ೥౓৽ଔೖࣾ "*ࣄۀຊ෦ڠۀϦςʔϧϝσΟΞ%JW

    アプリ運 用 カンパニー @BIwashi @B_Sardine ࣗಈੜ੒Λ׆༻ͨ͠ɺӡ༻อकίετΛ཈͑Δ&SSPS "MFSU3VOCPPLͷҰݩू໿؅ཧ 'FBUVSF'MBH%FFQ%JWF ΦϒβʔόϏϦςΟݚम࣮ફฤ 6OJ fi FE%J ff ܗࣜͷࠩ෼͔Β(P"45Λߏஙͯ͠ GFBUVSF fl BHΛࣗಈܭ૷͢Δ
  3. 自己 紹介 岩 見 彰太 / Iwamin 株式会社サイバーエージェント ೥౓৽ଔೖࣾ "*ࣄۀຊ෦ڠۀϦςʔϧϝσΟΞ%JW

    アプリ運 用 カンパニー @BIwashi @B_Sardine ࣗಈੜ੒Λ׆༻ͨ͠ɺӡ༻อकίετΛ཈͑Δ&SSPS "MFSU3VOCPPLͷҰݩू໿؅ཧ 'FBUVSF'MBH%FFQ%JWF ΦϒβʔόϏϦςΟݚम࣮ફฤ 6OJ fi FE%J ff ܗࣜͷࠩ෼͔Β(P"45Λߏஙͯ͠ GFBUVSF fl BHΛࣗಈܭ૷͢Δ
  4. 1 .トランクベース開発とは 2 .feature fl ag の概要 3 .feature fl

    ag の種類 4 .フラグ管理システム SaaS 5 .課題感 6 .OpenFeature 7 . 自 動 生 成と宣 言 的集約管理 8 .The Full Scope of the Dream Feature Flag System
  5. トランクベース開発とは • 直接 main に対してどんどん merge していくスタイル • branch は1~2

    日 以内に merge する短命なものでなくてはいけない • 特徴 •マージに伴うコンフリクトを最 小 限にできる •変更が加えられた main branch は常に prod に リリース可能な状態になっている •開発単位を 小 さく保ち、頻繁に main に pushし、 それに伴い CI/CD を実 行 して 自 動テストや脆弱性 診断の FBK を素早く得ることで開発スピードと デプロイまでの時間短縮を実現している トランクベース開発の trunk は Subversion というバージョン管理システム における main のことを trunk と呼んでいたらしい トランクベース開発について - 赤 帽エンジニアブログ
  6. トランクベース開発とは • 直接 main に対してどんどん merge していくスタイル • branch は1~2

    日 以内に merge する短命なものでなくてはいけない • 特徴 •マージに伴うコンフリクトを最 小 限にできる •変更が加えられた main branch は常に prod に リリース可能な状態になっている •開発単位を 小 さく保ち、頻繁に main に pushし、 それに伴い CI/CD を実 行 して 自 動テストや脆弱性 診断の FBK を素早く得ることで開発スピードと デプロイまでの時間短縮を実現している トランクベース開発の trunk は Subversion というバージョン管理システム における main のことを trunk と呼んでいたらしい トランクベース開発について - 赤 帽エンジニアブログ
  7. feature fl ag とは • コードを変更することなくシステムの振る舞いを変更可能にする仕組み • 利点と使い所や課題 •トランクベース開発においては修正を fl

    ag 付きで実装しておくことで、その機能の反映可否 をコードを変更することなく 行 えるようにできる •「 fl ag 付きで実装する = fl ag を含む if 分岐で実装を囲んでおくことによっていつでもその実装を有効化無効化できるよ うに実装をしておく」 •役割を終えたら feature fl ag を削除する、といいうのを徹底してやらないとコードが複雑化 してしまう •feature fl ag の実装は属 人 化しがちなので、早めに実装した 人 が削除しないと簡単に負債化してしまう The Go gopher was designed by Renée French. GO Feature Flag
  8. HOGE_FLAG := true if HOGE_FLAG == true { // flag

    ON ͷ࣌ʹ࣮ߦ͍ͨ͠ॲཧ // ex.) ൓ө͍ͨ͠मਖ਼ // ex.) AB ςετͷ A Λ༗ޮԽ͢Δ // ... } else { // flag OFF or flag Λઃఆ͍ͯ͠ͳ͍࣌ʹ // ࣮ߦ͍ͨ͠ॲཧ // ex.) मਖ਼લͷطଘͷ࣮૷ // ex.) AB ςετͷ B Λ༗ޮԽ͢Δ // ... } 実装イメージ • HOGE_FLAG とういう fl ag が ON だっ た場合と OFF だった場合の処理を if 文 で書く • AB テストや特定の実装を有効化する場 合などに使える The Go gopher was designed by Renée French. GO Feature Flag
  9. • feature fl ag にはいくつかカテゴリがあり、それぞれの 用 途によってライフ サイクルやON/OFF の頻度などが異なる •

    適切に理解して使 用 する必要がある • (feature fl ag と feature toggle という2つの呼び名が存在するが 同じもの) feature fl ag の種類 The Go gopher was designed by Renée French. Feature Toggles (aka Feature Flags)
  10. • トランクベース開発を可能にするために使 用 • 進 行 中の機能を main に merge

    して、いつでも本番環境に展開できるように する • 1~2週間以上存在してはいけないことが推奨 されている • 基本的に静的 request単位などでON/OFFが切り替わらない Release Toggles
  11. • A/Bテストやカナリアリリースを実現するために使 用 • 仮説検証 用 の実装や 一 部のユーザーに限定して機能を公開する場合などに使 用

    される • データ駆動型の最適化に使 用 される • A/Bテストなどの場合は有意なデータを収集する ために 十 分な期間が必要だが、システムに変更 を加えてしまうとその影響が載ってしまう可能性 があるため、時間や週単位が推奨されている Experiment Toggles
  12. • システム動作の運 用面 の制御に使 用 • パフォーマンスへの影響が不明確な新機能をロールアウトする際に導 入 し、 必要に応じて無効化や低下をできるようにする

    • 障害発 生 時のリカバリー 用 として使 用 する • テクニックとして、システム全体の負荷が 高 まっ てしまった際に重要ではない機能を OFF にして 対応する 「Circuit Breaker」としての機能も あるらしい Ops Toggles
  13. • 特定のユーサーのみ機能を加えたり、特別な機能を解放する場合などに使 用 • 課 金 ユーザーや PoC の対象者に限定的に機能を解放するなど •

    Experiment Toggles が仮説検証のA/Bテストなどの 用 途であったのに対し、 Permission Toggles はすでに完成している 機能の限定公開の 用 途として使 用 される Permission Toggles
  14. • feature fl ag の状態をリモート管理してくれる • feature fl ag の状態を管理する

    方 法は 色 々ある 環境変数 json などの静的ファイル • feature fl ag の値を管理し、リアルタイムでの変更や context 情報から適切に値を決定す るなどを実現するサービスが存在 • これらは基本的にSDK(API)になっており、 fl ag 名と contex 情報(ターゲティングなど をする際のユーザー情報やリクエスト情報など)を渡して、その結果として評価値を返す フラグ管理システム(以下FFSaaS)
  15. • SDK の実装は各サービス違う 変更したくなった場合には使 用 している実装部分 を全て修正する必要 • 開発のフェーズやニーズによって速度や求 められる機能が異なる

    置き換えたくなる需要がそこそこ発 生 する可能性 • 移 行 が 大 変 置き換えした際にデグレする可能性もある • 複数兼 用 する可能性もある 複数の SDK を使いこなす必要がある 依存などがあった場合は 大 変 ベンダーロックイン import configcat "github.com/configcat/go-sdk/v9" client := configcat.NewClient("#YOUR-SDK-KEY#") enabled := client.GetBoolValue("flagName", false, nil) if enabled { doTheNewThing() } $PO fi H$BU import flagsmith "github.com/Flagsmith/flagsmith-go-client/v3" client := flagsmith.NewClient(os.Getenv("#YOUR-SDK-KEY#")) flags, _ := client.GetEnvironmentFlags(ctx) enabled, _ := flags.IsFeatureEnabled("flagName") if enabled { doTheNewThing() } 'MBHTNJUI
  16. • SDK の実装は各サービス違う 変更したくなった場合には使 用 している実装部分 を全て修正する必要 • 開発のフェーズやニーズによって速度や求 められる機能が異なる

    置き換えたくなる需要がそこそこ発 生 する可能性 • 移 行 が 大 変 置き換えした際にデグレする可能性もある • 複数兼 用 する可能性もある 複数の SDK を使いこなす必要がある 依存などがあった場合は 大 変 ベンダーロックイン import configcat "github.com/configcat/go-sdk/v9" client := configcat.NewClient("#YOUR-SDK-KEY#") enabled := client.GetBoolValue("flagName", false, nil) if enabled { doTheNewThing() } $PO fi H$BU import flagsmith "github.com/Flagsmith/flagsmith-go-client/v3" client := flagsmith.NewClient(os.Getenv("#YOUR-SDK-KEY#")) flags, _ := client.GetEnvironmentFlags(ctx) enabled, _ := flags.IsFeatureEnabled("flagName") if enabled { doTheNewThing() } 'MBHTNJUI GetBoolValue という関数で bool を取得 GetEnvironmentFlags という関数で fl ags を取得 IsFeatureEnabled という関数で bool 取得
  17. • 2022年5 月 に公開された 比 較的新しい OSS CNCF の Incubating

    Project にも採択 • feature fl ag 管理のオープン標準規格 • 直接的な feature fl ag を管理する OSS ではなくあくまで抽象化層 • Provider を介して、対応している feature fl ag service と繋ぐことができる OpenFeature Provider • Provider の interface を実装すると任意の custom provider を作れる ex. 環境変数、S 3 、 自 前サーバー etc … • Dynatrace が主導している OpenFeature とは 0QFO'FBUVSFc$/$'
  18. • ベンダーロックインを避けられる アプリケーションの実装部分が変わらない 好きな FFSaaS を気軽に乗り換えられる 複数のツールを使うこともできる • 環境や種類によって FFSaaS

    を使い分けられる 環境 dev、stg では頻繁に fl ag を 入 れ替えるため、便利さのために FFSaaS を使 用 するが、本番環境ではリードタイムを許容できるのと 金 銭 面 から環境変数によって更新する 種類 ターゲティングがなく、特に context 情報などを必要としない Release Toggle などは環境変数を使 用 特定の条件の 人 に出し分ける AB テストや限定公開の条件をある程度 長 期的に更新する必要があるような Experiment Toggle や Permission Toggle などでは FFSaaS を使う • feature fl ag の実装箇所は変えることなく Provider を差し替えるのみ OpenFeature のなにが嬉しいか
  19. OpenFeature のなにが嬉しいか OpenFeature: 標準 API を定義し共通の SDK を提供 ベンダー: プロバイダーの開発に集中

    アプリケーション開発: 共通の SDK を使 用 して実装し Provider を付け替えるだけ OpenFeature - a standard for feature fl agging | OpenFeature
  20. • アプリケーション実装者が操作する主要 コンポーネント • 手 順 Provider 設定 Client 作成

    Flag 評価 • Flag 評価値は2種類 Basic Evaluation Detailed Evaluation Evaluation API &WBMVBUJPO"1*c0QFO'FBUVSF &WBMVBUJPO%FUBJMT4USVDUVSF'JFMET 5ZQFTBOE%BUB4USVDUVSFTc0QFO'FBUVSF &SSPS$PEF
  21. // set a value to the global context openfeature.SetEvaluationContext(openfeature.NewTargetlessEvaluationContext( map[string]interface{}{

    "region": "us-east-1-iah-1a", }, )) // set a value to the client context client := openfeature.NewClient("my-app") client.SetEvaluationContext(openfeature.NewTargetlessEvaluationContext( map[string]interface{}{ "version": "1.4.6", }, )) // set a value to the invocation context evalCtx := openfeature.NewEvaluationContext( "user-123", map[string]interface{}{ "company": "Initech", }, ) boolValue, err := client.BooleanValue("boolFlag", false, evalCtx) EvaluationContext • 動的評価 用 のコンテキストデータ ホスト アプリケーション識別 子 IPアドレス etc … • フラグ評価に対して暗黙的 ・ 明 示 的に渡すことができる
  22. // set a value to the global context openfeature.SetEvaluationContext(openfeature.NewTargetlessEvaluationContext( map[string]interface{}{

    "region": "us-east-1-iah-1a", }, )) // set a value to the client context client := openfeature.NewClient("my-app") client.SetEvaluationContext(openfeature.NewTargetlessEvaluationContext( map[string]interface{}{ "version": "1.4.6", }, )) // set a value to the invocation context evalCtx := openfeature.NewEvaluationContext( "user-123", map[string]interface{}{ "company": "Initech", }, ) boolValue, err := client.BooleanValue("boolFlag", false, evalCtx) EvaluationContext • 動的評価 用 のコンテキストデータ ホスト アプリケーション識別 子 IPアドレス etc … • フラグ評価に対して暗黙的 ・ 明 示 的に渡すことができる
  23. &WFOUTc0QFO'FBUVSF • プロバイダーまたはフラグ管理システムの状態の取得対応 • 検知可能な状態 フラグの定義変更 新フラグの追加や既存フラグの変更 プロバイダーの準備完了 プロバイダーが使 用

    可能になったことの通知 エラー状態 フラグの評価やプロバイダーとの通信でのエラー発 生 • プロバイダーの役割 イベント発 行 特定イベントを検知したことを 示 すコールバック実 行 オプションでイベントデータの提供 • クライアント or グローバルAPIハンドラーの役割 発 行 されたイベントデータを使 用 して処理を実 行 Events
  24. 課題感 • FFSaaS を使 用 した場合フラグ情報が分散してしまう • 問題点 typo の検出ができない

    FFSaaSに現存する fl ag の全容が把握しにくい( fl ag のステータスではなく存在するか)
  25. 課題感 • FFSaaSを使 用 した場合に情報が分散してしまう • 問題点 typo の検出ができない FFSaaSに現存する

    fl ag の全容が把握しにくい( fl ag のステータスではなく存在するか)
  26. 課題感 • FFSaaSを使 用 した場合に情報が分散してしまう • 問題点 typo の検出ができない FFSaaSに現存する

    fl ag の全容が把握しにくい( fl ag のステータスではなく存在するか) アプリケーションコードとTerraform(FFSaaS) は分離してしまう
  27. 宣 言 管理 ・ 集約管理のモチベーション • FFSaaS、アプリケーション、Terraform に fl ag

    情報を書く必要があるがどれも 本質は同じもの 同じ情報なのであれば集約して管理したい • GitOps として宣 言 的に管理したい GitOps で全て管理したい feature fl ag の情報も Git 上で管理するようにしたい 作成はまだしも、削除し忘れが多いのでそれをしっかり認識できるようにしたい • fl ag について記述された 一 つの場所から、 用 途に合わせて 色 々なものを 自 動 生 成 したい feature fl ag エコシステムの開発をしたい
  28. Protoc Plugin • Protocol Bu ff ers に記述された情報を使 用 して

    自 動 生 成などを 行 う • Extend を使 用 して message や fi eld に情報を 追加したり拡張することができる • 今回は protoc plugin をサクッと書ける protoc-gen-star(PG*)を使 用 MZGUQSPUPDHFOTUBSQSPUPDQMVHJOMJCSBSZGPSF ffi DJFOUQSPUPCBTFEDPEFHFOFSBUJPO Go Conference mini 2 023 Winter in KYOTO %FW0QT%BZT50,:0
  29. Protoc Plugin • すでにPJTでは 大 量の protoc plugin がある •

    ここに新しく追加する形 自 動 生 成されたファイル数 1 , 9 6 8 自 動 生 成された 行 数 338 , 667
  30. syntax = "proto3"; package featureflag; import "ext/feature_flag.proto"; option (ext.feature_flag_config) =

    { configcat: { env_names: [ "stg", "prod" ] } flagsmith: { env_names: [ "dev" ] } }; message Release { option (ext.feature_flag_message).default_expiry_days = 7; bool my_first_test_bool = 1; bool my_second_test_bool = 2 [(ext.feature_flag_field).expiry_days = 30]; string my_first_test_string = 3; int32 my_first_test_int = 4; float my_first_test_float = 5; } message Experiment { bool my_first = 1 [(ext.feature_flag_field).expiry_days = 14]; } message Ops { option (ext.feature_flag_message).default_expiry_days = 90; bool my_first = 1; } message Permission { bool my_first = 1; } feature fl ag proto fi le
  31. syntax = "proto3"; package featureflag; import "ext/feature_flag.proto"; option (ext.feature_flag_config) =

    { configcat: { env_names: [ "stg", "prod" ] } flagsmith: { env_names: [ "dev" ] } }; message Release { option (ext.feature_flag_message).default_expiry_days = 7; bool my_first_test_bool = 1; bool my_second_test_bool = 2 [(ext.feature_flag_field).expiry_days = 30]; string my_first_test_string = 3; int32 my_first_test_int = 4; float my_first_test_float = 5; } message Experiment { bool my_first = 1 [(ext.feature_flag_field).expiry_days = 14]; } message Ops { option (ext.feature_flag_message).default_expiry_days = 90; bool my_first = 1; } message Permission { bool my_first = 1; } feature fl ag proto fi le
  32. option (ext.feature_flag_config) = { configcat: { env_names: [ "stg", "prod"

    ] } flagsmith: { env_names: [ "dev" ] } }; feature fl ag proto fi le • 作成する terraform 用 の情報 • 各FFSaaSで必要な情報を 入 れる
  33. syntax = "proto3"; package featureflag; import "ext/feature_flag.proto"; option (ext.feature_flag_config) =

    { configcat: { env_names: [ "stg", "prod" ] } flagsmith: { env_names: [ "dev" ] } }; message Release { option (ext.feature_flag_message).default_expiry_days = 7; bool my_first_test_bool = 1; bool my_second_test_bool = 2 [(ext.feature_flag_field).expiry_days = 30]; string my_first_test_string = 3; int32 my_first_test_int = 4; float my_first_test_float = 5; } message Experiment { bool my_first = 1 [(ext.feature_flag_field).expiry_days = 14]; } message Ops { option (ext.feature_flag_message).default_expiry_days = 90; bool my_first = 1; } message Permission { bool my_first = 1; } feature fl ag proto fi le
  34. message Release { option (ext.feature_flag_message).default_expiry_days = 7; bool my_first_test_bool =

    1; bool my_second_test_bool = 2 [(ext.feature_flag_field).expiry_days = 30]; string my_first_test_string = 3; int32 my_first_test_int = 4; float my_first_test_float = 5; } message Experiment { bool my_first = 1 [(ext.feature_flag_field).expiry_days = 14]; } message Ops { option (ext.feature_flag_message).default_expiry_days = 90; bool my_first = 1; } message Permission { bool my_first = 1; } feature fl ag proto fi le • fl ag のタイプごとに fl agを記述 • bool以外の型を使いたい場合は string や int 32 などを使 用 • expiry days は reminder test で 使 用 する(後述)
  35. // Code generated by protoc-gen-go-feature-flag. DO NOT EDIT. // source:

    go.tmpl package copenfeature type FeatureFlag string const ( ReleaseMyFirstTestBool FeatureFlag = "RELEASE_MY_FIRST_TEST_BOOL" ReleaseMySecondTestBool FeatureFlag = "RELEASE_MY_SECOND_TEST_BOOL" ReleaseMyFirstTestString FeatureFlag = "RELEASE_MY_FIRST_TEST_STRING" ReleaseMyFirstTestInt FeatureFlag = "RELEASE_MY_FIRST_TEST_INT" ReleaseMyFirstTestFloat FeatureFlag = "RELEASE_MY_FIRST_TEST_FLOAT" ExperimentMyFirst FeatureFlag = "EXPERIMENT_MY_FIRST" OpsMyFirst FeatureFlag = "OPS_MY_FIRST" PermissionMyFirst FeatureFlag = “PERMISSION_MY_FIRST" ) feature fl ag for application • feature fl ag 実装部分で使うようの fl ag を定数で定義 • 自 動 生 成することでコピペなどによ る事故を回避 • 今回は Go を 生 成したが別 言 語でも 可能 fronend/native も同様に使 用 可
  36. if copenfeature.BooleanValue(ctx, copenfeature.ReleaseMyFirstTestBool) { fmt.Println("my_first_test_bool is true") } else {

    fmt.Println("my_first_test_bool is false") } 生 成された fl agを使う • 生 成された定数を指定すればいい • 定数も 自 動 生 成されており、ソースも同じなので 一 致が保証される
  37. // Code generated by protoc-gen-go-feature-flag. DO NOT EDIT. // source:

    config_cat.tmpl resource "configcat_setting" "releasemy_first_test_bool" { config_id = configcat_config.test_config.id key = "RELEASEMY_FIRST_TEST_BOOL" name = "ReleaseMyFirstTestBool" setting_type = "boolean" order = 0 } Terraform • FFSaaS に fl ag を追加する Terraform を 生 成 • アプリケーション 用 の実装(Go)と 一 致が保証 • 複数の FFSaaS 用 の Terraform を 生 成すれば変更が容易 OpenFeature Provider を付け替えればすぐさま使 用 可能
  38. Application OpenFeature を採 用 しているので OpenFeature Provider を変えるだけ FFSaaS 自

    動 生 成しているので 生 成する Terraform を変えるだけ
  39. message Release { option (ext.feature_flag_message).default_expiry_days = 7; bool my_first_test_bool =

    1; bool my_second_test_bool = 2 [(ext.feature_flag_field).expiry_days = 30]; string my_first_test_string = 3; int32 my_first_test_int = 4; float my_first_test_float = 5; } message Experiment { bool my_first = 1 [(ext.feature_flag_field).expiry_days = 14]; } message Ops { option (ext.feature_flag_message).default_expiry_days = 90; bool my_first = 1; } message Permissioning { bool my_first = 1; } feature fl ag の削除 • feature fl ag の削除は徹底してやら ないとコードが複雑化してしまう • 作成後 一 定期間経過したら通知する ようにしたい • 一 定期間経過したら落ちるテストを 自 動 生 成して CI で検知する
  40. message Release { option (ext.feature_flag_message).default_expiry_days = 7; bool my_first_test_bool =

    1; bool my_second_test_bool = 2 [(ext.feature_flag_field).expiry_days = 30]; string my_first_test_string = 3; int32 my_first_test_int = 4; float my_first_test_float = 5; } message Experiment { bool my_first = 1 [(ext.feature_flag_field).expiry_days = 14]; } message Ops { option (ext.feature_flag_message).default_expiry_days = 90; bool my_first = 1; } message Permissioning { bool my_first = 1; } feature fl ag の削除 通知するまでに 日 数を Proto File で指定する • feature fl ag の削除は徹底してやら ないとコードが複雑化してしまう • 作成後 一 定期間経過したら通知する ようにしたい • 一 定期間経過したら落ちるテストを 自 動 生 成して CI で検知する
  41. feature fl ag の削除 // Code generated by protoc-gen-go-feature-flag. DO

    NOT EDIT. // source: reminder.tmpl package reminder import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/BIwashi/test/backend/pkg/copenfeature" "github.com/BIwashi/test/backend/pkg/ctime" ) var ( releaseMyFirstTestBoolCreationDate = time.Date(2024, 6, 12, 22, 44, 23, 271402523, ctime.JSTLocation) releaseMyFirstTestBoolExpiryDate = releaseMyFirstTestBoolCreationDate.AddDate(0, 0, 7) ) func TestReleaseMyFirstTestBoolFlagExpiry(t *testing.T) { _ = copenfeature.ReleaseMyFirstTestBool t.Run("MyFirstTestBoolFlag", func(t *testing.T) { assert.True(t, time.Now().In(ctime.JSTLocation).Before(releaseMyFirstTestBoolExpiryDate)) }) }
  42. feature fl ag の削除 // Code generated by protoc-gen-go-feature-flag. DO

    NOT EDIT. // source: reminder.tmpl package reminder import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/BIwashi/test/backend/pkg/copenfeature" "github.com/BIwashi/test/backend/pkg/ctime" ) var ( releaseMyFirstTestBoolCreationDate = time.Date(2024, 6, 12, 22, 44, 23, 271402523, ctime.JSTLocation) releaseMyFirstTestBoolExpiryDate = releaseMyFirstTestBoolCreationDate.AddDate(0, 0, 7) ) func TestReleaseMyFirstTestBoolFlagExpiry(t *testing.T) { _ = copenfeature.ReleaseMyFirstTestBool t.Run("MyFirstTestBoolFlag", func(t *testing.T) { assert.True(t, time.Now().In(ctime.JSTLocation).Before(releaseMyFirstTestBoolExpiryDate)) }) } フラグ作成 日 時と 通知の来る 日 時を指定
  43. feature fl ag の削除 // Code generated by protoc-gen-go-feature-flag. DO

    NOT EDIT. // source: reminder.tmpl package reminder import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/BIwashi/test/backend/pkg/copenfeature" "github.com/BIwashi/test/backend/pkg/ctime" ) var ( releaseMyFirstTestBoolCreationDate = time.Date(2024, 6, 12, 22, 44, 23, 271402523, ctime.JSTLocation) releaseMyFirstTestBoolExpiryDate = releaseMyFirstTestBoolCreationDate.AddDate(0, 0, 7) ) func TestReleaseMyFirstTestBoolFlagExpiry(t *testing.T) { _ = copenfeature.ReleaseMyFirstTestBool t.Run("MyFirstTestBoolFlag", func(t *testing.T) { assert.True(t, time.Now().In(ctime.JSTLocation).Before(releaseMyFirstTestBoolExpiryDate)) }) } protoから削除(定数が削除)されたら テストファイルを 手 動削除する 忘れていたらコンパイルエラーで気づけるように
  44. feature fl ag の削除 // Code generated by protoc-gen-go-feature-flag. DO

    NOT EDIT. // source: reminder.tmpl package reminder import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/BIwashi/test/backend/pkg/copenfeature" "github.com/BIwashi/test/backend/pkg/ctime" ) var ( releaseMyFirstTestBoolCreationDate = time.Date(2024, 6, 12, 22, 44, 23, 271402523, ctime.JSTLocation) releaseMyFirstTestBoolExpiryDate = releaseMyFirstTestBoolCreationDate.AddDate(0, 0, 7) ) func TestReleaseMyFirstTestBoolFlagExpiry(t *testing.T) { _ = copenfeature.ReleaseMyFirstTestBool t.Run("MyFirstTestBoolFlag", func(t *testing.T) { assert.True(t, time.Now().In(ctime.JSTLocation).Before(releaseMyFirstTestBoolExpiryDate)) }) } 現在時刻が通知 日 時を過ぎていたらテストが失敗 CIで検知できるようにする
  45. # Code generated by protoc-gen-go-feature-flag. DO NOT EDIT. - name:

    "ff/ReleaseMyFirstTestBool" color: "000000" description: "Release flag" - name: "ff/ReleaseMySecondTestBool" color: "000000" description: "Release flag" - name: "ff/ReleaseMyFirstTestString" color: "000000" description: "Release flag" - name: "ff/ReleaseMyFirstTestInt" color: "000000" description: "Release flag" - name: "ff/ReleaseMyFirstTestFloat" color: "000000" description: "Release flag" - name: "ff/ExperimentMyFirst" color: "000000" description: "Experiment flag" - name: "ff/OpsMyFirst" color: "000000" description: "Ops flag" - name: "ff/PermissioningMyFirst" color: "000000" description: "Permissioning flag" PR ラベル • feature fl ag を削除する際に追加した時の実装を 見 たい • 削除ライフサイクル(後述)対策として、リリー スされたバージョンに含まれている feature fl ag を知りたい • ghactions-github-labeler で 自 動 生 成した yaml からラベルを作成 yaml の git di ff 差分からラベルを PR に付与 DSB[ZNBYHIBDUJPOHJUIVCMBCFMFS(JU)VC"DUJPOUP NBOBHFMBCFMTPO(JU)VC
  46. ライフサイクルの違い • FFSaaS に対しての操作は基本的に全環境 一 括で 行 われる • アプリケーションコードは各環境ごとに

    行 われる • FFSaaS から削除する際はアプリケーションコードから feature fl ag が削除 された version が prod まで上がって初めて可能になる 削除のライフサイクルが異なる 適切に削除するには削除ライフサイクルを考慮して設計する必要がある
  47. prod まで fl ag のアプリケーション実装の version が上がると削除できる この時点では prod で

    fl ag を使 用 しているので FFSaaS からは削除してはいけない
  48. Release Note: v 1 . 0 . 0 • [ADD]

    Add fl ag A and … • [ADD] Add fl ag X and … • [DELETE] Remove fl ag Y … • hoge • foo • piyo ff / fl agA ff / fl agX ff / fl agY PRに fl ag 名のラベルを 自 動でつける タイトルに [ADD] or [DELETE] をつける
  49. Release Note: v 1 . 0 . 0 • [ADD]

    Add fl ag A and … • [ADD] Add fl ag X and … • [DELETE] Remove fl ag Y … • hoge • foo • piyo ff / fl agA ff / fl agX ff / fl agY prodまで 行 ったらその version を送信
  50. Release Note: v 1 . 0 . 0 • [ADD]

    Add fl ag A and … • [ADD] Add fl ag X and … • [DELETE] Remove fl ag Y … • hoge • foo • piyo ff / fl agA ff / fl agX ff / fl agY [ADD] がついている PR を探す フラグ名をラベルから取得する
  51. Release Note: v 1 . 0 . 0 • [ADD]

    Add fl ag A and … • [ADD] Add fl ag X and … • [DELETE] Remove fl ag Y … • hoge • foo • piyo ff / fl agA ff / fl agX ff / fl agY feature fl ag を 自 動削除する (Pͷ"45Λղੳͯ͠'FBUVSF5PHHMFΛ૟আ͢ΔGSFFF%FWFMPQFST)VC
  52. Release Note: v 1 . 0 . 0 • [ADD]

    Add fl ag A and … • [ADD] Add fl ag X and … • [DELETE] Remove fl ag Y … • hoge • foo • piyo ff / fl agA ff / fl agX ff / fl agY Release Note: v 2 . 0 . 0 • [DELETE] Remove fl ag A ff / fl agA [DELETE] をタイトルにつけて 生 成
  53. Release Note: v 1 . 0 . 0 • [ADD]

    Add fl ag A and … • [ADD] Add fl ag X and … • [DELETE] Remove fl ag Y … • hoge • foo • piyo ff / fl agA ff / fl agX ff / fl agY Release Note: v 2 . 0 . 0 • [DELETE] Remove fl ag A ff / fl agA prodまで 行 ったらその version を送信
  54. Release Note: v 1 . 0 . 0 • [ADD]

    Add fl ag A and … • [ADD] Add fl ag X and … • [DELETE] Remove fl ag Y … • hoge • foo • piyo ff / fl agA ff / fl agX ff / fl agY Release Note: v 2 . 0 . 0 • [DELETE] Remove fl ag A ff / fl agA [DELETE] がついている PR を探す フラグ名をラベルから取得する
  55. Release Note: v 1 . 0 . 0 • [ADD]

    Add fl ag A and … • [ADD] Add fl ag X and … • [DELETE] Remove fl ag Y … • hoge • foo • piyo ff / fl agA ff / fl agX ff / fl agY Release Note: v 2 . 0 . 0 • [DELETE] Remove fl ag A ff / fl agA フラグ名の terraform fi le を削除
  56. まとめ • OpenFeature と protoc plugin 活 用 して、 自

    由にフラグ管理システムを差 し替えられるようにした • protoc plugin を活 用 して proto fi le に情報を集約することによって情報を 宣 言 集約管理できた アプリケーションコード インフラコード リマインダーテストコード PRラベラー • feature fl ag を意識しなくてもいい feature fl ag エコシステムを構築したい The Go gopher was designed by Renée French. GO Feature Flag
  57. fval, err := client.ofc.BooleanValue(ctx, "testFlag", false, openfeature.EvaluationContext{}) if err !=

    nil { logger.Error(ctx, "failed to get boolean value" } OpenFeature の具体実装例 for Go • OpenFeature を素で使った場合 毎回引数に 入 れないといけないものが多くて少し可読性が悪い デフォルト値、evaluation context 毎回必要なわけではないので必要な時だけ 入 れるようにしたい
  58. func BooleanValue(ctx context.Context, flag FeatureFlag, opts ...Option[bool]) bool { var

    ( err error opt = options[bool]{ evaluationContext: ctxkey.GetOpenFeatureEvaluationContext(ctx), defaultValue: false, } ) span, ctx := trace.StartSpan( ctx, fmt.Sprintf("copenfeature.BooleanValue.%s", flag), ) defer func() { span.Finish(err) }() for _, o := range opts { o(&opt) } eflag, err := client.ofc.BooleanValue( ctx, string(flag), opt.defaultValue, opt.evaluationContext, opt.ofcOpts..., ) if err != nil { errorLogger(ctx, "failed to get boolean value", flag, err) return opt.defaultValue } return eflag } 少しラップ • functional option patternで 入 れ られるように少しラップしている • span も仕込んでいる
  59. func EvaluationContextUnaryServerInterceptor(logger log.Logger) grpc.UnaryServerInterceptor { return func( ctx context.Context, req

    interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, ) (interface{}, error) { ai, ed := extractInfoFromMetadata(ctx) userID, evalCustomData := getUserInfo(ctx, info) evaluation := map[string]interface{}{ "appVersion": ai.xAppVesion, "appBuild": ai.xAppBuildNumber, "deviceModel": ed.xDeviceModel, "customData": evalCustomData, } ctx = ctxkey.SetOpenFeatureEvaluationContext( ctx, copenfeature.NewEvaluationContext( userID, evaluation, ), ) return handler(ctx, req) } } EvaluationContext • gRPC Interceptor で取得した動 的評価表のデータを context に 入 れておく • ラップした関数ではこの context から取り出したデータを 使 用 するように実装しておく • Option が 入 れられた際は Overwrite する
  60. Option • Option にはジェネリクスを使 用 defaultValue に型制約をつける func BooleanValue(ctx context.Context,

    flag FeatureFlag, opts ...Option[bool]) bool { ... return eflag } type options[T any] struct { evaluationContext of.EvaluationContext ofcOpts []of.Option defaultValue T } type Option[T any] func(*options[T]) func WithEvaluationContext[T any](ctx of.EvaluationContext) Option[T] { return func(o *options[T]) { o.evaluationContext = ctx } } func WithOFClientOptions[T any](opts ...of.Option) Option[T] { return func(o *options[T]) { o.ofcOpts = opts } } func WithDefaultValue[T any](v T) Option[T] { return func(o *options[T]) { o.defaultValue = v } }
  61. if copenfeature.BooleanValue( ctx, copenfeature.TestFlag, copenfeature.WithDefaultValue(true), copenfeature.WithEvaluationContext[bool](copenfeature.NewEvaluationContext("", nil)), ) { …

    } Option copenfeature.WithDefaultValue("string"), Bool 以外の型の値を default value に 入 れようとすると怒られる