AppConfig用のOpenFeatureプロバイダを自作した
OpenFeatureのエコシステムにAWS AppConfigのプロバイダが無かったので自作した。ポーリング+プロセス内評価、取得失敗時は前回定義で継続。切替はデプロイなし14秒、段階配信中の評価は行き来なしを実測。フラグキーに"."が使えない制約で命名規約を変えた話も。
検証リポジトリのフィーチャーフラグは OpenFeature を語彙にして、ローカルは flagd で動かしている。AWS での本命は AppConfig にすると決めていた。理由は、AppConfig の段階デプロイ——割合を刻む + ベイク時間 + CloudWatch アラームでの自動ロールバック——が「展開は 1% → 10% → 50% → 100%、切り替え判定はマネージドの閾値アラームから始める」という運用規約と そのまま噛み合うため。
問題がひとつ。go-sdk-contrib/providers に AppConfig のプロバイダが存在しない
(AWS 系は Parameter Store 用の aws-ssm だけ)。なので自作した。約 300 行 + テスト。
形: ポーリングしてプロセス内で評価する
AppConfig のデータプレーン(appconfigdata)は
StartConfigurationSession → GetLatestConfiguration の繰り返しで使う。
プロバイダはこれをポーリングして定義をプロセス内に保持し、評価はローカルで行う。
評価のたびに AWS を呼ばない。flagd の in-process リゾルバと同じ形で、
プロバイダを差し替えても評価の性質(レイテンシ、可用性)が変わらない。
失敗時の挙動は3段で設計した:
- 取得に失敗しても前回の定義で評価を続ける(stale)。次の周期で再試行し、 セッションが失効していたら張り直す
- 一度も取得できていない間は PROVIDER_NOT_READY を返し、呼び出し側が 宣言した既定値へ倒す
- 起動時に基盤へ届かない場合はプロバイダの初期化がエラーを返し、アプリは 警告ログを出して既定値で走り出す(フラグ基盤の停止でアプリを止めない)
stale のテストは、実装をわざと壊して(取得失敗時に定義を消す変異を入れて) テストが失敗することまで確認した。検査は破ってみせて初めて信用できる。
細かい仕様をひとつ: GetLatestConfiguration は変更が無いとき空の応答を返す。
空を「定義が消えた」と解釈すると、変更が無いだけでフラグが全部未定義になる。
空は「前回のまま」であり、トークンだけ進めて保持した定義を触らない。
フラグ定義は git、反映は専用パイプライン
フラグ定義は「リポジトリ内のファイルを git 管理し、変更を PR レビューに載せる」 と決めている(誰がいつ何を変えたかが履歴に残る)。AppConfig では定義が AWS 側に あるので、git の定義を AppConfig へ反映する専用パイプラインを書いた。 ローカル flagd 用の定義ファイルを単一ソースとして、AppConfig の feature flags 形式へ変換 → 構成バージョン登録 → デプロイ開始 → 完了(ベイク含む)まで待つ。
このパイプラインはアプリのデプロイと独立している。デプロイ(コード)と リリース(フラグ)を2つの独立した手段として扱うためで、フラグの切替・巻き戻しに アプリのデプロイもロールバックも要らない。
注意が必要な仕様が2つ
どちらもドキュメントを先に精読していれば避けられた類のもので、実装中に実物から 返ってきた。同じものを作る人は先に知っておくとよい。
フラグキーは ^[a-z][a-zA-Z0-9_-]{0,63}$ — ”.” が使えない。
最初の反映が BadRequestException で弾かれた。それまでの命名規約 <種類>.<機能名>
(例: ops.photo_disable_uploads)の ”.” がキー制約に合わない。対処は2案あって、
パイプラインで ”.” を ”_” に変換して格納しプロバイダが同じ規則で逆引きする案
(実装して動くところまで確認した)と、命名規約側を <種類>_<機能名> に変える案。
規約側を変えた。変換の対をパイプラインとプロバイダの両端で保守し続けるより、
マネージドの基盤で使えない文字を規約から外すほうが安い。パイプラインには
キー制約の機械検査を残したので、規約から外れた名前は反映の前に止まる。
deletion protection が削除を拒む。 検証後の terraform destroy が environment と
configuration profile の削除で actively being used と拒否された。AppConfig は
直近(既定1時間)にポーリングしたクライアントがいる資源の削除を拒む仕様で、
Terraform provider にバイパスの口が無い。CLI の --deletion-protection-check BYPASS
で消せる。「検証してすぐ消す」使い捨て運用では毎回ここに当たる。
実測
手元で動かしたアプリ(プロバイダ以外は無変更)に対して:
| 確認 | 結果 |
|---|---|
| flagd → AppConfig の差し替え | usecase / handler の変更 0 行(合成ルートのプロバイダ交換のみ) |
| フラグ切替(即時戦略) | パイプライン実行から 14 秒で挙動が反転。アプリの再起動なし |
| 段階デプロイ(50%×2分 + ベイク1分) | 1秒間隔 193 サンプル中遷移1回のみ。ON/OFF の行き来なし |
| 取得失敗 | 存在しないプロファイルで起動 → 警告ログ + 宣言した既定値で稼働 |
14 秒の内訳は、即時戦略のデプロイが約 3 秒 + プロバイダのポーリング間隔 (最小 15 秒、AppConfig の下限)以内。キルスイッチの反映速度はポーリング間隔で 決まるので、これを縮めたければ間隔を 15 秒に張り付ける。
段階デプロイの意味論は flagd と違う点に注意がいる。flagd の fractional targeting は 主体(ユーザー)のハッシュで振り分けるが、AppConfig の割合は クライアント(= ポーリングしているプロセス)単位の配布割合で、同一クライアントは 旧定義か新定義のどちらかを一貫して見る。主体単位の安定はこの構造から従う一方、 「特定ユーザー群にだけ出す」型のターゲティングは AppConfig の段階デプロイでは 表現できない(それは属性とアプリ側の条件で組む)。
まとめ
- AppConfig の OpenFeature プロバイダは無かったので書いた。ポーリング + プロセス内評価、stale 継続、未取得時は宣言既定値。約 300 行で足りる
- フラグ定義は git、反映は専用パイプライン。コードとフラグは独立した2つの手段
- 制約は測ると出てくる: フラグキーの文字集合(命名規約を変更)、 ポーリング間隔の下限 15 秒、deletion protection。どれもドキュメントより先に 実物が教えてくれた