弁護士ドットコム株式会社 Creators’ blog

弁護士ドットコムがエンジニア・デザイナーのサービス開発事例やデザイン活動を発信する公式ブログです。

Azure App Configuration を使って機能の利用可否を自動で切り替える

こんにちは。 弁護士ドットコム株式会社 Professional Tech Lab の伊藤 (@michimani) です。

Professional Tech Lab は今年の 2 月に新しく作られたチームで、主に AI(特に自然言語処理)を用いたプロダクトの開発や研究をしています。私はその中で、新しいプロダクトの開発や既存プロダクトへの AI を活用した機能の実装を担当しています。

この記事では、 Azure App Configuration のリソース作成方法とアプリケーションからリソースへアクセスする方法について説明します。 また実際にアプリケーションが提供している機能の利用可否を再起動・再デプロイなしで切り替えるアーキテクチャの例についても紹介します。

はじめに

この記事は 弁護士ドットコム Advent Calendar 2023 の 18 日目の記事です。

昨日は こがさん @poemnによる プロジェクトマネージャーになって半年、やったこと、そして意識したこと。 でした。

App Configuration とは

App Configuration は Azure が提供しているサービスのひとつで、アプリケーションの設定情報を管理するためのサービスです。設定情報は key-value 形式で管理されており、アプリケーションは REST API を通じて設定情報にアクセスできます。

アプリケーションの設定情報は環境変数として保持できますが、その場合は設定情報を変更するたびにアプリケーションを再起動・再デプロイする必要があります。App Configuration を使うことで設定情報をアプリケーションから切り離し、再起動・再デプロイすることなくアプリケーションの挙動を変更できます。

App Configuration リソースの作成方法

App Configuration には、任意の値を key-value 形式で保存できる Key and value と、 その中でも特に真偽値を保存できる Feature という概念があります。今回は Feature を利用します。

Azure Portal や Azure CLI による App Configuration のリソース作成方法については公式ドキュメント 1 に記載されています。 なので、ここでは Terraform 2 を使ってリソースを定義してみます。

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "=3.84.0"
    }
  }

  required_version = "= 1.6.5"
}


provider "azurerm" {
  features {}
}
resource "azurerm_resource_group" "main" {
  name     = "sample-rg"
  location = "japaneast"
}

resource "azurerm_app_configuration" "main" {
  name                = "sample-app-configuration"
  resource_group_name = azurerm_resource_group.main.name
  location            = "japaneast"
  sku                 = "standard"
}

data "azurerm_client_config" "current" {}

resource "azurerm_role_assignment" "main_dataowner" {
  scope                = azurerm_app_configuration.main.id
  role_definition_name = "App Configuration Data Owner"
  principal_id         = data.azurerm_client_config.current.object_id
}

resource "azurerm_app_configuration_feature" "feature" {
  configuration_store_id = azurerm_app_configuration.main.id
  name                   = "sushi-service"
  enabled                = true

  lifecycle {
    ignore_changes = [
      enabled,
    ]
  }

  depends_on = [
    azurerm_role_assignment.main_dataowner,
  ]
}

output "appconfig_endpoint" {
  value       = azurerm_app_configuration.main.endpoint
  description = "endpoint of app configuration"
}

azurerm_app_configuration_feature.enabled の値は Terraform の管理外で変更するため ignore_changes に設定しています。

Python のコードから App Configuration リソースへアクセスする方法

App Configuration へのアクセスは REST API を通じて行うことができますが 3 、 今回は Python 用の SDK を使ってアクセスします。必要なモジュールは下記のとおりです。

これらのモジュールを利用して、 Feature リソースを取得・更新するコードを書いてみます。

App Configuration クライアントの作成

Feature リソースを取得・更新するためには、まず App Configuration クライアントを作成する必要があります。 App Configuration のクライアント作成時には App Configuration のエンドポイントが必要です。

エンドポイントの値は terraform output で確認するか、下記の Azure CLI コマンドで確認できます。

az appconfig show \
--resource-group 'sample-rg' \
--name 'sample-app-configuration' \
--query 'endpoint' \
--output tsv

今回はエンドポイントの値を環境変数 APP_CONFIGURATION_ENDPOINT に設定するものとして、下記のようにクライアントを作成します。

from os import environ

from azure.identity import DefaultAzureCredential

def init_app_configuration_client() -> AzureAppConfigurationClient | None:
    endpoint = environ.get("APP_CONFIGURATION_ENDPOINT", "")
    if len(endpoint) == 0:
        print("APP_CONFIGURATION_ENDPOINT is not set")
        return None

    credential = DefaultAzureCredential()

    return AzureAppConfigurationClient(base_url=endpoint, credential=credential)

DefaultAzureCredential() では、実行環境の認証情報を利用してクライアントを生成します。ローカルで実行する場合は Azure CLI の認証情報を利用しますが、 Azure 上の Container Apps などで実行する場合は各リソースに設定された Identity を利用します。

Feature リソースの取得

Feature リソースには .appconfig.featureflag/ という prefix が自動で付与されています。 対象の Feature にアクセスするには Feature 名に prefix を付けた値を Key として get_configuration_setting メソッドで取得します。

from json import loads as json_loads
from traceback import format_exc

from azure.appconfiguration import AzureAppConfigurationClient
from azure.core import exceptions

def is_enabled_feature(client: AzureAppConfigurationClient, feature_name: str) -> bool:
    key = ".appconfig.featureflag/{}".format(feature_name)

    try:
        feature_flag = client.get_configuration_setting(key=key)

        if feature_flag is None:
            return False

        return json_loads(feature_flag.value)["enabled"]

    except exceptions.ResourceNotFoundError:
        print(f"key: {key} is not found")
        return False

    except Exception:
        print(format_exc())
        return False

Feature リソースは ConfigurationSetting として返されるので、 value プロパティを Python Object に変換して enabled の値を取り出しています。4

Feature リソースの更新

Feature リソースの更新は set_configuration_setting で行います。 このメソッドは引数に ConfigurationSetting 型のオブジェクトを受け取り、更新後の ConfigurationSetting 型のオブジェクトを返します。 下記の実装では ConfigurationSetting を継承している FeatureFlagConfigurationSetting 型のオブジェクトを渡しています。

from json import loads as json_loads
from traceback import format_exc

from azure.appconfiguration import AzureAppConfigurationClient, FeatureFlagConfigurationSetting
from azure.core import exceptions

def update_enabled(
    client: AzureAppConfigurationClient, feature_name: str, enabled: bool
) -> bool | None:
    key = ".appconfig.featureflag/{}".format(feature_name)

    try:
        feature_setting = FeatureFlagConfigurationSetting(
            feature_id=feature_name,
            enabled=enabled,
        )

        new_config_setting = client.set_configuration_setting(feature_setting)

        if new_config_setting is None:
            return None

        return json_loads(new_config_setting.value)["enabled"]

    except exceptions.ResourceNotFoundError:
        print(f"key: {key} is not found")
        return None

    except Exception:
        print(format_exc())
        return None

挙動を確認してみる

Feature の取得・更新するコードを実際に動かしてみます。

import sys
from json import loads as json_loads
from os import environ
from traceback import format_exc
from typing import Final

from azure.appconfiguration import (
    AzureAppConfigurationClient,
    FeatureFlagConfigurationSetting,
)
from azure.core import exceptions
from azure.identity import DefaultAzureCredential


def init_app_configuration_client() -> AzureAppConfigurationClient | None:
    endpoint = environ.get("APP_CONFIGURATION_ENDPOINT", "")
    if len(endpoint) == 0:
        print("APP_CONFIGURATION_ENDPOINT is not set")
        return None

    credential = DefaultAzureCredential()

    return AzureAppConfigurationClient(base_url=endpoint, credential=credential)


def update_enabled(
    client: AzureAppConfigurationClient, feature_name: str, enabled: bool
) -> bool | None:
    key = ".appconfig.featureflag/{}".format(feature_name)

    try:
        feature_setting = FeatureFlagConfigurationSetting(
            feature_id=feature_name,
            enabled=enabled,
        )

        new_config_setting = client.set_configuration_setting(feature_setting)

        if new_config_setting is None:
            return None

        return json_loads(new_config_setting.value)["enabled"]

    except exceptions.ResourceNotFoundError:
        print(f"key: {key} is not found")
        return None

    except Exception:
        print(format_exc())
        return None


def is_enabled_feature(client: AzureAppConfigurationClient, feature_name: str) -> bool:
    key = ".appconfig.featureflag/{}".format(feature_name)

    try:
        feature_flag = client.get_configuration_setting(key=key)

        if feature_flag is None:
            return False

        return json_loads(feature_flag.value)["enabled"]

    except exceptions.ResourceNotFoundError:
        print(f"key: {key} is not found")
        return False

    except Exception:
        print(format_exc())
        return False


FEATURE_NAME: Final[str] = "sushi-service"

if __name__ == "__main__":
    client = init_app_configuration_client()
    if client is None:
        sys.exit(1)

    enabled = is_enabled_feature(client, FEATURE_NAME)
    if enabled:
        print("sushi service is available")
    else:
        print("sushi service is NOT available")

    new_enabled = update_enabled(client, FEATURE_NAME, not enabled)
    print(f"Updated feature flag: {new_enabled}")

このコードを実行すると、実行するたびに Feature の値が切り替わることを確認できます。

$ python main.py
sushi service is available
Updated feature flag: False

$ python main.py
sushi service is NOT available
Updated feature flag: True

実際のアプリケーションではどのような使い方が想定できるか

App Configuration の基本的な使い方がわかったところで、実際のアプリケーションでどのような使い方が想定できるかを考えてみます。例えば下記のようなアーキテクチャが考えられます。

アーキテクチャ図
App Configuration を利用して機能の利用可否を自動で切り替えるアーキテクチャの例

まず、 Azure OpenAI Service にリクエストを送信するアプリケーションがあります。このアプリケーションは、 Azure App Configuration の Feature を参照して、 Azure OpenAI Service にリクエストを送信するかどうかを判断します。

Service Alerts では Azure OpenAI Service の特定のメトリクスを監視し、アラート状態が切り替わった際に Function Apps の関数を呼び出します。監視するメトリクスとしては、 GPT-4 によって処理されたトークン数などが考えられます。 この関数では、トリガーされたときの Service Alerts のアラート状態に応じて App Configuration の Feature の値を更新します。

Azure Cache for Redis を置いているのは、アプリケーションで取得した Feature の値をキャッシュするためです。 App Configuration には一定期間あたりのリクエスト数に制限があり、Standard SKU では 1 時間あたり 30,000 リクエストが上限となっています。5

この制限を超えてリクエストすると、 App Configuration は 429 Too Many Requests を返します。 このような状況を避けるためにアプリケーションは優先的に Redis から値を取得します。そして、キャッシュが存在しない場合のみ App Configuration にリクエストし、取得した値を Redis に保存します。

ただし、このキャッシュ時間が長いと App Configuration を利用するメリットである機能の利用可否をリアルタイムに切り替えることができなくなってしまうので設定する時間には注意が必要です。

まとめ

以上、 Azure App Configuration を使ってアプリケーションの再起動・再デプロイなし機能の利用可否を切り替える方法を紹介しました。

このような仕組みは フィーチャーフラグリリースフラグ などと呼ばれており、デプロイとリリースの分離や迅速なリリース・ロールバックの実現に効果的です。

弊社ではクラウドサインの開発において AWS の CloudWatch Evidently を利用していますが、App Configuration は広義での同類サービスと言えます。 クラウドサインでの CloudWatch Evidently 利用については SRE NEXT 2023 で登壇された SRE チーム上田さんの発表内で触れられているので、ぜひそちらもご覧ください。

明日は @dskymd によるブログです。お楽しみに。


  1. クイックスタート: Azure App Configuration ストアを作成する | Microsoft Learn
  2. hashicorp/azurerm | Terraform Registry
  3. Azure App Configuration REST API | Microsoft Learn
  4. feature_flag.enabled としても値を取得できますが、静的解析ツールの型チェックが通らないため value を変換してから取得しています。
  5. 価格 - App Configuration | Microsoft Azure