Conftest,Gatekeeperを用いたKubernetesマニフェストバリデーションと、Gatorを用いたテスト
Abstract
Kubernetes (K8s) のマニフェストのバリデーションを行う手法を解説する。
効率的な開発・リリースを行うため、K8sを採用する場合は通常CI/CDパイプラインの採用も行う。
すなわち、コードの変更に応じて自動的にビルド・テスト・デプロイまで行われる前提となる。
CI中でアプリケーションのビルド・テストが行われることはもちろん必要だが、同様にK8sマニフェストのテストやセキュリティチェックが行われることが望ましい。
また、CD中で組織のポリシーに沿わないK8sマニフェストがデプロイされることも防ぐ必要がある。
本解説ではOPA, Conftest, Konstraint, Gatekeeper, Gatorという技術スタックを用いて上記を自動的に実現する仕組みを、簡単なコードと共に解説する。
Introduction
Background
Abstractで記述したように、CI/CDパイプラインでK8sマニフェストのバリデーションを実施することは重要である。
現在のところ、CI中で主要なツールは以下である。
CD中で主要なツールは以下である。
- Gatekeeper[6]
- OPAをベースとして動くK8s admission 管理ソフトフェア
- ベースとなるCRDを作成し(
ConstraintTemplate
)、そのCRDを利用することでデプロイされるリソースのバリデーションを行うことができる
CI・CDの片方だけで採用しても、もう片方でバリデーションが行えないと不便であったりリスクが考えられるため、CI/CD中では上記ソフトウェア群を併用するのが望ましい。
ただし、OPAとGatekeeperはベースとなる言語は同一であるものの、書き方が微妙に異なり、単純に統合することは困難である。
したがって、OPA用とGatekeeper用に2度ポリシーを記述する必要がある。
この点は詳細な解説があるため省略する[7] [8]。
この点を解決するために、Konstraint[9]というライブラリが存在する。
これは、Conftestの記法をGatekeeperに変換するライブラリである。
このライブラリを採用することで以下のメリットが有る。
- CI/CD中で記述するポリシーコードが単一になる。
- Rego言語を用いてポリシーのモジュール化を行うことができる。
- Rego言語を用いて簡単にコーディングやテストを行いつつ、Gatekeeperに自動的に変換することができる。
反面、変換時にコードの可読性が下がるなど、デメリットも有る。
しかし、総合的に判断して現状では有力な選択肢と考えられる。
上記に加えて、Gatekeeper v3.7.x からはGator[10]を用いてGatekeeperポリシー自体のテストを行うことができるようになった。
v3.9.x 時点では alpha 版であるが、Gatekeeperコミュニティによって精力的に開発が行われており、将来的に有力な選択肢になる可能性が高い。
GatorでもCI中のバリデーションが行えるようになったため、ConftestやKonstraintが不要になるかという議論はあっても良いと考える。
この点、個人的な意見だが、コーディング・テストのしやすさ、coverageの導出などの手間を考えると、相補的に運用していくのが好ましいと思われる。
本解説では上記の技術スタックを用いて、K8sマニフェストのバリデーション基盤のCI/CDを行う方法を記述する。
全体感を重視し、各々の詳細な解説はスコープ外とする。
全体の流れとしては以下の通り。
-
Confestを用いて、Rego言語に基づいてK8sマニフェストのバリデーションを行うコードを記述する。
確認として、ユニットテストの記述やopaを用いたカバレッジの測定、実際のK8sマニフェストのバリデーションを行う。 - Konstraintを用いてRego言語をGatekeeper形式に変更する。これにより、実際のK8sクラスタでadmissionを行うことができる。
- 生成されたGatekeeperマニフェストのテストとして、Gatorを用いたテストを行う。
- 上記を自動化するために、GitHub Actionsを用いたCIを構築する。
- 最終的な確認として、kindクラスターを用いて生成されたマニフェストの確認を行う。
Example
K8sマニフェストのバリデーションの例としては、個人的な興味から有名なCDツールであるArgoCD[11]を取り上げる。
ArgoCDにはProject[12]という機能があるが、デフォルトで導入されている default
Projectは権限の範囲が広く、削除不可であるという難点がある(変更は可能)。
このため、default
Projectを利用不可にしてしまいたい[13]。
具体的には、以下の application.yaml
をデプロイ不可とする。
.spec.project
の値にのみ注目すればよい。
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: bad-application
namespace: argocd
spec:
destination:
namespace: default
server: https://kubernetes.default.svc
project: default # <- This will be not allowed
source:
path: kustomize-guestbook
repoURL: https://github.com/argoproj/argocd-example-apps.git
targetRevision: main
Source code
本解説で用いるソースコードは全てGithub 上にある[14]。
Conftest
Code
まず、Rego言語を用いて default
Projectへのデプロイを禁止にする。
コードの例を以下に示す[15]。
# METADATA
# title: Deny ArgoCD Application deployed in the default project
# description: |-
# Deny ArgoCD application deployed in the default project.
# custom:
# matchers:
# kinds:
# - kinds:
# - Application
# apiGroups:
# - "argoproj.io"
package argocd.deny_app_in_default_project
import data.lib.core
violation[{"msg": msg}] {
name := core.resource.metadata.name
project := core.resource.spec.project
project == "default"
msg := sprintf("Error: %s ArgoCD Application is not permitted to use default ArgoCD project.", [name])
}
大体は読めば理解できると考えるが、ポイントは以下の通り。
- Konstraintを用いるため、METADATAにGatekeeper用の設定を記述すること。
-
import data.lib.core
を用いて、Konstraintのライブラリをimportすること。-
conftest pull github.com/plexsystems/konstraint/examples/lib -p lib
でpullできる[8:1]。
-
- ConftestとGatekeeperの差分を吸収するために、
input.metadata.name
ではなく、core.resource.metadata.name
として上記のライブラリを用いること。- この事情はKonstraint公式に詳しく記述されている[7:1]。
簡単なテストも記述しておく。解説は省略する。
package argocd.deny_app_in_default_project
test_violation {
input := {
"apiVersion": "argoproj.io/v1alpha1",
"kind": "Application",
"metadata": {"name": "test-app"},
"spec": {"project": "default"},
}
violation[{"msg": "Error: test-app ArgoCD Application is not permitted to use default ArgoCD project."}] with input as input
}
test_no_violation {
input := {
"apiVersion": "argoproj.io/v1alpha1",
"kind": "Application",
"metadata": {"name": "test-app"},
"spec": {"project": "not-default"},
}
not violation[{"msg": "Error: test-app ArgoCD Application is not permitted to use default ArgoCD project."}] with input as input
}
Test
コーディングが完了したので、テストを行う(前述の通り、Konstraintのライブラリをダウンロードするのを忘れないこと)。
手元の環境では他にもいくつかテストを追加しているため、outputの値は違うことに注意。
- Conftest
-
テスト:
conftest verify -p .
output34 tests, 34 passed, 0 warnings, 0 failures, 0 exceptions, 0 skipped
- 失敗する場合は
--trace
オプションをつけてデバッグすると良い。
- 失敗する場合は
-
- OPA
-
テスト:
opa test . --ignore *.yaml
outputPASS: 34/34
- 後に配置するyamlファイルをテストから除くため、ignoreとしている。
-
coverage:
opa test . --ignore *.yaml --coverage -v | jq .coverage
[5:1]output89.4
-
マニフェストチェック(
default
Projectのため失敗):conftest test -p . --namespace argocd.deny_app_in_default_project ./argocd/argocd-deny-app-in-default-project/tests/disallowed/aplication-in-default-prj.yaml
outputFAIL - ./argocd/argocd-deny-app-in-default-project/tests/disallowed/aplication-in-default-prj.yaml - argocd.deny_app_in_default_project - Error: bad-application ArgoCD Application is not permitted to use default ArgoCD project.
-
マニフェストチェック(
non-default
Projectのため成功):conftest test -p . --namespace argocd.deny_app_in_default_project ./argocd/argocd-deny-app-in-default-project/tests/allowed/aplication-in-non-default-prj.yaml
output1 test, 1 passed, 0 warnings, 0 failures, 0 exceptions
-
上記により OPA, Conftestを用いたテストの実行、及び実際のマニフェストのチェックが完了した。
Konstraint
上記コードを元にKonstraintを実行し、Gatekeeperマニフェストの生成を行う。
konstraint doc .
konstraint create .
自動的に policy.md
, template.yaml
, constraint.yaml
が作成されたはずである。
$ ls ./argocd/argocd-deny-app-in-default-project/
constraint.yaml src.rego src_test.rego suite.yaml template.yaml tests
Gator
Gatorを用いて生成された template.yaml
, constraint.yaml
のテストを行う。
gator test
と gator verify
があるが、多くのテストをシステマチックに実行する場合は verify
のほうが便利だと思うので、こちらを利用する[10:1]。
v3.9.xの時点ではα版であることを再度注意しておく。
suite.yaml
を作成し、これを元にテストを行う[16]。
ファイルは以下の通りである。読めばだいたいわかるように、Suite、tests、casesからなる。
- Suite
- テスト用のCRD
- tests
- caseのまとまり。test毎にGatekeeper
ConstraintTemplate
リソースとConstraints
リソースを指定する。
- caseのまとまり。test毎にGatekeeper
- case(詳細は公式ドキュメント参照[17])
- テストケース。テストする対象のマニフェストを指定する。
- assertionsでviolationの回数を指定できる。yesはat least once。
- messageでエラーメッセージの1部を指定できる。
kind: Suite
apiVersion: test.gatekeeper.sh/v1alpha1
tests:
- name: deny-app-default-prj
template: template.yaml
constraint: constraint.yaml
cases:
- name: allowed-non-default-prj
object: "./tests/allowed/aplication-in-non-default-prj.yaml"
assertions:
- violations: no
- name: disallowed-default-prj
object: "./tests/disallowed/aplication-in-default-prj.yaml"
assertions:
- violations: yes
- message: "ArgoCD Application is not permitted to use default ArgoCD project."
violations: 1
Gatorの実行は容易。
gator verify ./argocd/argocd-deny-app-in-default-project/
ok home/toyamagu/github.com/toyamagu-2021/konstraint-example/argocd/argocd-deny-app-in-default-project/suite.yaml 0.017s
PASS
上記のように成功していることがわかる。
ConftestからGatorまで、全てをいちいち手で実行するのは面倒なため、スクリプトを作成した[18]。
CI/CD
ここでのCI/CDは上記のKonstraint, Gatorなどの実行を自動化することを指す。
素直にコード化すれば良く、解説は省略するが、GitHub Actionsを用いて自動化を行った[19]。
PRに coverage をコメントさせる等の処置を行ってある。
GatekeeperのテストをGatorのみでなくよりきちんとやるのであれば、 Kind などを用いたローカルクラスタでのE2Eテストをしておくとより安全と考えられる。
Test in a Kind cluster
KindクラスターにArgoCDとGatekeeperをデプロイし、動作テストを行う。
-
クラスター作成とArgoCD・Gatekeeperデプロイ。スクリプトを作成した[20]
-
マニフェスト apply
$ k apply -f ./argocd/argocd-deny-app-in-default-project/template.yaml constrainttemplate.templates.gatekeeper.sh/argocddenyappindefaultproject created $ k apply -f ./argocd/argocd-deny-app-in-default-project/constraint.yaml argocddenyappindefaultproject.constraints.gatekeeper.sh/argocddenyappindefaultproject created
-
失敗例
$ k apply -f ./argocd/argocd-deny-app-in-default-project/tests/disallowed/aplication-in-default-prj.yaml Error from server (Forbidden): error when creating "./argocd/argocd-deny-app-in-default-project/tests/disallowed/aplication-in-default-prj.yaml": admission webhook "validation.gatekeeper.sh" denied the request: [argocddenyappindefaultproject] Error: bad-application ArgoCD Application is not permitted to use default ArgoCD project
-
成功例
$ k apply -f ./argocd/argocd-deny-app-in-default-project/tests/allowed/aplication-in-non-default-prj.yaml application.argoproj.io/good-application created
Conclusion
本解説では、K8sマニフェストのバリデーションを行う方法、またそのCI/CDの方法を記述した。
ConftestやKonstraintを用いた、優れた解説は以前からあった [8:2] [5:2]が、本解説では異なる例や、Github Actionsの例を含めて記述した。
加えて、GatorというCLIツールが加わることにより、容易にCI中でGatekeeperマニフェストのテストが行えるようになった。
しかし、コーディング・テストの容易性という観点から、依然としてConftestやKonstraintの利点は多くあると思われる。
今回用いたのはArgoCDでデプロイできるProjectを制限するという、トリビアルな例だった。
他にも、CDツールであるArgoCDにはAutoSync等場合によっては(本番環境など)セキュリティ上disableとしたい機能がある。
このように、環境に応じて適切な機能制限を加えることは、Gatekeeperの重要な役割であると考えられる。本番運用する際の導入を今後も検討していきたい。
References
-
https://www.cncf.io/blog/2020/07/23/conftest-joins-the-open-policy-agent-project/#:~:text=Conftest fits nicely into the,lots of different use cases. ↩︎
-
https://github.com/open-policy-agent/conftest/blob/108edfe44f247c2048ed7247f6ea28cea72bcb26/policy/engine.go#L401 ↩︎
-
https://engineering.mercari.com/blog/entry/introduce_conftest/ ↩︎ ↩︎ ↩︎
-
https://open-policy-agent.github.io/gatekeeper/website/docs/ ↩︎
-
https://github.com/plexsystems/konstraint#why-this-tool-exists ↩︎ ↩︎
-
https://open-policy-agent.github.io/gatekeeper/website/docs/gator/ ↩︎ ↩︎
-
https://argo-cd.readthedocs.io/en/stable/user-guide/projects/ ↩︎
-
RBACを用いて制限するという手もあるが、ArgoCD v2.4.xでapp of apps パターンを用いる場合などは、開発者がhackできる可能性がある。 ↩︎
-
https://github.com/toyamagu-2021/konstraint-example/blob/main/argocd/argocd-deny-app-in-default-project/ ↩︎
-
https://open-policy-agent.github.io/gatekeeper/website/docs/gator#suites ↩︎
-
https://open-policy-agent.github.io/gatekeeper/website/docs/gator/#cases ↩︎
-
https://github.com/toyamagu-2021/konstraint-example/blob/main/scripts/test-and-run.sh ↩︎
-
https://github.com/toyamagu-2021/konstraint-example/blob/main/.github/workflows/konstraint.yaml ↩︎
-
https://github.com/toyamagu-2021/konstraint-example/blob/main/scripts/kind-with-argocd-and-gatekeeper.sh ↩︎
Discussion