ConftestでK8s ManifestのPolicy Testをする
はじめに
Kubernetes Manifestに対して使えるPolicy Testingのツールをいくつか試してみます。
全4回くらいの予定です。
- ConftestでK8s ManifestのPolicy Testing(本記事)
- OPA GatekeeperでK8s ManifestのPolicy Testing
- KonstraintでRegoとConstraintTemplateを管理
- GatorでConstraintTemplate/Constraintも含めたPolicy Testing
「やってみた」系の記事になりますので、Regoの書き方やConftestのサブコマンドを網羅するような内容ではありません。そちらが気になるかたは下記のサイトをどうぞ。
ConftestでPolicy Testing
ConftestはKubernetes Manifestなどの構造化されたデータに対するポリシーチェックを行うためのツールです。
ポリシーを記述するにはOpen Policy AgentのRego言語を利用します。
K8s Manifestを作成して色々いじってみます。下記のようなDeploymentを用意しました。
apiVersion: apps/v1
kind: Deployment
metadata:
name: dog-app-deployment
labels:
app.kubernetes.io/name: nginx
app.kubernetes.io/part-of: dog-application
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
まずはconftestのドキュメントにあるregoコードを使ってみます。
内容としては
- Deploymentの各コンテナはroot起動しない設定になっているか
- Deploymentの
matchLabels
にはapp
が設定されているか
のPolicy Testとなります。
package main
deny[msg] {
input.kind == "Deployment"
not input.spec.template.spec.securityContext.runAsNonRoot
msg := "Containers must not run as root"
}
deny[msg] {
input.kind == "Deployment"
not input.spec.selector.matchLabels.app
msg := "Containers must provide app label for pod selectors"
}
移行、K8s Manifestはapp-manifest
ディレクトリ、Regoはpolicy
ディレクトリに配置することとします。
.
├── README.md
├── app-manifest
│ └── dog-app-deployment.yaml
└── policy
└── main_deployment.rego
conftest
コマンド実行時、デフォルトで./policy
ディレクトリにある.rego
を参照します。明示的にディレクトリを指定したい場合は--policy
オプションを付けて実行する必要があります。
実行してみましょう。
$ conftest test ./app-manifest/dog-app-deployment.yaml --policy ./policy
FAIL - ./app-manifest/dog-app-deployment.yaml - main - Containers must not run as root
2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions
2つのRuleのうち1つがpased
、もう1つはfailure
となりました。
Deploymentを修正して、runAsNonRoot
を設定します
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app.kubernetes.io/name: nginx
app.kubernetes.io/part-of: dog-application
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
securityContext:
runAsNonRoot: true
もう一度実行してみます。
$ conftest test ./app-manifest/dog-app-deployment_runAsNonRoot.yaml --policy ./policy
2 tests, 2 passed, 0 warnings, 0 failures, 0 exceptions
今度は成功しました。
次はService
のManifestを作成し、それに対してPolicy Testをやってみましょう。
apiVersion: v1
kind: Service
metadata:
name: dog-app
spec:
ports:
- port: 80
targetPort: 80
selector:
app: nginx
Regoは以下のような形に。
Policy Testの内容は
- Serviceの
type
はClusterIPが設定されているか(デフォルトの設定でも良いとする)
のような形にしてみます。
package main
deny[msg] {
input.kind == "Service"
not object.get(input.spec, "type", "N_DEFINED") == "N_DEFINED"
not input.spec.type == "ClusterIP"
msg := "Services must not LoadBalancer"
}
Deploymentの時と同じようにコマンドを実行します。
$ conftest test ./app-manifest/dog-app-service.yaml --policy ./policy
3 tests, 3 passed, 0 warnings, 0 failures, 0 exceptions
失敗はしませんでしたが、3つのRuleが評価されています。
Service向けに書いたRuleは1つだけなのですが、Deployment向けに書いたRuleも実行されてしまっています。
実はConftestにも名前空間のような機能があり、デフォルトではmain
のパッケージにある各種Ruleを実行します。
なので、Deployment用とService用でpackageを別にします。
package deployment
deny[msg] {
input.kind == "Deployment"
not input.spec.template.spec.securityContext.runAsNonRoot
msg := "Containers must not run as root"
}
deny[msg] {
input.kind == "Deployment"
not input.spec.selector.matchLabels.app
msg := "Containers must provide app label for pod selectors"
}
package service
deny[msg] {
input.kind == "Service"
not object.get(input.spec, "type", "N_DEFINED") == "N_DEFINED"
not input.spec.type == "ClusterIP"
msg := "Services must be ClusterIP"
}
もう一度実行してみます。
$ conftest test ./app-manifest/dog-app-service.yaml --policy ./policy --namespace service
1 test, 1 passed, 0 warnings, 0 failures, 0 exceptions
ちゃんとService向けに書いたRegoのRuleのみ実行されました。
DeploymentとServiceのManifestに対するPolicy Testをとりあえず動かすところまでできました。
次にRule自体が正しいかのテストを書いていきます。
Ruleに対するテストを書く前に、テストがしやすいようRegoのコードを少し変更します。
package deployment
deny_run_as_nonroot_is_not_set[msg] {
input.kind == "Deployment"
not input.spec.template.spec.securityContext.runAsNonRoot
msg := "Containers must not run as root"
}
deny_matchlabels_app_is_not_set[msg] {
input.kind == "Deployment"
not input.spec.selector.matchLabels.app
msg := "Containers must provide app label for pod selectors"
}
package service
deny_service_type_is_not_cluster_ip[msg] {
input.kind == "Service"
not object.get(input.spec, "type", "N_DEFINED") == "N_DEFINED"
not input.spec.type == "ClusterIP"
msg := "Services must be ClusterIP"
}
Conftestで使うRegoのRuleですが、deny_
のようなprefixをつければRuleとして認識されるため、上記のような名前をつけることも可能です。
Ruleのテストのファイル名は慣例的に<任意>_test.rego
のような名前にすることが多いようです。テストのルールについてはtest_<任意>
のような形でないといけません。
Deploymentに対するRuleは下記の通りなので、そこからテストを書いてみます。
- Deploymentの各コンテナはroot起動しない設定になっているか
- Deploymentの
matchLabels
にはapp
が設定されているか
Rule | テストの内容 |
---|---|
1 |
runAsNonRoot がtrue
|
1 |
runAsNonRoot がfalse
|
1 |
runAsNonRoot が設定されていない |
2 |
matchLabels にapp が設定されている |
2 |
matchLabels にapp が設定されていない |
package deployment
test_run_as_nonroot_is_true {
not deny_run_as_nonroot_is_not_set["Containers must not run as root"] with input as {
"kind": "Deployment",
"spec": {
"template": {
"spec": {
"securityContext": {
"runAsNonRoot": true
}
}
},
},
}
}
test_run_as_nonroot_is_false {
deny_run_as_nonroot_is_not_set["Containers must not run as root"] with input as {
"kind": "Deployment",
"spec": {
"template": {"spec": {"securityContext": {"runAsNonRoot": false}}},
},
}
}
test_without_run_as_nonroot {
deny_run_as_nonroot_is_not_set["Containers must not run as root"] with input as {
"kind": "Deployment",
"spec": {
"template": {"spec": "none"},
},
}
}
test_deployment_with_matchlabels {
not deny_matchlabels_app_is_not_set["Containers must provide app label for pod selectors"] with input as {
"kind": "Deployment",
"spec": {
"selector": {"matchLabels": {
"app": "app",
}},
},
}
}
test_deployment_without_matchlabels {
deny_matchlabels_app_is_not_set["Containers must provide app label for pod selectors"] with input as {
"kind": "Deployment",
"spec": {
"selector": {"matchLabels": {
"none": "none",
}},
},
}
}
Serviceに対するRuleは下記の通りなので、こちらも同じくテストを書いてみます。
- Serviceの
type
はClusterIPが設定されているか(デフォルトの設定でも良いとする)
Rule | テストの内容 |
---|---|
1 |
type がCluster IP
|
1 |
type がセットされていない(ClusterIP になる) |
1 |
type がNodePort
|
1 |
type がLoadBalancer
|
package service
test_service_type_is_cluster_ip {
not deny_service_type_is_not_cluster_ip["Services must be ClusterIP"] with input as {
"kind": "Service",
"spec": {
"type": "ClusterIP"
},
}
}
test_service_type_is_not_set {
not deny_service_type_is_not_cluster_ip["Services must be ClusterIP"] with input as {
"kind": "Service",
}
}
test_service_type_is_nodeport {
deny_service_type_is_not_cluster_ip["Services must be ClusterIP"] with input as {
"kind": "Service",
"spec": {
"type": "NodePort"
},
}
}
test_service_type_is_loadbalancer {
deny_service_type_is_not_cluster_ip["Services must be ClusterIP"] with input as {
"kind": "Service",
"spec": {
"type": "LoadBalancer"
},
}
}
Rule対するテストを実行してみましょう。
$ conftest verify --policy ./policy
9 tests, 9 passed, 0 warnings, 0 failures, 0 exceptions, 0 skipped
上手くいきました。
今回は単純なRuleですが、実際の現場では複雑なRuleを記載することもあるためRegoのRule自体のテストはしっかりと書きましょう。
以上、Conftestでできることをざっくりまとめると下記の通りとなります。
- Regoを使ったPolicy Testing(
conftest test
) - Regoに記載したRuleのテスト(
conftest verify
)
他にもサブコマンドはあるのでconftest -h
で見てみると良いかと思います。
次回はGatekeeperでKubernetes環境上でのPolicy Testingを試してみます。
Discussion