KAKEHASHI Tech Blog

カケハシのEngineer Teamによるブログです。

Lambda Powertools の Feature Flags やってみた

こちらの記事は カケハシ Advent Calendar 2022 の 2 日目の記事になります。 https://adventar.org/calendars/7444

はじめに

こんにちは、おくすり連絡帳 Pocket Musubiというサービスを開発している石井です。

私は主にアプリケーションのサーバーレスなインフラとバックエンドを開発をしております。サーバレスとなると必然的に Lambda を利用することがほとんどです。 そこで Lambda の開発を簡単にしてくれる AWS Lambda Powertools for Python (以下:Lambda Powertools)というライブラリーがありましてよく使います。

Lambda Powertools は個人的にも好きなので「全部の機能を触ってみたいなー」と思っていますが使ったことのない機能もたくさんあります。今回はその中で、前々から気になっていた Feature Flags という機能を調べてみました。

この記事でやること

Lambda Powertools の Feature Flags を使って、Lambda のリクエストイベントにuser_idの key があり、値がhogeだった場合は True, それ以外は False になるフラグを実装して動作確認をしてみます。インフラは CDK(2.46.0) を使って実装します。

全体の構成

絵にするとざっくりこんな感じです。

AppConfig と Lambda を利用しますので CDK でデプロイします。 そしてコンソールから Lambda を叩いて動作確認をします。

AppConfig ではアプリケーション、環境、プロファイルを各々1つづつ作成します。このプロファイルの中に Lambda Powertools の Feature Flags の設定値を登録します。 Lambda には AppConfig のアクセス権を付与してあげて、Lambda Powertools を使うためのパブリックレイアーを紐付けます。

補足) AWS AppConfig とは?

アプリケーションの設定値やフィーチャーフラグを安全かつ迅速にデプロイしてくれるサービスです。 余談ですが AppConfig のフィーチャーフラグ機能と Lambda Powertools の Feature Flags は全くの別物です。また Feature Flags は AppConfig を使いますが、フィーチャーフラグ機能は使いません。AppConfig のフィーチャーフラグ機能でない機能を使ってフィーチャーフラグを実現するのが Feature Flags です。

AppConfig は階層構造になっており、利用するためにはまずアプリケーションを作成する必要があります。 次にアプリケーションの下に環境を追加します。アプリケーションと環境の関係は1:Nです。 その後さらに環境の下にプロファイルを追加します。環境とプロファイルの関係も1:Nです。 追加されたプロファイルはバージョンと設定値を持っており、この設定値に値を入力します。

ここで設定した値を Feature Flags はいい感じに処理してくれます。

Feature Flags の AppConfig に設定する値のフォーマットについて

通常 AppConfig のプロファイルの値は自由に設定することができます。しかし Feature Flags を利用する場合はフォーマットが決まっています。 詳細は公式を参照ください。

今回はフォーマットに従い、こんな感じにしてみました。

features_config = {
    # ルール付きの動的に変化するフラグだよ
    "dynamic_hogehoge_flags": {
        "default": False,
        "rules": {
            "hoge rule 1": {
                "when_match": True,
                "conditions": [
                    {
                        "action": "EQUALS",
                        "key": "user_id",
                        "value": "hoge",
                    }
                ],
            }
        },
    },
    # 静的なフラグ
    "static_hogehoge_flag": {
        "default": False,
    },
}

Dict の第一階層は任意のフラグ名です。その下にフラグの定義を書いていきます。 中を少し見ますとrulesにフラグが返す値のルールを書きます。 ルール単体の定義はconditionsに条件を書き、when_matchに条件がマッチした場合の返り値を書きます。 ルールは複数記述が可能で、複数ルールにマッチする場合は最初にマッチしたルールの値を返します。

defaultにはどのルールにもマッチしなかった場合の値を書きます。

case 文と似てますね。

インフラとサンプルアプリのデプロイ

ここまでの話をまとめて CDK のテンプレートに落とすとこんな感じです。

# app.py
# ----------------
import json
import os

import aws_cdk as cdk
import aws_cdk.aws_appconfig as appconfig
import aws_cdk.aws_iam as iam
import aws_cdk.aws_lambda as _lambda

app = cdk.App()
stack = cdk.Stack(app, "lambda-powertools-feature-flags-demo-stack")

features_config = {
    # ルール付きの動的に変化するフラグだよ
    "dynamic_hogehoge_flags": {
        "default": False,
        "rules": {
            "hoge rule 1": {
                "when_match": True,
                "conditions": [
                    {
                        "action": "EQUALS",
                        "key": "user_id",
                        "value": "hoge",
                    }
                ],
            }
        },
    },
    # (参考)ルールのない静的なフラグ
    "static_hogehoge_flag": {
        "default": False,
    },
}

# ==> AppConfig

# AppConfigにアプリケーションを追加
app_config = appconfig.CfnApplication(stack, "app", name="demo-app")

# AppConfigに環境を追加
config_env = appconfig.CfnEnvironment(stack, "env", application_id=app_config.ref, name="dev")

# AppConfigにプロファイルを追加
config_profile = appconfig.CfnConfigurationProfile(
    stack,
    "profile",
    application_id=app_config.ref,
    location_uri="hosted",
    name="features",
)

# プロファイルに新しいバージョンを追加
hosted_cfg_version = appconfig.CfnHostedConfigurationVersion(
    stack,
    "version",
    application_id=app_config.ref,
    configuration_profile_id=config_profile.ref,
    content=json.dumps(features_config),
    content_type="application/json",
)

# デプロイ戦略を作成
app_config_deployment = appconfig.CfnDeployment(
    stack,
    id="deploy",
    application_id=app_config.ref,
    configuration_profile_id=config_profile.ref,
    configuration_version=hosted_cfg_version.ref,
    environment_id=config_env.ref,
    deployment_strategy_id="AppConfig.AllAtOnce",  # 即時一括反映
)

# ==> lambda

powertools_layer = _lambda.LayerVersion.from_layer_version_arn(
    stack,
    "lambda-powertools-layer",
    f"arn:aws:lambda:{os.getenv('CDK_DEFAULT_REGION')}:017000801446:layer:AWSLambdaPowertoolsPythonV2-Arm64:16",
)
function = _lambda.Function(
    stack,
    "function",
    runtime=_lambda.Runtime.PYTHON_3_9,
    architecture=_lambda.Architecture.ARM_64,
    code=_lambda.Code.from_asset("runtime"),
    handler="index.handler",
    layers=[powertools_layer],
    environment={
        "APPLICATION_NAME": app_config.name,
        "ENV_NAME": config_env.name,
        "PROFILE_NAME": config_profile.name,
    },
)
function.add_to_role_policy(
    iam.PolicyStatement(
        effect=iam.Effect.ALLOW,
        actions=["appconfig:GetLatestConfiguration", "appconfig:StartConfigurationSession"],
        resources=["*"],
    )
)

app.synth()

次に Lambda ですが、これは「百聞は一見にしかず」だと思うので動作するコードを書いておきます。

# runtime/index.py
# ----------------

import os

from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags

feature_flags = FeatureFlags(
    store=AppConfigStore(
        application=os.getenv("APPLICATION_NAME"),
        environment=os.getenv("ENV_NAME"),
        name=os.getenv("PROFILE_NAME"),
    )
)


def handler(event, context):
    flg = feature_flags.evaluate(
        name="dynamic_hogehoge_flags",
        context={"user_id": event["user_id"]},
        default=False,
    )
    print(f"フラグは{str(flg)}です")
    return

最後にapp.pyruntime/index.pyをディレクトリにいれて、下記のコマンドでデプロイします。

$ pip install aws-cdk-lib
$ cdk -a "python3 app.py" deploy

デプロイに結構時間がかかるのでコーヒーでも入れて待ちましょう。

動作確認

Lambda のコンソールから下記な event を Lambda に流します。

{
  "user_id": "hoge"
}

結果、下記が print されました。

フラグはTrueです

次に下記な event を流します。

{
  "user_id": "fuga"
}

結果、下記が print されました。

フラグはFalseです

動的なフラグとして正常に動いていそうですね!

後片付け

使わなければお金はかからないですが、気持ち悪いので下記のコマンドで削除します。

$ cdk -a "python3 app.py" destroy

さいごに

Lambda Powertools の Feature Flags を実際に利用してみて、設定方法や使い方をまとめてみました。

これを利用することで、フィーチャーフラグの管理も CDK で行うことができて良さそうです。またフォーマットも癖がなく使いやすいように思いました。 今回はサンプルのために Lambda と AppConfig を同じ Stack へ入れてしまいましたが、実際に利用する場合は Feature Flags 用の独立した Stack を作成し、どこにも影響を与えずにフラグを定義を更新できる構成にすると良いと思います。 競合として AppConfig のフィーチャーフラッグが存在しますが、CDK で管理するという文脈だと、今現在の CDK は AppConfig に対して L1 コンストラクトしか存在しないため、Lambda Powertools の Feature Flags ほうが 1 つの JSON に定義がまとまっているので人に優しく管理できそうです。

以上です。ありがとうございました! この記事がなにかのお役に立てれば幸いです。