こんにちは、カミナシでソフトウェアエンジニアをしている 佐藤 と申します。
弊社で開発・提供しているノンデスクワーカー向けプラットフォーム「カミナシ」(以降「カミナシレポート」や「弊社アプリケーション」と呼びます)において、Feature Flags の仕組みを整備し、デプロイとロールアウトの分離を加速させたことについてご紹介したいと思います。
登場する技術
- Amazon Elastic Container Service (ECS)
- AWS AppConfig
- AWS AppConfig agent
前提知識
後半の「技術的な話」以降の部分は、以下の技術についても触れています。
- Feature Flags、Feature Toggles
- AWS AppConfig
- Amazon Elastic Container Service (ECS)
- Terraform
「背景」や「解決策」といった概要部分は、AWS に関する予備知識がなくとも、理解できる記載となるように努めました。
なお、記事内におけるサンプルソースコードは、全て TypeScript で記述しています。カミナシでは現在、バックエンドでは Go、フロントエンドは TypeScript を主に使用しています。
本記事における「Feature Flags」
Feature Flags と一口に言っても様々な種類がありますが、本記事の以降の記述は、以下のイメージを持って読み進めていただければと思います。
- システムを利用するユーザーの属性に応じて、機能のON・OFF(公開・非公開)を切り替える仕組み
- ON・OFF の切り替えは、(できるだけ)ソースコードの修正やデプロイを伴わずにできる
Feature Flags そのものについて詳しい議論を知りたい方は、以下のサイトなどをご参考ください。
カミナシレポートのアーキテクチャ
以降の説明をするにあたり、大まかなカミナシレポートのアーキテクチャを示します。
- ブラウザからアクセスする「Web」アプリケーション(SPA)
- 「Mobile」アプリケーション
- フロントエンドからアクセスされる「API Server」
- いくつかの「Batch Job」
といった、シンプルな構成です。
背景・課題
今回、仕組みを整備する前から、簡易的な Feature Flags の仕組みは実装・利用していました。それは以下のように、機能公開・非公開の条件に関するデータを、ソースコードにハードコーディングする方法でした(*1)。
// 「機能公開・非公開の条件に関するデータ」 const someAttributeValues = [ 123, 124, 125 ] const isFeatureXEnabled = (someAttributeValue: number) => { return someAttributeValues.includes(someAttributeValue) } const doSomething = () => { const userInfo: { someAttribute: number } = ... if (isFeatureXEnabled(userInfo.someAttribute)) { // doSomething with FeatureX } else { // doSomething without FeatureX } }
(以降ではこの「機能公開・非公開の条件に関するデータ」のことを簡単に「Feature Flags の設定値」と呼びます。)
この方法には以下のような課題がありました。
- 一部・全部問わず、ユーザーへの機能の公開・非公開の切り替えのためには、ソースコードの修正 & デプロイが必要である。
- タイムリーな機能の解放ができない(*2)。
- 当該デプロイに起因する障害があり、切り戻しが発生した場合、機能の公開・非公開状態も切り替わってしまう。
- 一元管理が難しい。機能の公開・非公開の処理が、リポジトリ毎に実装されることになる。
- どの機能が Feature Flags で制御されているのか把握しづらい。
- 同一の機能の公開・非公開が複数のリポジトリにまたがって制御されている場合、修正漏れのリスクがある。
また、自分自身やチームのメンバーの肌感覚としても、このような Feature Flags の利用にはストレスを感じていました。
*1 図では「Batch Job」には、「Hard Coding!」を付与していません。直近では「Batch Job」に 「Hard Coding」による Feature Flags の実装の予定・実績がなかったためです。なお「Batch Job」の1つである「Excel変換」という機能では、リアーキテクティング時に AWS AppConfig を用いた段階的なロールアウトを行っています(参考:https://speakerdeck.com/kaminashi/a-story-that-improved-a-lot-when-re-architecting-a-function-that-was-about-to-become-technical-debt?slide=44)。
*2 トランクブランチへのマージ=デプロイのような構成であればこの問題は特に気にするものではないと考えられますが、カミナシレポートの現在のデプロイ頻度は、週2回程度であり、この頻度に合わせると、タイムリーに機能解放できない、という事情があります。(もちろん、デプロイ頻度をあげる取り組みも目下推進中です)
解決策
以下のような仕組みを整備することにより、上記課題を解決しました。
- 「Feature Flags の設定値」は AWS AppConfig に外部化する。
- AWS AppConfig とは、アプリケーションの設定情報を管理するマネージドサービスで、AWS AppConfig の API を呼び出すことでその設定情報が得られる。
- AWS AppConfig へのアクセスは、バックエンドの API Server が一元的に行う。
- フロントエンドは、API Server の専用エンドポイントにアクセスし、機能の公開・非公開の情報を得る。
- 下図で、「GET /enabledFeatures」としている部分。
- 前提として、ユーザーの属性に応じて、機能の公開・非公開が決まるため、同エンドポイントは、認証済みのユーザーのみがアクセスできるものとする。
もう少し使用している AWSのサービスを意識すると以下のような構成になります。
「Feature Flags の設定値」が、AWS AppConfig に外部化されることで、ソースコードの修正・デプロイをせずに機能の公開・非公開の切り替えが可能となりました。
また「Feature Flags の設定値」は、AWS AppConfig で一元管理することで、切り替え時の修正漏れのリスクもほぼなくなりました。
AWS AppConfig agent などの技術的な詳細は後述します。
Front End が API Server を経由して AWS AppConfig にアクセスしている理由
AWS AppConfig の設定情報を参照するためには、AWS AppConfig の API を呼び出すための AWS IAM クレデンシャルが必要です。Front End に AWS IAM クレデンシャルを受け渡し、Front End から直接 AWS AppConfig の API を呼び出すことは技術的には可能です。
しかし、今回は、AWS AppConfig へのアクセスは API Server に一元化し、Front End は API Server に作成したエンドポイント(「GET /enabledFeatures」)を経由して、「Feature Flags の設定値」を参照する構成としています。
この方法の方が、現時点では、実装・運用のコストが低く、妥当な方法と考えられたためです。
「Feature Flags の設定値」の更新はどうやるの?
技術的な詳細は、後述しますが、「Feature Flags の設定値」の中身を、専用のリポジトリを作成し、管理することとしました(ここでは、大まかに設定ファイルを専用のリポジトリで管理しているようなイメージを持っていただけると良いかと思います)。
また、同リポジトリで、トランクブランチへのマージ → apply(*1) の実行により、AWS AppConfig に変更内容が反映されるように構成することで、設定更新時のミスも発生しづらくなっています。
* 1 Terraform において、リソースの変更を反映させるためのコマンドです。
技術的な話
「カミナシレポート」のバックエンドの API Server コンテナは Amazon Elastic Container Service (ECS) を用いて、AWS Fargate 上で稼働させています。
AWS AppConfig agent は便利!
前述した図において AWS AppConfig agent というコンテナがありますが、これについて解説します。
こちらは、ECS や EKS においてアプリケーションと併用できる、AWS が提供する sidecar コンテナイメージです。この sidecar を利用することで、AWS AppConfig へのアクセスを簡単に行うことができます。
AWS SDK などを用いて、AWS AppConfig にアクセスするコードを書いたことがある人であれば、ピンと来る方もいるかと思いますが、AWS AppConfig への API Call および、取得されたデータを適切に管理するのは、若干煩雑です。
AWS SDK を使って、自前で実装すると、以下のような感じになります。
let setTimeoutId = undefined const startAppConfigSessionCommand = new StartConfigurationSessionCommand({ ApplicationIdentifier: APP_CONFIG_APPLICATION, ConfigurationProfileIdentifier: APP_CONFIG_PROFILE, EnvironmentIdentifier: APP_CONFIG_ENVIRONMENT, }) const startSessionResponse = await appConfigClient.send(startAppConfigSessionCommand) const pollConfiguration = async (token: string | undefined): Promise<void> => { const appConfigCommand = new GetLatestConfigurationCommand({ ConfigurationToken: token, }) const appConfigResponse = await appConfigClient.send(appConfigCommand) if (!appConfigResponse.NextPollIntervalInSeconds || !appConfigResponse.NextPollConfigurationToken) { throw Error('Invalid response from AppConfig') } if (appConfigResponse.Configuration) { let content = '' for (let i = 0; i < appConfigResponse.Configuration.length; i++) { content += String.fromCharCode(appConfigResponse.Configuration[i]) } if (content) { setConfigDataToLocalCache(content) } } setTimeoutId = setTimeout(() => { if (setTimeoutId) { clearTimeout(setTimeoutId) } pollConfiguration(appConfigResponse.NextPollConfigurationToken) }, appConfigResponse.NextPollIntervalInSeconds * 1000) } await pollConfiguration(startSessionResponse.InitialConfigurationToken)
ざっと以下のような処理を書く必要があります。
- Application、Configuration Profile、Environment を指定し、セッションを開始する。
- セッションを開始したら、指定されたトークン、インターバルで、ポーリングを行う。
- AWS AppConfig から返された Configuration データが存在する(更新があった)場合、ローカルキャッシュなどを更新する
- ローカルキャッシュも適切に管理する。
実装することが困難というほどではないですが、用意された仕組みがあるならそれを使いたい、という感覚を覚えます。
上記のような処理を、一手に引き受けてくれるのが、AWS AppConfig agent です。
- タスク定義に AWS AppConfig agent コンテナを追加する。
- アプリケーション内では、以下のようにシンプルな HTTP GET リクエストにより、Configuration データを取得する。
- "http://localhost:2772/applications/application_name/environments/environment_name/configurations/configuration_name"
という形で簡易に、AWS AppConfig が扱えるようになります。
公式ドキュメント:
AWS AppConfig agent のデメリット
このような具合に便利な AWS AppConfig agent ですが、挙げるとすれば以下のようなデメリットもあります。今回の弊社の使い方においては、あまり大きなものとならないと考えました。
- コンテナを追加する必要があるため、vCPU・Memory を新たに割り当てる必要がある。コスト面でデメリットとなる可能性あり。
- 本記事執筆時点で、OSS ではない模様。詳細な動作を知りたい場合やトラブルシューティングの際につまづく可能性あり。
- アプリケーションとは別のコンテナを起動する必要がある。AWS AppConfig agent のコンテナが障害により停止してしまった場合など、考慮しておくべき点が増えてしまう。
最後の点についてですが、ご参考までに弊社では、障害時の考慮として以下の対策を実施しました。
- ECS タスク定義において、AWS AppConfig agent の essential を「false」にすることで、AWS AppConfig agent が障害などで停止した場合でも、アプリケーション全体が停止してしまうことを回避する。
- CloudWatch Logs で「ERROR」レベルのログを監視する(Metric Filter、CloudWatch Alarm で Slack 通知など)。
なお、コスト面に関しては、後ほどもう少し詳しく述べます。
AWS AppConfig の設定値について
Configuration Profile のタイプ
Configuration Profile のタイプは、
- Feature Flag
- Freeform Configuration
の2つから選択できます。
今回は「Feature Flag」を選択しました。「FreeForm Configuration」に比べ、自由度は低いですが、今回、やろうと思っていたことは十分に実現できるものでした。
その上で、
- 機能毎の設定様式が統一され、設定内容が理解しやすくなる。
- フォーマット(JSON スキーマ)が明確に定まっており、意図しない設定をしづらくなる。
などのメリットがあると考え、採用しました。
Configuration Profile のタイプについて詳細:
Deployment Strategy
以下、2つの設定値について補足します。
- Deployment time: AWS AppConfig のデータがターゲット(コンテナ、サーバーetc)にデプロイされるのにかかる合計の時間。
- Bake time: ターゲットへのデプロイが完了した後、ロールバックを受け付ける時間(逆に、Bake time が経過するまでは、新たな deployment を開始できない)。
今回は、
- Deployment time = 0
- Bake time = 0
で設定しました。
理由としては、現状では以下のような運用としたいためです。
- 機能解放時は全ての取得元(API Server Container など)に一度に一気に反映させる(Deployment time = 0 の理由)。
- 設定を切り戻したい時は、AWS AppConfig の仕組みで Rollback するのではなく、後述する専用のリポジトリで変更履歴まで含めて一元管理したい(Bake time = 0 の理由)。
- 逆に、AWS AppConfig の仕組みで Rollback すると、設定値を管理しているリポジトリと、AWS AppConfig の状態が乖離してしまう。
Deployment Strategy について詳細:
AWS AppConfig の設定値の管理方法
「Feature Flags の設定値」に関わる AWS AppConfig のリソースは、専用のリポジトリで管理することにしました。
具体的には、以下のリソースを Terraform で管理しています。
- aws_appconfig_application
- aws_appconfig_configuration_profile
- aws_appconfig_hosted_configuration_version(→これの中身が「Feature Flags の設定値」)
- aws_appconfig_environment
- aws_appconfig_deployment_strategy
- aws_appconfig_deployment
同リポジトリの CI / CD 周りは、Terraform Cloud で管理しており、トランクブランチへのマージをトリガーに Plan が開始されるようになっています。
リソースとして aws_appconfig_deployment まで作成しているため、Apply されると同時に「Feature Flags の設定値」の変更がAWS AppConfig 上でデプロイされます。
弊社では、インフラ周りのリソースはすでに、Terraform で管理していますが、以下のような理由により、既存のインフラ周りのリポジトリとは管理を別にしました。
- 「Feature Flags の設定値」は、インフラ関係のリソースとは更新頻度や理由が大きく異なるため、分けていた方が管理上の都合が良い。
- インフラ周りのリポジトリを更新した時に、余計な Plan の差分が発生することを避けたい
- AppConfig の aws_appconfig_configuration_profile にて type = "AWS.AppConfig.FeatureFlags"の場合、aws_appconfig_hosted_configuration_version の content に変更がなくても、差分が発生する( terraform plan で差分が出る)(*1)。
*1 動作確認の結果、このような振る舞いとなっていました。この挙動が Terraform の仕様なのか、バグなのかは、現状では断定できていません。
検討した代替案
以下、今回は採用しませんでしたが、検討した代替案を紹介します。
「Feature Flags の設定値」の情報をどこに格納するか?
RDS、Dynamo、S3など何かしらの外部ストレージに「Feature Flags の設定値」を格納する
AWS AppConfig に比べ自由度が高いが、そのぶん設計・実装・管理が難しい。設計、実装、管理や認知コストなどが高くなりそうなどの理由で、当案は採用しませんでした。
Amazon CloudWatch Evidently を利用する
Cloud Watch Evidently でも ユーザー毎(entity ID 毎)に、機能のON・OFFを切り替えることができ、要件は満たせるものでした。
定義できる設定の自由度・不自由度のバランスや、AWS AppConfig agent などのエコシステムの扱いやすさなどの点で、AWS AppConfig が上回ると考え、当案は採用しませんでした。
環境変数として「Feature Flags の設定値」を格納する
構成がシンプルであることが魅力的な案です。
AWS Systems Manager Parameter Store に保存し、ECSの環境変数から参照する、タスク定義に環境変数を直接記述するなど、いくつかの方法が考えられますが、以下の理由により、当案は採用しませんでした。
- 反映には、タスクの再起動が必要なため、若干の手間がかかる(もちろん、何らかのトリガーで自動で再起動するような構成も不可能ではないが、そのような仕組みを作るとなると、当案のシンプルさという魅力がなくなる)
- 環境変数にある程度の複雑さをもった構造化データを持たせるのが大変。
Front End からどうやって AWS AppConfig の設定を取得するか?
Amazon Congito を活用し、Front End から直接 AWS AppConfig の API にアクセスする
Amazon Cognito の Developer authenticated identities authflow - Enhanced authflow を用いて、Front End が、API Server 経由で token を取得 → AWS AppConfig の API に直接アクセスする、という案です。
AWS AppConfig に限らず、様々な AWS のサービスを Front End で活用したい未来が到来したら、検討すべき案と考えました。
現状では、「解決策」として採用した案の方が、シンプルで実装・運用しやすい上に、やりたいことは十分に実現できているため、当案は採用しませんでした。
コストの話
AWS AppConfig のコスト
AWS AppConfig は、設定データのリクエストを行う毎に課金が発生します。
- AWS AppConfig に設定データをリクエストした時(API call した時) に課金される。
- さらに、リクエストの結果、データが取得される時は、追加で課金される。
つまり、API Call が多ければ多いほど、AWS AppConfig の設定データの更新(AppConfig のデプロイ)が多ければ多いほど、コストが増えます。
今回は、試算してみたところ、大した金額とはならなかったため、AWS AppConfig 自体のコストは、特にデメリットとはなりませんでした。
参考:
- https://aws.amazon.com/systems-manager/pricing/
- https://repost.aws/questions/QUevHYubJ-TESBPlUxRyujmg/i-want-to-understand-aws-appconfig-pricing-model
AWS AppConfig agent のコスト
AWS Fargate を利用している場合、vCPU、Memory に応じてコストがかかります。
今回は、簡単な動作確認などの結果、AWS AppConfig agent は、かなり省スペックでも動作すると想定されたため、すでに稼働しているコンテナに割り当てているvCPU・Memoryの一部を、AWS AppConfig agent のコンテナに割り当て直すことにしました。
これにより、可能な限り追加コストを発生させずに、導入しました。
まとめ & 今後
- 機能公開・非公開の設定値をソースコードにハードコーディングする方式では、タイムリーなリリースや設定値の一元管理をする上で、問題があったこと
- 上記の仕組みを、AWS AppConfig を利用した仕組みを整備することで解消したこと
- AWS AppConfig 周りの仕組みをどのように構築したかの技術的な話
を中心に紹介しました。
今後は、Feature Flags の適切な利用を促進するため、
- 定義できる機能数の制限
- ある機能に対する制限を定義しておける期間の制限
などを検討していきたいと思っています。
最後までお読みいただき、ありがとうございました。何かの参考になれば幸いです!
最後に宣伝です!カミナシでは強いエンジニアリングチームを一緒に作っていく仲間を募集しています。当記事を読んで少しでも興味の沸いた方はぜひ、弊社の他の記事やリクルートサイトなどをご覧ください!