お久しぶりです。
最近すっかりポケモンにはまっており、仕事と読書・paiza のスキルチェック以外はほとんどポケモン盾をやってました。

さて、今回は機能フラグ (feature flag) に関する話です。
プロダクトによっては、リリース前の機能など一部条件にマッチしたユーザにしか閲覧させたくないといったケースがあると思います。
そういった際によく使われるのが機能フラグです。

機能フラグって?

アプリケーションのコードは変更せずに、機能フラグを操作することで機能を使えるかどうかを制御します。
便利な仕組みではあるものの、そのフラグを取得・判定する処理自体はアプリケーションに必要な処理なので、一定の実装コストはかかります。
そうはいっても、非エンジニアであってもほぼリアルタイムに機能提供を制御できるという点は大きなメリットです。

この機能フラグを提供するツールについては自前で用意することも可能ではあるものの、操作用の UI を用意したりとなかなか手間なので基本的に SaaS を使うことが多いです。
有名なところだと Firebase Remote ConfigLaunchDarklySplit などがあります。

今回やること

ここでようやく本題です。
AWS (Amazon Web Services) でもこの仕組みが提供されています(私は最近知りました…)。

AWS は仕事・プライベート問わず使う機会も多く、AWS 上で完結できると便利だなという思いから、今回上記サービスを使ってみることにしました。

機能フラグを作成

以前の記事で書いた Terraform を使いたかったんですが、Evidently についてはまだサポートされていなかったため、やむなく AWS コンソールで作業します。
CloudWatch → Evidently を選択し、プロジェクトを作成します。
今回は evidently-sample としました。
あくまで検証なので、評価イベントも保存しないようにしておきます。

作成されたプロジェクトを選択します。

機能を追加します。

真偽値以外も利用できるようですが、今回はわかりやすく真偽値のみ利用します。
バリエーションで返すパターンを定義しておき(今回は真偽値なので true/false の2種類)、オーバーライドで返す条件を追加します。
今回は「ユーザが testuser の場合のみ真を返す」という設定にしておきます。

コードを書く

続いてコードです。
今回は Go を使って試しました。
仕事・プライベート問わず使うことが多く、最近一番好きな言語でもあります。

なお、AWS プロファイルは設定済みとします。
利用する AWS プロファイルが default ではない場合は、事前に export AWS_PROFILE={profile name} を実行して利用するプロファイルを設定しておきます。

package main

import (
    "context"
    "flag"
    "fmt"
    "log"
    "os"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/evidently"
    "github.com/aws/aws-sdk-go-v2/service/evidently/types"
)

var (
    // コマンドから取得するユーザ
    entityID    = flag.String("user", "dummy", "Specify user")
    // サブコマンドの一覧
    subCommands = map[string]func() error{
        "hoge": func() error {
            fmt.Println("Hello hoge!!")
            return nil
        },
    }
)

// 指定したユーザが指定したサブコマンドを使えるかを、Evidently で定義した値から判定する
func enableCommand(c *evidently.Client, entityID string, command string) (bool, error) {
    resp, err := c.EvaluateFeature(context.TODO(), &evidently.EvaluateFeatureInput{
        EntityId: aws.String(entityID),
        Project:  aws.String("evidently-sample"),
        Feature:  aws.String(fmt.Sprintf("enable-%s-command", command)),
    })

    if err != nil {
        return false, err
    }

    // resp.Value の型は types.VariableValue インターフェースなので、該当する実装にキャストする
    return resp.Value.(*types.VariableValueMemberBoolValue).Value, nil
}

func main() {
    // 設定ファイルを読み込み
    cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("ap-northeast-1"))
    if err != nil {
        log.Fatalf("unable to load SDK config, %v", err)
    }

    // Evidently のサービスクライアントを作成
    svc := evidently.NewFromConfig(cfg)
    if len(os.Args) > 1 {
        flag.CommandLine.Parse(os.Args[2:])

        sc := os.Args[1]
        fn, ok := subCommands[sc]
        // 指定したサブコマンドが定義されていなければエラー
        if !ok {
            log.Fatalf("invalid command: %s", sc)
        }
        // 指定したユーザにサブコマンドを使う権限がなければエラー
        if flag, err := enableCommand(svc, *entityID, sc); err != nil {
            log.Fatal(err)
        } else if !flag {
            log.Fatalf("invalid command: %s", sc)
        }
        if err := fn(); err != nil {
            log.Fatal(err)
        }
    }
}

以下のように実行します。

$ # オーバーライドで設定したユーザ (testuser) の場合
$ go run cmd/evidently-sample/main.go hoge --user testuser
Hello hoge!!

$ # 上記以外の場合
$ go run cmd/evidently-sample/main.go hoge --user invaliduser
2022/08/29 10:23:26 invalid command: hoge
exit status 1

なお、今回使ったコードは以下リポジトリに置いています。

まとめ

SDK のおかげもあって、想像以上にシンプルに利用することができました。
ただ、AWS の認証情報が必須なので、フロントから直接 Evidently をコールすることが難しいように思いました(これに関しては良い方法があるのかもしれないので、有識者がいればぜひ教えていただきたいです)。

ちなみに、課金体系は以下の通りでした。

  • Evidently イベント: 100 万イベントあたり 5 USD
  • Evidently 分析ユニット: 100 万分析ユニットあたり 7.50 USD

ただ、こちらにもある通り、一定は無料利用枠で利用できるようです。

Evidently に限らず、機能フラグを提供するサービスでは A/B テストもできるため、機会があればそちらも試してみようと思います。