Sieve を使って Kubernetes コントローラをテストしてみる

本記事は、[TUNA-JP Advent Calendar 2022](https://qiita.com/advent-calendar/2022/tuna-jp) 4日目の投稿です。今回は、VMware とイリノイ大学が共同で研究を行っている、[Sieve](https://github.com/sieve-project/sieve) と呼ばれるツールについて紹介したいと思います。

# Sieve って何？

Kubernetes 上でアプリケーションを動かすために、様々なベンダーやコミュニティが Kubernetes コントローラを開発しています。Kubernetes コントローラはアプリケーションのデプロイや設定の管理を担っているため、その信頼性は非常に重要です。ネットワークの切断や再起動が発生したとしても、正しく動作することが求められます。

Sieve とは、そうしたKubernetes コントローラの信頼性をテストするため、自動化されたテストツールです。Sieve を使うことで、障害に伴って発生し得る潜在的なバグを検出することができます。Sieve を使うためにはいくつか準備が必要ですが、Kubernetes コントローラ自体のコードは修正する必要がないので、導入が容易であるというメリットがあります。また、オープンソースプロジェクトとして公開されているので、誰でも試してみることができます。


# Sieve の基本的なアイデア

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621351/8c5d7963-5995-6556-1674-777ca048f56b.png)

Sieve は、まずはじめに、事前に定義されたテストケース（アプリケーションの新規デプロイや設定変更）を実行して、正常に動作した場合の全リソース（Pod や Volumeなど）の状態を、正常なパターンとして記録します。次に、正常なパターンに対して途中で擬似的に障害を発生させ、変化したリソースの状態（画像だと緑色のブロック）を障害有りのパターンとして記録します。この正常なパターンと障害有パターンでリソースの状態を比較して、Desired State が同じであるか、途中で想定外のこと（例えば、Volume の削除）が発生していないかなどを検知します。

Sieve がテストしてくれる障害は、現時点で以下の3パターンです。

- Intermediate-state パターン：Control Loop 中にクラッシュが発生
- Stale-state パターン：キャッシュの影響で過去のリソースの状態を取得してしまった場合
- Unobserved-state パターン：遅延が生じてリソースの状態を一部スキップしてしまった場合

これら3パターンを擬似的に発生させた場合に、Kubernetes コントローラが想定通りに動作するかテストを行います。

以上が、Sieve の基本的なアイデアになります。より詳細な解説は以下の資料から見ることができるので、興味のある方は論文や動画などを参照いただければと思います。

**英語資料**
- [Automatic Reliability Testing For Cluster Management Controllers](https://www.usenix.org/conference/osdi22/presentation/sun), OSDI \'22
- [Sieve: Automated Reliability Testing for Kubernetes Controllers](https://www.vmware.com/explore/video-library/video-landing.html?sessionid=1652966335957001NTRK&videoId=6311930491112), VMware Explore 2022 US

**日本語資料**
- [Sieve: Kubernetes コントローラのための自動化された信頼性テストツール](https://www.vmware.com/explore/jp/content/sess/VI22261.html), VMware Explore 2022 Japan


# デモを動かしてみる

残念ながら自分で Kubernetes コントローラを開発したことがないので、今回は Sieve のリポジトリに含まれている[デモ](https://github.com/sieve-project/sieve/blob/main/docs/demo.md)を動作させたいと思います。

サンプルと言っても、意図的にバグを含ませたようなコントローラではなく、実際に開発されている RabbitMQ のコントローラが対象です。Sieve が検出したバグは、実際に [Issue として報告](https://github.com/rabbitmq/cluster-operator/issues/782)されていて、修正も行われています。


## 準備

Sieve を動かすためには、以下が必要になるので、実行する環境に合わせてインストールします。

- Docker ※sudo なしで `docker` コマンドが実行可能
- go ※`$GOPATH` の設定が必要
- python3
- kubectl ※ `$KUBECONFIG` の設定が必要
- kind

## サンプルの実行

まずは、Sieve のリポジトリを clone します。

```
$ git clone https://github.com/sieve-project/sieve.git
```

次に、Sieve に必要な python パッケージをインストールします。

```
$ cd sieve
$ pip3 install -r requirements.txt
```

以下を実行して、Sieve で使うための kind と RabbitMQ コントローラのイメージを作成します。※ イベントをトレースするために一部改変が必要

```
$ python3 build.py -m test
$ python3 build.py -c examples/rabbitmq-operator -m test -s 4f13b9a942ad34fece0171d2174aa0264b10e947
```

以下のコマンドを実行することで、Sieve によるテストがはじまります。今回は、PVC のサイズ変更をテストケースとして、Intermediate-state パターンを障害有パターンとしてテストを行います。

```
$ python3 sieve.py -c rabbitmq-operator -m test -w resize-pvc -p bug_reproduction_test_plans/rabbitmq-operator-intermediate-state-1.yaml
```

上記コマンドを実行すると、まずテストを実行するための Kind クラスタを作成します。

```
Running Sieve with mode: test...
Log dir: sieve_test_results/rabbitmq-operator/resize-pvc/test/rabbitmq-operator-intermediate-state-1.yaml
Deleting cluster "kind" ...
try to create kind cluster; retry count 1
Creating cluster "kind" ...
 ✓ Ensuring node image (ghcr.io/sieve-project/action/node:v1.18.9-test) 🖼
 ✓ Preparing nodes 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
Set kubectl context to "kind-kind"
You can now use your cluster with:

kubectl cluster-info --context kind-kind

Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community 🙂



Test with: sieve_test_results/rabbitmq-operator/resize-pvc/test/rabbitmq-operator-intermediate-state-1.yaml/rabbitmq-operator-intermediate-state-1.yaml
Waiting for apiservers to be ready...
```

Kind クラスタの作成が完了すると、次はテスト対象のコントローラをデプロイします。

```
Setting up Sieve server...
[OK] Sieve server set up
configmap/sieve-testing-global-config created
Loading image ghcr.io/sieve-project/action/rabbitmq-operator:test to kind nodes...
Image: "ghcr.io/sieve-project/action/rabbitmq-operator:test" with ID "sha256:af1556ecf117b6210e1684b4b16a097e2980be30b33abbcfb8f99e50b4fb1d6b" not yet present on node "kind-control-plane", loading...
Deploying operator...
Installing csi provisioner...
customresourcedefinition.apiextensions.k8s.io/volumesnapshotclasses.snapshot.storage.k8s.io created
customresourcedefinition.apiextensions.k8s.io/volumesnapshotcontents.snapshot.storage.k8s.io created
customresourcedefinition.apiextensions.k8s.io/volumesnapshots.snapshot.storage.k8s.io created
~~ snip ~~
role.rbac.authorization.k8s.io/rabbitmq-cluster-leader-election-role created
clusterrole.rbac.authorization.k8s.io/rabbitmq-cluster-operator-role created
rolebinding.rbac.authorization.k8s.io/rabbitmq-cluster-leader-election-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/rabbitmq-cluster-operator-rolebinding created
deployment.apps/rabbitmq-operator created
Wait for the operator pod to be ready...
[OK] Operator deployed
```

コントローラのデプロイが完了した後は、テストケースを実行を行います。

```
Running test workload...

kubectl apply -f examples/rabbitmq-operator/test/rmqc-1.yaml
rabbitmqcluster.rabbitmq.com/rabbitmq-cluster created
2022-12-04 06:53:20.082144
wait until pod rabbitmq-cluster-server-0 becomes Running...
wait takes 72.742240 seconds
2022-12-04 06:54:32.824542

kubectl patch RabbitmqCluster rabbitmq-cluster --type merge -p='{"spec":{"persistence":{"storage":"15Gi"}}}'
rabbitmqcluster.rabbitmq.com/rabbitmq-cluster patched
2022-12-04 06:54:32.940480
wait until statefulset rabbitmq-cluster-server has storage size 15Gi...
wait takes 30.240761 seconds
2022-12-04 06:55:03.181409
```

テストケースの実行が終わると、以下のように Sieve が検知した問題について出力されます。

```
wait for final grace period 80 seconds
Generating controller family list...
Generating state update summary...
Generating end state...
Skipping endpointslice/default/rabbitmq-cluster-* for state-update-summary checker
Skipping endpointslice/default/rabbitmq-cluster-nodes-* for state-update-summary checker
Skipping endpoints/default/rabbitmq-cluster-nodes for state-update-summary checker
Skipping endpoints/default/rabbitmq-cluster for state-update-summary checker
2 detected end state inconsistencies as follows
End state inconsistency - object field has a different value: persistentvolumeclaim/default/persistence-rabbitmq-cluster-server-0["spec"]["resources"]["requests"]["storage"] is 15Gi after reference run, but 10Gi after testing run
End state inconsistency - object field has a different value: persistentvolumeclaim/default/persistence-rabbitmq-cluster-server-0["status"]["capacity"]["storage"] is 15Gi after reference run, but 10Gi after testing run

[PERTURBATION DESCRIPTION]
Sieve restarts the controller rabbitmq-operator when the trigger expression trigger1 is satisfied, where
trigger1 is satisfied after the controller rabbitmq-operator issues:
delete statefulset/default/rabbitmq-cluster-server with the 1st occurrence.

Deleting cluster "kind" ...
Total time: 350.5156054496765 seconds
```

ここでは、最終的な状態が正常なパターンと障害有パターンで異なっていたことを検知しています。具体的には、正常なパターンではストレージサイズが 15Gi であったことに対し、障害有パターンでは 10Gi のままでした。`[PERTURBATION DESCRIPTION]` には、さらに詳細な状況が記述されていて、StatefulSet を削除した時にコントローラの再起動を行ったことで、不一致が発生したことを示しています。

この検出されたバグに関しては、修正案も含めて [Issue](https://github.com/rabbitmq/cluster-operator/issues/782) として報告されています。


# おわりに

Sieve のサンプルデモを動かすところまで紹介しましたが、いかがでしょうか。ツールとしてはまだまだ洗練されていない部分もありますが、単純なテストでは検出が難しいような潜在的なバグも見つけてくれます。コントローラのコードはそのままでテストができるので、興味がある方は一度お試しいただければと思います。


# 余談

リソース不足のためか、Kind クラスタの作成に失敗したため、Sieve 専用の Ubuntu 仮想マシンを作りました。あと、ストレージの性能が悪いと Kind クラスタの作成がタイムアウトするため、そこそこ高性能なマシンで試してみることをおすすめします。