注目イベント!
春の新人向け連載企画開催中
新人エンジニアの皆さん、2024春、私たちと一緒にキャリアアップの旅を始めませんか?
IT業界への最初の一歩を踏み出す新人エンジニアをサポートする新連載スタート!
mameyose

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)が必要なユースケースを想定します。

事前準備

#

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にするとすぐにロールバックします。 ↩︎

豆蔵では共に高め合う仲間を募集しています!

recruit

具体的な採用情報はこちらからご覧いただけます。