はじめに
こんにちは。3-shakeのSreake事業部に所属する長澤翼(@toversus26)です。第8回目の今回は、Kubernetes Operatorのエンドツーエンド(E2E)テストツールである「Kyverno Chainsaw」について紹介します。
Kyverno Chainsawは、Kubernetes OperatorのE2Eの挙動を宣言的に定義してテストするCLIツールです。2023年12月にKubernetes向けのポリシーエンジンを開発しているKyvernoから初期バージョンがリリースされました。2024年9月17日時点でバージョンはv0.2.9となっています。Kyverno内で利用されていることもあり比較的活発に開発が進められています。
開発の背景
Kyverno Chainsawが開発された経緯は公式の「Kyverno Chainsaw - The ultimate end to end testing tool!」の記事にまとめられています。
Kubernetes Operatorの品質を担保するためにE2Eテストを実装することは重要です。Kubernetes Operatorでは主にGinkgoを利用したBehavior-Driven Development(BDD)を採用しており、実装が想定通りかを確認します。しかし、Kubernetes OperatorのE2Eテストを実装して保守するには、時間やコストが掛かります。E2Eテストのコストが高くなるとテストのカバー率が悪くなり、結果としてKubernetes Operatorの品質が低下してしまいます。とりわけOSSプロジェクトの場合、プロジェクト毎にE2Eテストの実装方法(テスト用の内部フレームワーク)が異なるため、学習コストが高くなりがちです。この課題を解決するために、YAML形式で宣言的にE2Eテストを書くことができるシンプルなKyverno Chainsawが開発されました。
基本的な使い方
Kyverno Chainsawは、実行するテストの流れをYAML形式で宣言します。例えば、Deploymentリソースを作成したときにPodが正しく作成されるかを確認するテストを書いてみましょう。
以下のファイルをchainsaw-test.yaml
として保存します。
01 | apiVersion: chainsaw.kyverno.io/v1alpha1 |
04 | name: deployment-to-pod |
15 | name: nginx-deployment |
33 | # DeploymentからPodが作成され、Podが正常に動作していることを確認 |
Kyverno ChainsawではTest
と呼ばれるカスタムリソース風の設定ファイルにE2Eテストを定義します。
Test
の中のsteps
配下にテストの流れを記載する。stepには4つのアクションを記述できるが、今回はtry
のみを利用
try
のアクションの下に実際に実行するオペレーションを定義する。オペレーションは複数用意されているが、今回の例ではapply
とassert
の基本的なオペレーションのみを利用している。try
のアクションの中で定義したオペレーションが1つでもエラーになると、テストは失敗となる
apply
でDeploymentリソースを作成する。実際にリソースが作成されるためテストを実行する際にKubernetesクラスタが必要。今回はresource
にインラインでリソースを定義しているが、fileを利用することでリソースを定義したYAMLファイルを参照することもできる
assert
でDeploymentリソースからPodが作成されることと、Podが正常な状態であることを確認する。 一定時間経過してもPodが期待通りの状態に遷移しない場合はタイムアウトエラーとなり、テストが失敗する
Kyverno chainsawでテストを実行するには、事前にKubernetesクラスタを準備します。kindなどのツールを利用してKubernetesクラスタを準備するか、既存のクラスタを利用する場合はコンテキストを正しく設定してください。
公式のインストール手順に従ってCLI をインストールして実行します。「.」を指定すると、現在のディレクトリから再起的にchainsaw-test.yaml
を探してテストを実行します。
08 | === RUN chainsaw/deployment-to-pod |
09 | === PAUSE chainsaw/deployment-to-pod |
10 | === CONT chainsaw/deployment-to-pod |
11 | | 13:35:52 | deployment-to-pod | @chainsaw | CREATE | OK | v1/Namespace @ chainsaw-cuddly-elephant |
12 | | 13:35:52 | deployment-to-pod | step-1 | TRY | BEGIN | |
13 | | 13:35:52 | deployment-to-pod | step-1 | APPLY | RUN | apps/v1/Deployment @ chainsaw-cuddly-elephant/nginx-deployment |
14 | | 13:35:52 | deployment-to-pod | step-1 | CREATE | OK | apps/v1/Deployment @ chainsaw-cuddly-elephant/nginx-deployment |
15 | | 13:35:52 | deployment-to-pod | step-1 | APPLY | DONE | apps/v1/Deployment @ chainsaw-cuddly-elephant/nginx-deployment |
16 | | 13:35:52 | deployment-to-pod | step-1 | ASSERT | RUN | v1/Pod @ chainsaw-cuddly-elephant/* |
17 | | 13:35:53 | deployment-to-pod | step-1 | ASSERT | DONE | v1/Pod @ chainsaw-cuddly-elephant/* |
18 | | 13:35:53 | deployment-to-pod | step-1 | TRY | END | |
19 | | 13:35:53 | deployment-to-pod | step-1 | CLEANUP | BEGIN | |
20 | | 13:35:53 | deployment-to-pod | step-1 | DELETE | OK | apps/v1/Deployment @ chainsaw-cuddly-elephant/nginx-deployment |
21 | | 13:35:53 | deployment-to-pod | step-1 | CLEANUP | END | |
22 | | 13:35:53 | deployment-to-pod | @chainsaw | CLEANUP | BEGIN | |
23 | | 13:35:53 | deployment-to-pod | @chainsaw | DELETE | OK | v1/Namespace @ chainsaw-cuddly-elephant |
24 | | 13:35:58 | deployment-to-pod | @chainsaw | CLEANUP | END | |
25 | --- PASS: chainsaw (0.00s) |
26 | --- PASS: chainsaw/deployment-to-pod (6.27s) |
今回の例を元に、Chainsawを実行したときのテストの流れを見ていきます。
- ランダムな名前で新しくテスト用のnamespaceを作成
- Deploymentリソースを作成
- Podリソースが作成され、正常に起動するのを待つ
- Deploymentリソースを削除
- テスト用のnamespaceを削除
chainsaw-test.yaml
を書き換えて必ず失敗するテストに置き換えてみましょう。今回はassert
の中で検証するPodのステータスのフェーズをCompleted
に変更します。
2 | +++ chainsaw-test.update.yaml |
再度テストを実行すると、今度はassert
の段階でタイムアウトが発生してテストが失敗します。
07 | === RUN chainsaw/deployment-to-pod |
08 | === PAUSE chainsaw/deployment-to-pod |
09 | === CONT chainsaw/deployment-to-pod |
10 | | 13:48:17 | deployment-to-pod | @chainsaw | CREATE | OK | v1/Namespace @ chainsaw-stirred-gopher |
11 | | 13:48:17 | deployment-to-pod | step-1 | TRY | BEGIN | |
12 | | 13:48:17 | deployment-to-pod | step-1 | APPLY | RUN | apps/v1/Deployment @ chainsaw-stirred-gopher/nginx-deployment |
13 | | 13:48:17 | deployment-to-pod | step-1 | CREATE | OK | apps/v1/Deployment @ chainsaw-stirred-gopher/nginx-deployment |
14 | | 13:48:17 | deployment-to-pod | step-1 | APPLY | DONE | apps/v1/Deployment @ chainsaw-stirred-gopher/nginx-deployment |
15 | | 13:48:17 | deployment-to-pod | step-1 | ASSERT | RUN | v1/Pod @ chainsaw-stirred-gopher/* |
16 | | 13:48:47 | deployment-to-pod | step-1 | ASSERT | ERROR | v1/Pod @ chainsaw-stirred-gopher/* |
18 | ---------------------------------------------------------------- |
19 | v1/Pod/chainsaw-stirred-gopher/nginx-deployment-77d8468669-96d9b |
20 | ---------------------------------------------------------------- |
21 | * status.phase: Invalid value: "Running": Expected value: "Completed" |
29 | + name: nginx-deployment-77d8468669-96d9b |
30 | namespace: chainsaw-stirred-gopher |
32 | + - apiVersion: apps/v1 |
33 | + blockOwnerDeletion: true |
36 | + name: nginx-deployment-77d8468669 |
37 | + uid: 85049a1c-0307-48eb-aa35-ff1805f0bdfd |
42 | + image: docker.io/library/nginx:1.14.2 |
43 | + imageID: docker.io/library/nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d |
53 | + startedAt: "2024-09-15T04:48:18Z" |
55 | | 13:48:47 | deployment-to-pod | step-1 | TRY | END | |
56 | | 13:48:47 | deployment-to-pod | step-1 | CLEANUP | BEGIN | |
57 | | 13:48:47 | deployment-to-pod | step-1 | DELETE | OK | apps/v1/Deployment @ chainsaw-stirred-gopher/nginx-deployment |
58 | | 13:48:47 | deployment-to-pod | step-1 | CLEANUP | END | |
59 | | 13:48:47 | deployment-to-pod | @chainsaw | CLEANUP | BEGIN | |
60 | | 13:48:47 | deployment-to-pod | @chainsaw | DELETE | OK | v1/Namespace @ chainsaw-stirred-gopher |
61 | | 13:48:52 | deployment-to-pod | @chainsaw | CLEANUP | END | |
62 | --- FAIL: chainsaw (0.00s) |
63 | --- FAIL: chainsaw/deployment-to-pod (35.29s) |
70 | Error: some tests failed |
Chainsawは失敗したテストを調査するときに必要な情報を併せて表示してくれます。
- テストが失敗した原因を表示する。今回の例で言うと
status.phase: Invalid value: "Running": Expected value: "Completed"
の部分。PodのフェーズがCompleted
となることを期待しているが、実際にはRunning
であることが分かる
- 実際のPodリソースと
assert
で指定したマニフェストのメタデータとステータスの差分を表示する。今回はPodの名前やOwnerReference、Podステータスの一部の情報を記載していないため、それらの差分も一緒に表示されている
他にもcatch
アクションを定義することでPod EventやPodの状態、Podのログなど追加で表示したい情報をカスタマイズできます。
02 | +++ chainsaw-test.update.yaml |
Chainsawのテストを実行すると、リソースを削除する前に上記で追加した情報を表示してくれます。
04 | === RUN chainsaw/deployment-to-pod |
05 | === PAUSE chainsaw/deployment-to-pod |
06 | === CONT chainsaw/deployment-to-pod |
07 | | 14:09:53 | deployment-to-pod | @chainsaw | CREATE | OK | v1/Namespace @ chainsaw-allowing-terrier |
08 | | 14:09:53 | deployment-to-pod | step-1 | TRY | BEGIN | |
09 | | 14:09:53 | deployment-to-pod | step-1 | APPLY | RUN | apps/v1/Deployment @ chainsaw-allowing-terrier/nginx-deployment |
10 | | 14:09:53 | deployment-to-pod | step-1 | CREATE | OK | apps/v1/Deployment @ chainsaw-allowing-terrier/nginx-deployment |
11 | | 14:09:53 | deployment-to-pod | step-1 | APPLY | DONE | apps/v1/Deployment @ chainsaw-allowing-terrier/nginx-deployment |
12 | | 14:09:53 | deployment-to-pod | step-1 | ASSERT | RUN | v1/Pod @ chainsaw-allowing-terrier/* |
13 | | 14:10:23 | deployment-to-pod | step-1 | ASSERT | ERROR | v1/Pod @ chainsaw-allowing-terrier/* |
15 | | 14:10:23 | deployment-to-pod | step-1 | TRY | END | |
16 | | 14:10:23 | deployment-to-pod | step-1 | CATCH | BEGIN | |
17 | | 14:10:23 | deployment-to-pod | step-1 | CMD | RUN | |
19 | /opt/homebrew/bin/kubectl get events -n chainsaw-allowing-terrier |
20 | | 14:10:24 | deployment-to-pod | step-1 | CMD | LOG | |
22 | LAST SEEN TYPE REASON OBJECT MESSAGE |
23 | 31s Normal Scheduled pod/nginx-deployment-77d8468669-2z4rx Successfully assigned chainsaw-allowing-terrier/nginx-deployment-77d8468669-2z4rx to kind-control-plane |
24 | 30s Normal Pulled pod/nginx-deployment-77d8468669-2z4rx Container image "nginx:1.14.2" already present on machine |
25 | 30s Normal Created pod/nginx-deployment-77d8468669-2z4rx Created container nginx |
26 | 30s Normal Started pod/nginx-deployment-77d8468669-2z4rx Started container nginx |
27 | 31s Normal SuccessfulCreate replicaset/nginx-deployment-77d8468669 Created pod: nginx-deployment-77d8468669-2z4rx |
28 | 31s Normal ScalingReplicaSet deployment/nginx-deployment Scaled up replica set nginx-deployment-77d8468669 to 1 |
29 | | 14:10:24 | deployment-to-pod | step-1 | CMD | DONE | |
30 | | 14:10:24 | deployment-to-pod | step-1 | CMD | RUN | |
32 | /opt/homebrew/bin/kubectl get pods -l app=nginx -n chainsaw-allowing-terrier |
33 | | 14:10:24 | deployment-to-pod | step-1 | CMD | LOG | |
35 | NAME READY STATUS RESTARTS AGE |
36 | nginx-deployment-77d8468669-2z4rx 1/1 Running 0 31s |
37 | | 14:10:24 | deployment-to-pod | step-1 | CMD | DONE | |
38 | | 14:10:24 | deployment-to-pod | step-1 | CMD | RUN | |
40 | /opt/homebrew/bin/kubectl logs --prefix -l app=nginx -n chainsaw-allowing-terrier --all-containers |
41 | | 14:10:24 | deployment-to-pod | step-1 | CMD | DONE | |
42 | | 14:10:24 | deployment-to-pod | step-1 | CATCH | END | |
43 | | 14:10:24 | deployment-to-pod | step-1 | CLEANUP | BEGIN | |
44 | | 14:10:24 | deployment-to-pod | step-1 | DELETE | OK | apps/v1/Deployment @ chainsaw-allowing-terrier/nginx-deployment |
45 | | 14:10:24 | deployment-to-pod | step-1 | CLEANUP | END | |
46 | | 14:10:24 | deployment-to-pod | @chainsaw | CLEANUP | BEGIN | |
47 | | 14:10:24 | deployment-to-pod | @chainsaw | DELETE | OK | v1/Namespace @ chainsaw-allowing-terrier |
48 | | 14:10:29 | deployment-to-pod | @chainsaw | CLEANUP | END | |
49 | --- FAIL: chainsaw (0.00s) |
50 | --- FAIL: chainsaw/deployment-to-pod (35.56s) |
57 | Error: some tests failed |
ユースケース
Kyverno ChainsawはKubernetes Operator向けに開発されたツールですが、Kubernetesクラスタを運用する際にも利用できます。クラスタ管理者はKubernetesクラスタやクラスタ上で動作するサードパーティ製のツールを運用しています。これらのバージョン更新や設定変更の後に、想定通りに動作しているかを確認します。この動作確認をGinkgoなどを利用したE2Eテストとして実装して自動化している人たちも一部いますが、ほとんどが手動だと思います。あらかじめKyverno Chainsawで確認したい項目を宣言的に定義しておくことで、定常作業を効率化できます。
公式ドキュメントのVAPのポリシーを例にKyverno ChainsawでVAPをテストしてみましょう。今回確認するVAPのルールでは、Deploymentのレプリカ数が5以下であることを強制しています。以下のVAPのリソースは既に作成されている前提でテストを書いてみましょう。
01 | apiVersion: admissionregistration.k8s.io/v1 |
02 | kind: ValidatingAdmissionPolicy |
04 | name: replicalimit-policy.example.com |
11 | operations: ["CREATE", "UPDATE"] |
12 | resources: ["deployments"] |
14 | - expression: "object.spec.replicas <= 5" |
15 | chainsaw-test.yamlにE2Eテストを定義します。 |
16 | apiVersion: chainsaw.kyverno.io/v1alpha1 |
19 | name: vap-replicalimit-policy |
21 | # テスト時に作成するnamespaceの名前を指定 |
22 | namespace: vap-replicalimit-policy |
27 | apiVersion: admissionregistration.k8s.io/v1 |
28 | kind: ValidatingAdmissionPolicyBinding |
30 | name: replicalimit-policy-test.example.com |
32 | policyName: replicalimit-policy.example.com |
33 | validationActions: [Deny] |
34 | # テスト時に作成したnamespaceに対してポリシーを適用 |
38 | kubernetes.io/metadata.name: vap-replicalimit-policy |
43 | # ポリシーに準拠したレプリカ数を指定したDeploymentを作成 |
44 | # Deploymentが作成できれば良いのでassertは不要 |
65 | # ポリシーに準拠していないレプリカ数を指定したDeploymentを作成 |
94 | ($error != null): true |
今回のテストでは、事前に作成されているVAPが想定通りの挙動かを確認します。
- テストを実行するnamespaceの名前を明示的に指定し、Validating Admission Policy Bindingを作成してVAPをテストを実行するnamespaceに紐付ける
- ポリシーに準拠していないDeploymentを作成するテストでは、Operation checkの機能を利用してリソースの作成に失敗することを確認する。このようにKyverno Chainsawでは操作に失敗するテストも定義できる
E2Eテストを実行します。レプリカ数が10のDeploymentを作成するテストでエラーが発生していますが、テストには通過していることが分かります。リソースを反映したときのエラーメッセージも同時に表示してくれるため、想定したVAPのルールでエラーが発生しているかも確認できます。
08 | === RUN chainsaw/vap-replicalimit-policy |
09 | === PAUSE chainsaw/vap-replicalimit-policy |
10 | === CONT chainsaw/vap-replicalimit-policy |
11 | | 14:50:29 | vap-replicalimit-policy | @chainsaw | CREATE | OK | v1/Namespace @ vap-replicalimit-policy |
12 | | 14:50:29 | vap-replicalimit-policy | step-1 | TRY | BEGIN | |
13 | | 14:50:29 | vap-replicalimit-policy | step-1 | APPLY | RUN | admissionregistration.k8s.io/v1/ValidatingAdmissionPolicyBinding @ replicalimit-policy-test.example.com |
14 | | 14:50:29 | vap-replicalimit-policy | step-1 | CREATE | OK | admissionregistration.k8s.io/v1/ValidatingAdmissionPolicyBinding @ replicalimit-policy-test.example.com |
15 | | 14:50:29 | vap-replicalimit-policy | step-1 | APPLY | DONE | admissionregistration.k8s.io/v1/ValidatingAdmissionPolicyBinding @ replicalimit-policy-test.example.com |
16 | | 14:50:29 | vap-replicalimit-policy | step-1 | TRY | END | |
17 | | 14:50:29 | vap-replicalimit-policy | step-2 | TRY | BEGIN | |
18 | | 14:50:29 | vap-replicalimit-policy | step-2 | SLEEP | RUN | |
19 | | 14:50:59 | vap-replicalimit-policy | step-2 | SLEEP | DONE | |
20 | | 14:50:59 | vap-replicalimit-policy | step-2 | TRY | END | |
21 | | 14:50:59 | vap-replicalimit-policy | step-3 | TRY | BEGIN | |
22 | | 14:50:59 | vap-replicalimit-policy | step-3 | APPLY | RUN | apps/v1/Deployment @ vap-replicalimit-policy/nginx-1 |
23 | | 14:50:59 | vap-replicalimit-policy | step-3 | CREATE | OK | apps/v1/Deployment @ vap-replicalimit-policy/nginx-1 |
24 | | 14:50:59 | vap-replicalimit-policy | step-3 | APPLY | DONE | apps/v1/Deployment @ vap-replicalimit-policy/nginx-1 |
25 | | 14:50:59 | vap-replicalimit-policy | step-3 | TRY | END | |
26 | | 14:50:59 | vap-replicalimit-policy | step-4 | TRY | BEGIN | |
27 | | 14:50:59 | vap-replicalimit-policy | step-4 | APPLY | RUN | apps/v1/Deployment @ vap-replicalimit-policy/nginx-10 |
28 | | 14:50:59 | vap-replicalimit-policy | step-4 | CREATE | WARN | apps/v1/Deployment @ vap-replicalimit-policy/nginx-10 |
30 | deployments.apps "nginx-10" is forbidden: ValidatingAdmissionPolicy 'replicalimit-policy.example.com' with binding 'replicalimit-policy-test.example.com' denied request: failed expression: object.spec.replicas <= 5 |
31 | | 14:50:59 | vap-replicalimit-policy | step-4 | APPLY | DONE | apps/v1/Deployment @ vap-replicalimit-policy/nginx-10 |
32 | | 14:50:59 | vap-replicalimit-policy | step-4 | TRY | END | |
33 | | 14:50:59 | vap-replicalimit-policy | step-3 | CLEANUP | BEGIN | |
34 | | 14:50:59 | vap-replicalimit-policy | step-3 | DELETE | OK | apps/v1/Deployment @ vap-replicalimit-policy/nginx-1 |
35 | | 14:50:59 | vap-replicalimit-policy | step-3 | CLEANUP | END | |
36 | | 14:50:59 | vap-replicalimit-policy | step-1 | CLEANUP | BEGIN | |
37 | | 14:50:59 | vap-replicalimit-policy | step-1 | DELETE | OK | admissionregistration.k8s.io/v1/ValidatingAdmissionPolicyBinding @ replicalimit-policy-test.example.com |
38 | | 14:50:59 | vap-replicalimit-policy | step-1 | CLEANUP | END | |
39 | | 14:50:59 | vap-replicalimit-policy | @chainsaw | CLEANUP | BEGIN | |
40 | | 14:51:00 | vap-replicalimit-policy | @chainsaw | DELETE | OK | v1/Namespace @ vap-replicalimit-policy |
41 | | 14:51:05 | vap-replicalimit-policy | @chainsaw | CLEANUP | END | |
42 | --- PASS: chainsaw (0.00s) |
43 | --- PASS: chainsaw/vap-replicalimit-policy (35.36s) |
このように、Kyverno ChainsawはVAPの挙動を確認するテストツールとしても利用できます。VAPのテストツールとしては、同じくKyvernoが開発しているkyverno testがあります。なおkyverno testでテストを実行する際にKubernetesクラスタは不要ですが、2024年9月17日時点で以下の問題があり、痒いところに手が届いていませんでした。
その点、Kyverno ChainsawはE2Eテストのレポート機能が優れているため、十分に活用できます。また、汎用的な作りをしているため他のユースケースとも併用可能です。
おわりに
今回は、Kyverno Chainsawによる宣言的なE2Eテストの利用例を見てきました。Kubernetes Operatorの開発以外でもクラスタ管理者の定常作業の動作確認としても利用できます。基本的な機能の学習コストは低く、YAMLのマニフェストを管理するだけで良いので、メンテナンス性も高く感じました。
また、今回は紹介していませんが、Kyvernoの機能を元にした少し複雑な機能もあります。ただ、複雑な機能を使うくらいならGoでテストコードを記述した方が柔軟かつ見通しも良いはずです。
Kyverno Chainsawを使ってE2Eテストの導入のハードルを下げてみてはどうでしょうか。