Fastlyを活用したカナリアリリースを実現したい
CDNを活用したFeature Toggleを実装したいのでその設計をメモしながら考えたい
Fastly + Feature ToggleといえばNIKKEIだな
まず機能要件を整理したい。Feature Toggleを使って実現したいことは
- リクエストしてきたユーザーのうちn%に対して特定のflagがtrue/falseになる
- 実装の分岐でそれを得るためにclient javascriptからそのflagをreadできる(SPAで遷移を考慮するためであり、OptimizeなどのA/Bテスト基盤と組み合わせるため)
- flagが今true/falseどちらになってるのかを管理画面で確認したい
- そのflagの参照が最後いつなのかを知りたい
- リリースのタイミングではなく(ビルドプロセスで解決するものではなく)、ランタイムで解決される
この分岐の量が凄まじい数になるのはそもそも管理しきれないため、想定常時10個ほどの機能があるとする(新しいページでfeature toggleを使いたい場合、古い実装を削除する)
CDNを無視して考えると flags.example.com のようなサーバーを用意し、flag自体の管理をDBで行う。
管理画面ではflagの一覧とかをそのflagサーバーから持ってきて、状態を可視化できるようにする。また特定のflagを足したり消したり、変更したりした時に各flagに対してCRUDが存在するだけなのですごく簡単なjsonっぽいサーバーがかければ問題なさそう。
ここまでで以下の要件を達成できた
- flagの状態を可視化したい
アプリケーションとしては特定のkeyがtrue/falseかどうかが分かればいいので、実装としては
async function getFeatureToggle(key) {
const { data } = await axios.get(`http://flags.example.com/${key}`);
return data.isEnabled
}
のような実装でflagの最新の状態を得ることができる
または例えばlanding時に完全に解決しきって良いのであれば(ユーザーが滞在してる間にtoggleが切り替わる可能性を考慮しない場合)、先ほど書いたように全体の数は〜10個を想定するなら、 example.com/flags
のようなエンドポイントを作ってしまってjsonで一気にkvの形で返してしまえばlanding時に1度だけ叩かれて
{ flag1: true }
みたいなresponseを返せば良いと思う
時間指定や、起動した瞬間すぐみたいな要件がない限りこれをサーバーとして作らなくても flags.example.com でS3のバケットとかを当てておいて、jsonファイルをアップロードすればいい気がする
↑は間違いで、そんなわけなくてA/Bテストをするからユーザーの情報によってture/falseを決めれないといけない。し、A/Bテストは一定の期間内は一度決まれば同じ結果を返さないといけない。
そう考えると flags.example.com はbodyで返すのではなくset-cookieをして返すのが良さそう。
例えば新しいユーザーが来訪した時に、example.comにアクセスしてきて、そのページではABテストが行われていたとすると、50%の確率でtrue、50%の確率でfalseになるような設定をしないといけない。
なのでリクエストされてきたらまずそのfeatureに対してcookieを振ってるかを確認して、振ってなければ新しくどちらかをset-cookieする。2度目の来訪ではcookieが存在するので新しくset-cookieをすることなく前の値のままABの結果を得る
bodyとして返せばcookieにclient jsで触れられなくても構わない(http-onlyにすることが可能)
n%に振るというnの情報は管理画面から入稿すればいいので、そのデータをもとに flags.example.com はcookieをみてあれば、それを使うしなければ新しく値を作りset-cookieした上でresponse bodyに突っ込んで返せば良い。
これでほぼ条件を満たしていそう。feature毎に最後にリクエストされたのかを知りたい場合は1発でjsonを返すやり方ではなく各エンドポイントを使って、アクセスログを辿ればいいしそのやり方はパフォーマンスに影響が出そうで、パフォーマンスを優先したい場合は責任者に問う、またはOptimizeなどデータとして保持させるようなサービスから消す際にflags.example.comからも消して、example.comの実装の中身からも消せば良い
こう考えると
- n%でture/falseを返す関数が実装できる
- set-cookieができる
- bodyを作れる
- 管理画面から入稿した情報を読める
が出来れば何でも良い?
これを完全にFastlyだけで実現しようとすると今の自分の思考の中では、最後の管理画面で管理した情報を元に作るみたいなことが行えないので、やるとすればvclを何かから自動生成しtable的なのを実装する。
そのtableの情報を元に計算をして値を返す。
みたいなことをすれば可能っぽい。
tableにはkey:振りたい数を入れればいいのでvclでもtableで実現できる。
table FeatureToggle {
flag1: 50,
}
table.lookup('flag1', FeatureToggle, 0)
これで特定のkeyがn%で振られて欲しいみたいな条件を作って、実際にrecvしたタイミングでreq.http.Cookie::ab でcookieから値を取ってきて正規表現でぶち抜いてflag1=がなければ50%でtrue/falseを決定して値を変数においておいて、後でひっつけてset-cookieするみたいな風にできる?
と思ったけど、普通に辛いな。まずloopが出来ないからABで取得した全keyに対してこれをやることができるのかが謎い
vclでやらない方針に倒して、全てのtoggleをサーバーで管理する(toggle server)作戦で、その中身も全部originが解決するようにする方が自分には合ってるかもしれない。
[req] -> (recv) -> (hash) -> (miss) -> Origin -> toggle server -> (fetch) -> (deliver) -> [res]
みたいな流れか👀
flags.example.com は何%で1を振りたいかとそのidの一覧を返してくれるサーバーとして考え、期待するレスポンスは
{
flag1: { id: 'flag1', target: 50 },
flag2: { id: 'flag2', target: 10 }
}
例えば example.com/bar
ページをABテストしてる場合(Next.jsを全体としたライフサイクルで解決します)
- barへのrequest
- _document.tsx上のライフサイクルで flags.example.com を見に行って現在有効なパターンのtoggle一覧を取得する(public cache)
- 取得したtoggle一覧はメモリに保存する
- barのgetServerSidePropsで flags.get('flag1') を実行する
- flag1のvalueをcookieから取得する。存在すればそれを使い、なければメモリにあるtoggle一覧からflag1を取得してtarget%で1を返す関数を通しvariantを決定する。決定したらCookieにflag1=variantの形で書き込む
- X-Feature-Toggle-Flag1: variant, Vary: X-Feature-Toggle-Flag1 の形で /bar のキャッシュが混在しないようにする
Varyの付け方は悩ましい。このVaryの付け方をしてしまうと、vcl上でCookieを分解してそのkeyの数だけheaderを追加しないといけないけど、ループが出来なくても出来るかな...
一旦最近このモチベーションないのでclose