Flagger と Ingress Nginx でA/Bテストをする

| 9 min read
Author: noboru-kudo noboru-kudoの画像

前回は、FlaggerとNginxのIngress Controllerを使ってカナリアリリースを試しました。

今回はA/Bテストの方を試したいと思います。
前回のカナリアリリースは、徐々にカナリアバージョンのトラフィック量を増やしながら切り替えていく形でした。
FlaggerのA/Bテストは、一部のユーザーのみにカナリアバージョンを公開し、その後問題がない場合に全てのリクエストを一気にStableバージョンの方に振り向けます。

また、前回は完全自動化でStableバージョンへリリースしましたが、今回は最終的なStable環境へのリリースは手動承認(Manual Gating)が必要なユースケースを想定します。

Contents

事前準備

#

Nginx Ingress ControllerとFlaggerをインストールし、サンプルアプリ/Ingressをセットアップしておきます。
これは前回の記事と全く同じ手順です。

既にカナリアリリース用のCanaryリソースを作成している場合は削除し、サンプルアプリのバージョンを6.0.0に戻しておきます。

kubectl delete -f canary.yaml
kubectl -n test set image deployment/sample-app \
podinfo=ghcr.io/stefanprodan/podinfo:6.0.0

A/Bテスト構成の初期化

#

サンプルアプリをFlaggerの構成に初期化します。
カナリアリリース同様に、こちらもFlaggerのCanaryリソースを作成します。
以下ファイルをcanary-abtesting.yamlとして用意しました。

apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: sample-app
namespace: test
spec:
provider: nginx
# deployment reference
targetRef:
apiVersion: apps/v1
kind: Deployment
name: sample-app
# ingress reference
ingressRef:
apiVersion: networking.k8s.io/v1
kind: Ingress
name: sample-app
service:
port: 80
targetPort: 9898
analysis:
# チェック間隔
interval: 60s
# A/Bテスト失敗とみなすチェック失敗回数
threshold: 10
# メトリクスチェック回数
iterations: 3
match:
# HTTPヘッダベース
- headers:
x-canary:
exact: "insider"
# Cookieベース
- headers:
cookie:
exact: "canary"
metrics:
- name: request-success-rate
interval: 1m
# 99%のリクエストが200系
thresholdRange:
min: 99
- name: request-duration
interval: 1m
# 99%値のレスポンスが1秒以下
thresholdRange:
max: 1000
webhooks:
- name: acceptance-test
type: pre-rollout
url: http://flagger-loadtester.test/
timeout: 30s
metadata:
type: bash
cmd: "curl -sd 'test' http://sample-app-canary/token | grep token"
- name: load-test
type: rollout
url: http://flagger-loadtester.test/
timeout: 5s
metadata:
# 接続先のLB IPは以下で取得
# kubectl get ing sample-app -n test -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
cmd: "hey -z 1m -q 5 -c 2 -host 'sample.minikube.local' -H 'Cookie: canary=always' http://10.106.126.38/"

# Stableバージョン昇格Gate
- name: promotion-gate
type: confirm-promotion
url: http://flagger-loadtester.test/gate/check
# ロールバックGate
- name: rollback-gate
type: rollback
url: http://flagger-loadtester.test/rollback/check

# Gateリセット
- name: reset-promotion-gate
type: post-rollout
url: http://flagger-loadtester.test/gate/close
- name: reset-rollback-gate
type: post-rollout
url: http://flagger-loadtester.test/rollback/close

少し長いですが、カナリアリリースのときに作成したものと、大きくは変わりません。 違いのあるspec.analysis部分のみを説明します。

反復回数(iterations)

#

A/Bテストの場合は反復回数(iterations)を指定し、その代わりにmaxWeight/stepWeightを削除します。
iterationsはメトリクスチェックの回数です。ここで指定した回数分メトリクスチェックが繰り返され、全てのチェックをパスするとカナリアバージョンはStableバージョンへ昇格します。
ここでは、60秒間隔(interval)で3回チェックを実施するようにしました。使用するメトリクスは前回同様にステータスコードとレスポンスタイムです。

ルーティング条件(match)

#

カナリアバージョンにトラフィックを流す条件を指定します。FlaggerではHTTPヘッダとCookieによるルーティングをサポートしています。
ここでは、HTTPヘッダとしてX-Canary: insiderまたはCookieにcanary=alwaysのエントリーがある場合、カナリアバージョンへルーティングするようにしています。

Information

Cookieの値(always)はFlaggerの制約ではなく、Nginx Ingress Controllerの仕様です。
指定できる設定は利用するIngress Controllerまたはサービスメッシュ製品に依存するため注意が必要です。
Nginx Ingress Controllerの仕様は、公式ドキュメントを参照しくてださい。

イベント処理(webhooks)

#

Flaggerのライフサイクルイベントに対応した処理を定義します。
前回同様に事前の疎通確認と、Flagger Testerでメトリクスを収集するようにしています。
今回は手動承認(Manual Gating)を有効にするため、これに加えて以下を追加しました。

  • promotion-gate: Stableバージョン昇格の承認チェック
  • rollback-gate: カナリアバージョンロールバックの承認チェック

ここで指定したURLが200ステータスを返すと、Stableバージョンへの昇格またはロールバックが実行されます。
ここではFlagger Testerが備えるREST APIを使用しています。 このAPIはその時点のOpen/Closeの状態を返すものです(デフォルトはClose)。

最後の2つのWebHook(reset-promotion-gate/reset-rollback-gate)では、リリース終了後(post-rollout)に各GateをClose状態に戻しています。これは次回のリリースに備えるためです。

これを反映します。

kubectl apply -f canary-abtesting.yaml

FlaggerがCanaryリソースの作成を検知し、サンプルアプリを初期化します。
初期化後の形はカナリアリリースのときと同じです。

以下再掲します。

A/Bテストを試してみる

#

それでは新しいバージョンのリリースを、FlaggerのA/Bテストで実施してみます。
前回同様に、サンプルアプリのバージョンを上げてみます。

# image.tagを6.0.0 -> 6.0.1に変更
kubectl -n test set image deployment/sample-app \
podinfo=ghcr.io/stefanprodan/podinfo:6.0.1

Flaggerが新バージョンを検知し、A/Bテストを実施します。
カナリアリリースのときと同じく、新しいバージョンをカナリアバージョンとしてデプロイします。
ただし、今回は何もしなければ商用トラフィックがカナリアバージョンに流れることはありません。
カナリアバージョンのIngress(sample-app-canary)は以下の状態となりました。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
kustomize.toolkit.fluxcd.io/reconcile: disabled
# Flaggerが設定。指定したCookieまたはHTTPヘッダのリクエストのみをカナリアバージョンにルーティング
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-by-cookie: canary
nginx.ingress.kubernetes.io/canary-by-header: x-canary
nginx.ingress.kubernetes.io/canary-by-header-value: insider
nginx.ingress.kubernetes.io/canary-weight: "0"

上記のようにカナリアバージョンへのルーティング条件を指定するアノテーションが追加されています。

実際にcurlコマンドでアクセスし、レスポンスに含まれるバージョンを確認します。

LB_IP=$(kubectl get ing sample-app -n test -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
# Cookie指定
curl -s -H 'Host: sample.minikube.local' -b 'canary=always' http://${LB_IP}/ | jq .version
# HTTPヘッダ指定
curl -s -H 'Host: sample.minikube.local' -H 'X-Canary: insider' http://${LB_IP}/ | jq .version

どちらも6.0.1が返ってきます。カナリアバージョンの方にルーティングされています。
もちろんHTTPヘッダやCookieを指定しない場合は、Stableバージョンの6.0.0が返ってくることも確認できました。
ここでは、以下のように条件を満たしたリクエストのみがカナリアバージョンに流れている状態です。

全ての反復(iterations)が終わり、Canaryリソースの状態を確認すると以下のようになりました。

NAME         STATUS             WEIGHT   LASTTRANSITIONTIME
sample-app WaitingPromotion 0 2022-05-14T07:47:34Z

STATUSWaitingPromotionとなりました。
前回のカナリアリリースの際は、最大ウェイトに到達すると自動的にStableバージョンに昇格しましたが、今回は違います。
これは、先程webhooksで指定したpromotion-gateがClose状態となっているためです。

カナリアバージョンに問題がなかったと想定して、Stableバージョンへのリリースを承認します。
これにはFlagger TesterのREST APIを呼び出します。

FLAGGER_TESTER=$(kubectl get pod -n test -l app=loadtester -o jsonpath='{.items[0].metadata.name}')
kubectl -n test exec -it ${FLAGGER_TESTER} \
-- curl -d '{"name": "sample-app","namespace":"test"}' http://localhost:8080/gate/open

すると、Canaryリソースの状態が以下のように変わりました。

NAME         STATUS      WEIGHT   LASTTRANSITIONTIME
sample-app Promoting 0 2022-05-14T07:54:04Z

STATUSPromotingに変わり、カナリアバージョンがStableバージョンへと反映されます。
この辺りの動きはカナリアリリースと同じです。Stableバージョン昇格後は、カナリアバージョンはゼロスケールされ、初期状態に戻りました。
CanaryリソースのSTATUSSucceededへと変わります。

NAME         STATUS      WEIGHT   LASTTRANSITIONTIME
sample-app Succeeded 0 2022-05-14T08:04:01Z

手動ロールバック(リリース取消)

#

今度は、カナリアバージョンのアプリに問題があったと仮定し、手動ロールバックさせてみます。
まずは、イメージタグを再度更新します。

# image.tagを6.0.1 -> 6.0.2に変更
kubectl -n test set image deployment/sample-app \
podinfo=ghcr.io/stefanprodan/podinfo:6.0.2

再びカナリアバージョンが作成され、メトリクスチェックが開始され、承認待ち状態(WaitingPromotion)になります[1]
今度は承認せずに、Flagger TesterのロールバックGateをOpen状態にするREST APIを呼び出します。

kubectl -n test exec -it ${FLAGGER_TESTER} -- \
curl -d '{"name": "sample-app","namespace":"test"}' http://localhost:8080/rollback/open

すると、FlaggerはロールバックGateがOpenになったことを検知し、カナリアバージョンのロールバックを実行します。
具体的にはカナリアバージョンをゼロスケールし、Ingress ControllerのHTTPヘッダやCookieのルーティング条件のアノテーションを削除します。

Canaryリソースを確認すると、以下のようにFailedのステータスになりました。

NAME         STATUS   WEIGHT   LASTTRANSITIONTIME
sample-app Failed 0 2022-05-14T13:40:13Z

まとめ

#

FlaggerでA/Bテストを利用してアプリのリリースをしてみました。
また、今回はよくあるパターンとして、自動リリースではなく、手動承認のプロセスを組み込みました。

商用リリース前に社内ユーザーによる商用動作確認が必要というのはよくあるケースです。
今回実施した内容だと、メトリクスベースのチェックに加えて、手動テストも実施できますので、より確実なリリースプロセスになったと言えます。
特定のユーザーの場合にHTTPヘッダやCookieに別途専用ヘッダを組み込む工夫は必要になりますが、これの利用価値というか需要は結構あるような気がします。

FlaggerのA/Bテストはまるごとルーティング対象を切り替える方式で、カナリアリリースの延長線にある機能です。
UIコンポーネント切り替えや細かい表示をテストする場合は、やはりGoogle Optimizeのような専用サービスの方が使い勝手がよいかなと思いました。


参照資料


  1. ここではWaitingPromotion状態で実施しましたが、ここまで待たずにProgressing中にロールバックGateをOpenにするとすぐにロールバックします。 ↩︎

豆蔵デベロッパーサイト - 先週のアクセスランキング
  1. 自然言語処理初心者が「GPT2-japanese」で遊んでみた (2022-07-08)
  2. Tauri でデスクトップアプリ開発を始める (2022-07-08)
  3. Deno による Slack プラットフォーム(オープンベータ) (2022-09-27)
  4. Jest再入門 - 関数・モジュールモック編 (2022-07-03)
  5. ORマッパーのTypeORMをTypeScriptで使う (2022-07-27)
  6. 第1回 OpenAPI Generator を使ったコード生成 (2022-06-04)
  7. 直感が理性に大反抗!「モンティ・ホール問題」 (2022-07-04)
  8. Rust によるデスクトップアプリケーションフレームワーク Tauri (2022-03-06)
  9. 箱ひげ図で外れ値を確認する (2022-05-18)
  10. Nuxt3入門(第1回) - Nuxtがサポートするレンダリングモードを理解する (2022-09-25)