Kubernetesのカナリアデプロイメントを利用してA/Bテストを実装する方法

この記事はBob Reselmanによるゲスト投稿です。

A/Bテストは、ソフトウェアの2つのリリースバージョンを同時に実行し、ルーティング設定に従って特定のバージョンにアクセスを振り分けるというプラクティスです。

A/Bテストの典型的な例はWebサイトのユーザビリティテストです。サイトの運営者はWebサイトの安定版と並行してベータ版をリリースします。ユーザーはランダムに、または事前に定義した重みづけアルゴリズムに従って安定版またはベータ版にルーティングされます(たとえば受信トラフィックの10%だけがベータ版にルーティングされるなど)。

テスターやサイト分析者は、2つのバージョンから出力される利用データを集めて結果を比較します。ベータ版の結果が良ければ、より多くのトラフィックがベータ版に送信されます。結果がポジティブなものなら、いずれ、すべてのトラフィックがベータ版のサイトに送信されるようになります。ベータ版に何らかの問題が見られた場合、すべてのトラフィックが安定版にルーティングされ、ベータ版は公開アクセスから除かれます。修正が行われ、再びA/Bテストプロセスが始められます。

A/Bテストは有益なプラクティスですが、実施には手がかかる場合もあります。把握しておくべき流動的な要素は数多くあります。並行して実行される2つのバージョンのソフトウェアをリリースし、モニターしたうえで、問題が発生したらロールバックを行うには、多くの作業が必要な場合があります。カスタムのデプロイメントプロセスを採用している場合は特にそうです。 

Kubernetesは作業を格段に容易にします。Kubernetesはコンテナーオーケストレーションフレームワークであり、KubernetesのResourceConfigファイルに対して標準的なコマンドライン操作を行うだけで、容易にA/Bテストの実行と問題が起きたときのロールバックを実施できます。

Kubernetes のカナリアデプロイメントがA/Bテストの実装に役立つ可能性があります。カナリアデプロイメントでは、運用バージョンと並行して実験的なバージョンのコードを実行します。「炭鉱のカナリア」のようなものだと考えてください。カナリアが生きていれば、すべてうまくいっており、先に進んでも大丈夫です。

KubernetesでどのようにPodデプロイメントサービスリソースを利用してA/Bテストを行うか、またどのようにPodとKubernetesサービスの関係を確立し、カナリアデプロイメントを実現するかを説明していきましょう。

Kubernetesでのカナリアデプロイメント実装方法を示したコードの取得

ここからKubernetesでのカナリアデプロイメント実装方法を示したコードを取得できます。GitHubリポジトリのコードは、Katacodaインタラクティブラーニング環境を使用して、カナリアデプロイメントの実装をひととおり学ぶことができるようになっています。

Kubernetesの “service-to-pod” バインディングについて

Kubernetesでカナリアデプロイメントを実装する方法の詳細に入る前に、Kubernetesの基本的な概念についていくつか説明しておく必要があります。

Kubernetesはコードの1つまたは多数のインスタンスを少なくとも1つのマシンで、通常は多数のマシンで実行するよう設計されています。マシンは実マシンの場合も仮想マシンの場合もあります。Kubernetesの用語では、マシンの集合をクラスターと呼びます。通常、マシンはノードと呼ばれます。

Kubernetesでのコードのデプロイメントユニットはコンテナーです。それらのコンテナーはDockerHubGoogle Container RegistryAzure Container Registryなどのレジストリにコンテナーイメージがあります。ノードは1つまたは多数のコンテナーをホストできます。ただし、Kubernetesは明示的にノードにコンテナーをホストするわけではありません。Podと呼ばれるリソースを使用します。

Podについて

Kubernetesでは、コンテナーはPodと呼ばれる論理的構成ユニットで表されます。Podには1つまたは多数のコンテナーが含まれます。Podは実行時にKubernetesによってオンデマンドで作成されます。

Podの特性を説明する情報はマニフェストファイル、別名ResourceConfigファイルに保存され、YAMLまたはJSONで記述されます(コマンドラインで強制的にPodを構成し、作成することもできますが、ほとんどの運用環境では、Podやその他のKubernetesリソースはマニフェストファイルを使用して作成されます。これを宣言的作成と呼びます)。

KubernetesデプロイメントおよびReplicaSetについて

デプロイメントはKubernetesの正式なリソースです(下のリスト1を参照)。

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
 name: deployment-red
spec:
 replicas: 10
 template:
 metadata:
 labels:
 app: example_code
 color: red
 spec:
 containers:
 - name: echocolor
 image: reselbob/echocolor:v0.1
 ports:
 - containerPort: 3000
 env:
 - name: COLOR_ECHO_COLOR
 value: RED
 - name: COLOR_ECHO_VERSION
 value: V1

リスト1: Podの10個のレプリカを定義するデプロイメントマニフェスト

デプロイメントはPodのReplicaSetを表します。ReplicaSetは、特定のPodの少なくとも1つ、通常は多数の実行インスタンスを定義する論理的構成ユニットです。ReplicaSetに関して理解しておくべき重要事項は、ReplicaSetは定義に従ってクラスターが実行するPodレプリカ数を保証するということです。言い換えれば、デプロイメントのReplicaSetで、あるPodのインスタンスが常に10個実行される必要があると定義されていれば、ReplicaSetはKubernetesと協調して常に10個のインスタンスが実行されるよう保証します。 

何らかの理由で該当のKubernetesクラスターのノードがダウンし、マシンがホストするReplicaSetのPodインスタンスが存在しなくなった場合、Kubernetesは終了した分のPodを別のマシンで補充します。つまり、Kubernetesでは、デプロイメントは常にReplicaSetの状態を保証します。

Kubernetesサービスについて

Kubernetesのサービスは、クラスターに対してPodを提示する抽象的概念です。サービスはKubernetes固有のリソースです。Podおよびデプロイメントと同じように、サービスは強制的なコマンドで作成できます。しかし、やはりPodおよびデプロイメントと同じように、通常、サービスはマニフェストファイルを使用した運用設定で定義されます。

下のリスト2は、YAMLで記述されたマニフェストファイルの例で、echocolor-allという名前のKubernetesサービスを定義しています。

apiVersion: v1
kind: Service
metadata:
 name: echocolor-all
spec:
 selector:
 app: example_code
 ports:
 -
 protocol: TCP
 port: 3000
 targetPort: 3000
 type: NodePort

リスト2: サービスは、関連するマニフェストファイルのselector属性で定義された名前-値のペアに従って、サービスが表すPodを識別する

マニフェストファイルからサービスを作成するには、 kubectl applyコマンドセットを使用します。

kubectl apply -f service_01.yaml

説明

kubectlKubernetes API client用のコマンドです。

applyResourceConfigファイルを使用することを指示するサブコマンドです。

-fは適用されるResourceConfig(マニフェスト)ファイルを指定するオプションです。

service-01.yamlは使用するファイル名です。

理解しておくべき最も重要なことは、Kubernetesサービスは、Podとして構成された複数のコンテナーが提供するふるまいを表すということです。ですから、残る疑問は、クラスターにホストされた特定のPodのセットに、どのようにサービスがバインドされるかという点です。答えは、サービスのselector値とPodに定義されたラベルをマッピングすることによって、です。

Podの1つまたはそれ以上のラベルと、サービスの選択属性で指定された1つまたはそれ以上の名前-値のペアをマッピングすることで、サービスがPodにバインドされます。

下の図1を見てください。サービスのマニフェストファイルのselector属性には、app: example_codeという名前-値ペアがあります。図1に2つのPodがあることにも注目してください。図の各Podには、マニフェストファイルからlabels属性に関連する名前-値ペアだけを抜き出したものが示されています。どちらのPodにも、app: example_codeという名前-値ペアがあります。Kubernetes Podの検出ロジックの仕組み上、両方のPodにapp: example_codeが共通していることは重要です。

図1: サービスは、サービスのマニフェストのselector属性で定義されたものと同じ名前-値ラベルを持つPodにバインドされる。

Kubernetesサービスが作成されると、サービスのマニフェストファイルのselectorセクションで定義された名前-値ペアと一致するlabelセクションを持つPodが検索されます。上の図1の場合、サービスのマニフェストのselectorapp: example_codeと定義されているため、labelsセクションでapp: example_codeという名前-値ペアが定義されているPodが検索されます。したがって、クラスター内部のDNS名またはIPアドレスとポート番号によってサービスが呼び出されたとき、呼び出しはサービスに関連付けられたPodのコンテナーにフォワードされます。

サービスのセレクター値とPodラベルのマッピングは、Kubernetes環境でのカナリアデプロイメントを容易にするのに必須のメカニズムです。

カナリアデプロイメントの実装

Kubernetes環境にカナリアデプロイメントを導入するため、2つのKubernetesデプロイメントを作成し、それぞれのデプロイメントに固有のふるまいをするコンテナーを含むPodを持たせます。ただし、どちらのデプロイメントでも、デプロイメントマニフェストのlabelsセクションで定義された名前-値ペアは共通にします。その後、2つのKubernetesデプロイメントのPodが共通で使用する名前-値ペアにバインドされるセレクターを持つKubernetesサービスをリリースします。次の図 2 はこの概念を表しています。

図2: Kubernetesのカナリアデプロイメントでは、特定のデプロイメントのセットに含まれるすべてのPodがラベルを共有し、Kubernetesサービスにバインドされる。

では、この後で詳しく見ていきましょう。

ステーブルデプロイメントとカナリアデプロイメントの作成

下のリスト3は、deployment-redという名前のKubernetesデプロイメントを定義するマニフェストファイルです。デプロイメントはreselbob/echocolor:v0.1というコンテナーを含むPodのレプリカを10個作成します。各コンテナーにはCOLOR_ECHO_COLOR=REDおよびCOLOR_ECHO_VERSION=V1という2つの環境変数がインジェクトされます。

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
 name: deployment-red
spec:
 replicas: 10
 template:
 metadata:
 labels:
 app: example_code
 color: red
 spec:
 containers:
 - name: echocolor
 image: reselbob/echocolor:v0.1
 ports:
 - containerPort: 3000
 env:
 - name: COLOR_ECHO_COLOR
 value: RED
 - name: COLOR_ECHO_VERSION
 value: V1

リスト3: reselbob/echocolor:v0.1コンテナーを含み、環境変数COLOR_ECHO_COLOR=REDおよびCOLOR_ECHO_VERSION=V1がインジェクトされたPodのレプリカを10個定義するKubernetesデプロイメント

従って、 deployment-redには、環境変数COLOR_ECHO_COLOR=REDを持つという固有のふるまいがあり、deployment-greenにも環境変数COLOR_ECHO_COLOR=GREENを持つという固有のふるまいがあります。 

コンテナーreselbob/echocolor:v0.1内のアプリケーションは、HTTPレスポンスに2つの値を出力するWebサーバーです。値の1つは、環境変数COLOR_ECHO_COLORに割り当てられた値です。もう1つの出力値は、レスポンスが生成された時間です。つまり、deployment-redは文字列REDを出力するいっぽう、deployment-greenは文字列GREENを出力することを意味します。たしかにささいな動作ですが、サンプルとしては有効です。

デプロイメントdeployment-redはステーブルデプロイメントで、deployment-greenはカナリアデプロイメントです。 

各デプロイメントをクラスターにインジェクトするには、kubectl apply -f <MANIFEST_FILE>.yamlを実行します。例:

kubectl apply -f deployment-red.yaml

kubectl apply -f deployment-green.yaml

Kubernetesサービスの作成

次に、下のリスト4に示したマニフェストに従って2つのデプロイメントをサポートするKubernetesサービスをデプロイします。

apiVersion: v1
kind: Service
metadata:
 name: echocolor-all
spec:
 selector:
 app: example_code
 ports:
 -
 protocol: TCP
 port: 3000
 targetPort: 3000
 type: NodePort

リスト4: deployment-redとdeployment-greenの両方をサポートするKubernetesサービス

Kubernetesサービスechocolor-allのセレクターには、app: example_codeという名前-値ペアだけがあることに注目してください。この値-名前ペアはdeployment-reddeployment-greenのどちらも持っています。従って、サービスは両方のデプロイメントのPodにバインドされます。

deployment-redには10個のレプリカがあるいっぽう、deployment-greenには3つのレプリカしかないことにも注目してください。つまり、運用上、echocolor-allサービスが呼び出されたとき、deployment-redに紐づいたPodはdeployment-greenと比較して3倍強多くアクセスされます。このようにして、有効なカナリアデプロイメントが実現できます。Kubernetesの内部機構により、サービスechocolor-allの呼び出しはdeployment-greenのベータコードのPodに断続的にフォワードされます。

サービスの実行を試してみましょう。下のリスト6は、echocolor-allKubernetesサービスへのcurl呼び出しを20回行うコマンドラインとその結果です。サービスは、このインスタンスでは、IPアドレス172.17.0.78のポート31316で実行されています

(これを実際に実行するには、GitHubに関するこちらの記事のサンプルコードと手順をダウンロードしてください)。

$ for i in {1..20}; do curl 172.17.0.100:31763; done
{
“color”: “RED”,
“date”: “2019-12-20T02:46:34.335Z”
}{
“color”: “GREEN”,
“date”: “2019-12-20T02:46:34.352Z”
}{
“color”: “RED”,
“date”: “2019-12-20T02:46:34.388Z”
}{
“color”: “RED”,
“date”: “2019-12-20T02:46:34.404Z”
}{
“color”: “RED”,
“date”: “2019-12-20T02:46:34.422Z”
}{
“color”: “GREEN”,
“date”: “2019-12-20T02:46:34.441Z”
}{
“color”: “RED”,
“date”: “2019-12-20T02:46:34.452Z”
}{
“color”: “RED”,
“date”: “2019-12-20T02:46:34.463Z”
}{
“color”: “RED”,
“date”: “2019-12-20T02:46:34.473Z”
}{
“color”: “GREEN”,
“date”: “2019-12-20T02:46:34.486Z”
}{
“color”: “RED”,
“date”: “2019-12-20T02:46:34.503Z”
}{
“color”: “RED”,
“date”: “2019-12-20T02:46:34.514Z”
}{
“color”: “RED”,
“date”: “2019-12-20T02:46:34.524Z”
}{
“color”: “GREEN”,
“date”: “2019-12-20T02:46:34.531Z”
}{
“color”: “RED”,
“date”: “2019-12-20T02:46:34.543Z”
}{
“color”: “RED”,
“date”: “2019-12-20T02:46:34.554Z”
}{
“color”: “RED”,
“date”: “2019-12-20T02:46:34.561Z”
}{
“color”: “GREEN”,
“date”: “2019-12-20T02:46:34.568Z”
}{
“color”: “RED”,
“date”: “2019-12-20T02:46:34.578Z”
}{
“color”: “RED”,
“date”: “2019-12-20T02:46:34.587Z”
}

リスト5: カナリアデプロイメントを実行しているKubernetesクラスターに対して行われた呼び出しの大部分は、レガシーコードにルーティングされる。

上のリスト5では、単一のIPアドレスとポート番号に対してcurlコマンドが実行されているにもかかわらず、各呼び出しはdeployment-redおよびdeployment-greenの両方のPodにルーティングされ、deployment-redのほうが頻繁に呼び出されることがわかります。運用上、これは両方のデプロイメントが動作し、実行されることを意味します。deployment-greenに問題がなく、呼び出される頻度を増やしたい場合、下のリスト6に示すように、deployment-greenマニフェストファイル内のレプリカの数を増やします。

マニフェストのレプリカの値を変更したら、kubectl applyを使用してクラスターにマニフェストファイルを再適用します。例:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
 name: deployment-green
spec:
 replicas: 10
 template:
 metadata:
 labels:
 app: example_code
 color: green
 spec:
 containers:
 - name: echocolor
 image: reselbob/echocolor:v0.1
 ports:
 - containerPort: 3000
 env:
 - name: COLOR_ECHO_COLOR
 value: GREEN
 - name: COLOR_ECHO_VERSION
 value: V1

リスト6: Kubernetesデプロイメント環境で実行されるレプリカの数を増やすには、デプロイメントのマニフェストファイルのreplicas属性に割り当てる値を増やします。

kubectl apply -f deployment-green.yaml

マニフェストファイルを再適用すると、Kubernetesはデプロイメントの定義に従って実行するPodの数を増やし(または減らし)ます。

では、カナリアデプロイメントで問題が発生したらどうすればよいのでしょうか?その場合は、クラスターから問題のあるデプロイメントを削除するだけです。マニフェストを適用する代わりに、kubectl deleteコマンドセットを使用してデプロイメントを削除します。例:

kubectl delete -f deployment-green.yaml

この方法の優れているところは、Kubernetesは問題のあるデプロイメントに関連するPodをグレースフルに削除するので、安定したバージョンにはまったく影響を与えないことです。その後、問題を修正し、新しいコンテナーをコンテナーレジストリにデプロイしたら、あとはKubernetesクラスターにカナリアデプロイメントを再導入するだけです。それには、変更後のコンテナーを記述する新しい情報でマニフェストファイルを更新し、デプロイメントマニフェストを再度適用します。例:

kubectl apply -f deployment-green.yaml

まとめ

A/Bテストは現在のアプリケーションテストにおいて着実に行われるプラクティスになってきています。安定版とベータ版の両方のコードを並行して実行できることで、テスト技術者は現実のユーザーが関与する包括的なテストの恩恵を受けられます。

A/Bテストのデメリットは、裏で大変な作業が必要になる可能性があることです。しかし、朗報としては、Kubernetesを利用すると、共通のサービスで異なったふるまいを実行するのが容易になります。必要なのは、KubernetesのPod、デプロイメント、サービスの利用方法に関する基本的な知識だけです。あとは、多少の計画だけで、Kubernetesをカスタマイズすることもなく、カナリアデプロイメントを利用したA/Bテストをすぐに実現できるようになります。

基本を学ぶために費やした時間は、WebスケールでA/Bテストを実行できるようになれば、莫大なリターンをもたらすでしょう。

筆者のBob Reselmanについて:全国的に著名なソフトウェア開発者、システムアーキテクト、業界アナリスト、テクニカルライター/ジャーナリスト。コンピュータープログラミングに関する書籍のほか、ソフトウェア開発テクノロジーおよびテクニック、ソフトウェア開発カルチャーに関する記事を多数執筆。元Cap GeminiのPrincipal ConsultantおよびコンピューターメーカーGatewayのPlatform Architect。ロサンゼルス在住。現在は、ソフトウェア開発およびテスト活動のかたわら、自動化が雇用に与える影響に関する書籍を執筆中。連絡はLinkedInwww.linkedin.com/in/bobreselmanまで。

(この記事は、開発元Gurock社の Blog 「How to Implement A/B Testing Using a Kubernetes Canary Deployment」2020年3月26日の翻訳記事です。)

eBook 公開中

Paul Gerrard著 効果的なテスト管理12の秘密 (日本語)

テスト計画やテスト管理に役立つ12のトピックを解説します。

詳細はこちら