🤠

K8s ManifestのPolicy Test(Conftest/OPA Gatekeeper/Konstraint/gator)

2023/01/13に公開

はじめに

Kubernetes Manifestに対して使えるPolicy Testingのツールをいくつか試してみます。

  1. ConftestでK8s ManifestのPolicy Testing
  2. OPA GatekeeperでK8s ManifestのPolicy Testing
  3. KonstraintでRegoとConstraintTemplateを管理
  4. GatorでConstraintTemplate/Constraintも含めたPolicy Testing

「やってみた」系の記事になりますので、Regoの書き方やConftestのサブコマンドを網羅するような内容ではありません。そちらが気になるかたは公式ドキュメントの確認をお願いします。

想定シナリオ

クラスタ外からManaged LoadBalancerを通って各種システムが利用されます。
また、現在Dog Applicationは2つのバージョンが稼働しています。

apiVersion: v1
kind: Service
metadata:
  name: dog-app
  namespace: dog-app
spec:
  ports:
  - port: 80
    targetPort: 80
  selector:
    app: nginx
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dog-app-deployment-v1
  namespace: dog-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
      version: v1
  template:
    metadata:
      labels:
        app: nginx
        version: v1
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
  
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dog-app-deployment-v2
  namespace: dog-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
      version: v2
  template:
    metadata:
      labels:
        app: nginx
        version: v2
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
  

Dog App NamespaceのManifestを見ていくことにします。

想定要件

Dog Appの開発者が作成するYAMLについて要件を考えてみます。

K8s Resource 満たすべき要件
Service 想定外の外部公開を防ぐため、typeClusterIPのみとする。
Deployment PodTemplateのlabelにはappversionをつける。

開発者が作成するManifestについて、上記要件を満たしているか静的/動的 Policy Testを行っていきましょう。

ConftestでPolicy Testing

ConftestはKubernetes Manifestなどの構造化されたデータに対するポリシーチェックを行うためのツールです。
ポリシーを記述するにはOpen Policy AgentのRego言語を利用します。

まずはconftestのドキュメントにあるregoコードをベースにRuleを書いてみました。

package deployment
  
deny_pod_app_label_is_not_set[msg] {
  input.kind == "Deployment"
  not input.spec.template.metadata.labels.app
  
  msg := "Containers must provide app label for pod"
}
  
deny_pod_version_label_is_not_set[msg] {
  input.kind == "Deployment"
  not input.spec.template.metadata.labels.version
  
  msg := "Containers must provide version label for pod"
}
  
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"
}

以降、Dog ApplicationのManifestはapp-manifestディレクトリ、Regoはpolicyディレクトリに配置することとします。

.
├── README.md
├── app-manifest
│   ├── deployment-v1.yaml
│   ├── deployment-v2.yaml
│   └── service.yaml
└── policy
    └── deployment.rego

conftestコマンド実行時、デフォルトで./policyディレクトリにある.regoを参照します。明示的にディレクトリを指定したい場合は--policyオプションを付けて実行する必要があります。

実行してみましょう。

$ conftest test --namespace deployment ./app-manifest/deployment-v1.yaml 
  
2 tests, 2 passed, 0 warnings, 0 failures, 0 exceptions
$ conftest test --namespace deployment ./app-manifest/deployment-v2.yaml 
  
2 tests, 2 passed, 0 warnings, 0 failures, 0 exceptions
$ conftest test --namespace service ./app-manifest/service.yaml 
  
1 test, 1 passed, 0 warnings, 0 failures, 0 exceptions

すべてのPolicy Testが通りました。
試しにversionを消したDeploymentで試してみます。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: dog-app-deployment
  namespace: dog-app
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 test --namespace deployment ./app-manifest/deployment-without-label.yaml 
FAIL - ./app-manifest/deployment-without-label.yaml - deployment - Containers must provide version label for pod
  
2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions

1 passed, 1 failureとなりました。コメントにあるようにversion labelがないためです。

DeploymentとServiceのManifestに対するPolicy Testをとりあえず動かすところまでできました。
次にRule自体が正しいかのテストを書いていきます。

要件は下記の通りなので、そこからテストをざっくり書き出してみます。

K8s Resource 満たすべき要件
Service 想定外の外部公開を防ぐため、typeClusterIPのみとする。
Deployment PodTemplateのlabelにはappversionをつける。
K8s Resource テストの内容 分類
Deployment PodTemplateのlabelにappversionがある 正常系
Deployment PodTemplateのlabelにappがなくversionがある 異常系
Deployment PodTemplateのlabelにappがありversionがない 異常系
Deployment PodTemplateのlabelにappversionもない 異常系
Service typeがClusterIP 正常系
Service typeが設定されていない(=ClusterIP) 正常系
Service typeがNodePort 異常系
Service typeがLoadBalancer 異常系

上記をもとにRegoのテストを書いてみました。

package deployment
  
test_deployment_pod_template_with_app_label {
    not deny_pod_app_label_is_not_set["Containers must provide app label for pod"] with input as {
		"kind": "Deployment",
		"spec": {
			"template": {
				"metadata": {
					"labels": {
						"app": "test-app",
						"version": "test-version"
					}
				},
			},
		},
	}
}
  
test_deployment_pod_template_without_app_label {
    deny_pod_app_label_is_not_set["Containers must provide app label for pod"] with input as {
		"kind": "Deployment",
		"spec": {
			"template": {
				"metadata": {
					"labels": {
						"version": "test-version",
					}
				},
			},
		},
	}
}
  
test_deployment_pod_template_with_version_label {
    deny_pod_version_label_is_not_set["Containers must provide version label for pod"] with input as {
		"kind": "Deployment",
		"spec": {
			"template": {
				"metadata": {
					"labels": {
						"app": "test-app",
					}
				},
			},
		},
	}
}
  
test_deployment_pod_template_without_version_label {
    deny_pod_version_label_is_not_set["Containers must provide version label for pod"] with input as {
		"kind": "Deployment",
		"spec": {
			"template": {
				"metadata": {
					"labels": {
						"none": "none",
					}
				},
			},
		},
	}
}
  
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 
  
8 tests, 8 passed, 0 warnings, 0 failures, 0 exceptions, 0 skipped

上手くいきました。
今回は単純なRuleですが、実際の現場では複数のManifestに横断するような複雑なRuleを記載することもあるためRegoのRule自体のテストはしっかりと書きましょう。

以上、Conftestでできることをざっくりまとめると下記の通りとなります。

  • Regoを使ったPolicy Testing(conftest test
  • Regoに記載したruleのテスト(conftest verify

Kubernetesクラスタが利用できない環境でのPolicy TestingにはConftestは使えそうです。
次はOPA Gatekeeperを利用してKubernetesクラスタ環境でのPolicy Testingをやってみます。

OPA GatekeeperでPolicy Testing

OPA GatekeeperはKubernetesクラスタ上でAdmission Controllerとして動作し、特定のManifestのapplyを禁止するなどの機能を実現するためのツールです。

Conftestと同じく、ポリシーを記述するにはOpen Policy AgentのRego言語を利用しますが、入力パラメータの参照方法が少し違ったりしています。

GatekeeperにはRegoに加えてConstraintTemplateConstraintが必要になるので、まずはそれを作成します。

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sdeploymentpodtemplaterequiredlabels
spec:
  crd:
    spec:
      names:
        kind: K8sDeploymentPodTemplateRequiredLabels
      validation:
        openAPIV3Schema:
          type: object
          properties:
            labels:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sdeploymentpodtemplaterequiredlabels
  
        violation[{"msg": msg, "details": {"missing_labels": missing}}] {
          provided := {label | input.review.object.spec.template.metadata.labels[label]}
          required := {label | label := input.parameters.labels[_]}
          missing := required - provided
          count(missing) > 0
          msg := sprintf("you must provide labels: %v", [missing])
        }

このManifestでテンプレートができたので.spec.crd.spec.names.kindK8sDeploymentPodTemplateRequredLabelsを作成します。

ConftestではappversionのPolicyを別々に作成していましたが、流用のしやすさを考え、.spec.crd.spec.validation.openAPIV3Schema.propertiesの部分でパラメータ化しています。

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sDeploymentPodTemplateRequiredLabels
metadata:
  name: podtemplate-must-have-app-version-label
spec:
  match:
    kinds:
      - apiGroups: ["apps"]
        kinds: ["Deployment"]
    namespaces: ["dog-app"]
  parameters:
    labels: ["app", "version"]

対象はdog-appNamespaceにあるDeploymentとしており、必要とするLabelはappversionです。

Serviceも同じようにTemplateとConstraintを作成します。

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sservicerequiredtype
spec:
  crd:
    spec:
      names:
        kind: K8sServiceRequiredType
      validation:
        openAPIV3Schema:
          type: object
          properties:
            serviceType:
              type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sservicerequiredtype
        violation[{"msg": msg}] {
          not object.get(input.review.object.spec, "type", "N_DEFINED") == "N_DEFINED"
          not input.review.object.spec.type == input.parameters.serviceType
          msg := sprintf("you must set service type: %v", [input.parameters.serviceType])
        }
  
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sServiceRequiredType
metadata:
  name: service-required-clusterip
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Service"]
    namespaces: ["dog-app"]
  parameters:
    serviceType: "ClusterIP"
  

上記のManifestはK8sクラスタにapplyしておきます。

$ kubectl get k8sdeploymentpodtemplaterequiredlabels.constraints.gatekeeper.sh 
NAME                                      ENFORCEMENT-ACTION   TOTAL-VIOLATIONS
podtemplate-must-have-app-version-label                        0
$ kubectl get k8sservicerequiredtype.constraints.gatekeeper.sh 
NAME                         ENFORCEMENT-ACTION   TOTAL-VIOLATIONS
service-required-clusterip                        0

準備ができたので、Dog ApplicationのYAMLをapplyしてみます。

$ kubectl apply -f app-manifest/deployment-v1.yaml 
deployment.apps/dog-app-deployment-v1 created
  
$ kubectl apply -f app-manifest/deployment-v2.yaml 
deployment.apps/dog-app-deployment-v2 created
  
$ kubectl apply -f app-manifest/service.yaml 
service/dog-app created

上記は要件を満たしたManifestであるため問題なく作成されました。
次に、要件を満たしていないManifestをapplyしてみます。

$ kubectl apply -f app-manifest/deployment-without-label.yaml 
Error from server (Forbidden): error when creating "app-manifest/deployment-without-label.yaml": admission webhook "validation.gatekeeper.sh" denied the request: [podtemplate-must-have-app-version-label] you must provide labels: {"version"}

version LabelがないDeploymentをapplyした場合は上記の通り拒否されます。

$ kubectl apply -f app-manifest/service-nodeport.yaml 
Error from server (Forbidden): error when applying patch:
{"metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"name\":\"dog-app\",\"namespace\":\"dog-app\"},\"spec\":{\"ports\":[{\"port\":80,\"targetPort\":80}],\"selector\":{\"app\":\"nginx\"},\"type\":\"NodePort\"}}\n"}},"spec":{"type":"NodePort"}}
to:
Resource: "/v1, Resource=services", GroupVersionKind: "/v1, Kind=Service"
Name: "dog-app", Namespace: "dog-app"
for: "app-manifest/service-nodeport.yaml": error when patching "app-manifest/service-nodeport.yaml": admission webhook "validation.gatekeeper.sh" denied the request: [service-required-clusterip] you must set service type: ClusterIP

NodePortのServiceをapplyした場合と

$ kubectl apply -f app-manifest/service-loadbalancer.yaml 
Error from server (Forbidden): error when applying patch:
{"metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"name\":\"dog-app\",\"namespace\":\"dog-app\"},\"spec\":{\"ports\":[{\"port\":80,\"targetPort\":80}],\"selector\":{\"app\":\"nginx\"},\"type\":\"LoadBalancer\"}}\n"}},"spec":{"type":"LoadBalancer"}}
to:
Resource: "/v1, Resource=services", GroupVersionKind: "/v1, Kind=Service"
Name: "dog-app", Namespace: "dog-app"
for: "app-manifest/service-loadbalancer.yaml": error when patching "app-manifest/service-loadbalancer.yaml": admission webhook "validation.gatekeeper.sh" denied the request: [service-required-clusterip] you must set service type: ClusterIP

LoadBalancerのServiceをapplyした場合は上記の通り拒否されます。

以上、OPA Gatekeeperでできることをざっくりまとめると下記の通りとなります。

  • KubernetesのAdmission ControllerでのPolicy Testing

Kubernetesクラスタが利用できる環境でのPolicy TestingにOPA Gatekeeperが利用できることがわかりました。

ConftestもOPA GatekeeperもRegoを利用してPolicyを書きましたが両方には若干の書き方の違いがあることがわかりました。また、OPA GatekeeperはConstraintTemplateにRegoを埋め込まないといけません。

上記の負荷を下げるために、次はKonstraintを導入してみます。

KonstraintでRegoとConstraintTemplateを管理する

KonstraintはRegoからGatekeeperのConstraintTemplateを生成するためのツールです。またGithubリポジトリにはConftestとGatekeeperのRegoの差異をある程度吸収するためのライブラリも公開しています。

https://github.com/plexsystems/konstraint/tree/main/examples/lib

上記のlibの中身を少しのぞいてみましょう。

https://github.com/plexsystems/konstraint/blob/main/examples/lib/core.rego

package lib.core
  
default is_gatekeeper = false
  
is_gatekeeper {
	has_field(input, "review")
	has_field(input.review, "object")
}
  
resource = input.review.object {
	is_gatekeeper
}
  
resource = input {
	not is_gatekeeper
}
  
# 略

Conftestの場合、YAMLがinputとなるためinput.<Resouceのspec>となるのですが、Gatekeeperの場合はAdmissionReviewがinputとなるため、input.review.object.<Resourceのspec>となります。
その差異を吸収してresource.<Resourceのspec>でアクセスできるようにしよう、というのが上のライブラリになります。

このライブラリをつかってConftestのRegoとGatekeeperのRegoの際を吸収してみましょう。まずはConftestのRegoから修正します。
METADATAのコメントがありますが、いったん無視してください。このあとのKonstraintで利用します。)

# METADATA
# title: Deployment PodTemplate Required Labels
# description: >-
#  This policy allows you to require certain labels are set on a PodTemplate.
# custom:
#   parameters:
#     labels:
#       type: array
#       description: Array of required label keys.
#       items:
#         type: string
  
package k8sdeploymentpodtemplaterequiredlabels
  
import data.lib.core
  
violation[{"msg": msg}] {
  provided := {label | core.resource.spec.template.metadata.labels[label]}
  required := {label | label := input.parameters.labels[_]}
  missing := required - provided
  count(missing) > 0
  msg := sprintf("you must provide labels: %v", [missing])
}
  
  
package k8sdeploymentpodtemplaterequiredlabels
  
test_deployment_pod_template_with_app_label {
    input := {
		"kind": "Deployment",
		"spec": {
			"template": {
				"metadata": {
					"labels": {
						"app": "test-app",
						"version": "test-version"
					}
				},
			},
		},
		"parameters": {
			"labels": ["app", "version"]
		}
	}
	count(violation) == 0 with input as input
}
  
test_deployment_pod_template_without_app_label {
    input := {
		"kind": "Deployment",
		"spec": {
			"template": {
				"metadata": {
					"labels": {
						"version": "test-version"
					}
				},
			},
		},
		"parameters": {
			"labels": ["app", "version"]
		}
	}
	count(violation) == 1 with input as input
}
  
test_deployment_pod_template_with_version_label {
    input := {
		"kind": "Deployment",
		"spec": {
			"template": {
				"metadata": {
					"labels": {
						"version": "test-version"
					}
				},
			},
		},
		"parameters": {
			"labels": ["app", "version"]
		}
	}
	count(violation) == 1 with input as input
}
  
test_deployment_pod_template_without_version_label {
    input := {
		"kind": "Deployment",
		"spec": {
			"template": {
				"metadata": {
					"labels": {
						"other": "other-app"
					}
				},
			},
		},
		"parameters": {
			"labels": ["app", "version"]
		}
	}
	count(violation) == 1 with input as input
}
# METADATA
# title: Service Type Required
# description: >-
#  This policy allows you to require certain type are set on a Service.
# custom:
#   parameters:
#     serviceType:
#       type: string
#       description: String of required Service type.
  
  
package k8sservicerequiredtype
  
import data.lib.core
  
violation[{"msg": msg}] {
  core.has_field(core.resource.spec, "type")
  not core.resource.spec.type == input.parameters.serviceType
  msg := sprintf("you must set service type: %v", [input.parameters.serviceType])
}
package k8sservicerequiredtype
  
test_service_type_is_cluster_ip {
    input := {
		"kind": "Service",
		"spec": {
			"type": "ClusterIP" 
		},
		"parameters": {
			"serviceType": "ClusterIP"
		}
	}
	count(violation) == 0 with input as input
}
  
test_service_type_is_not_set {
    input := {
		"kind": "Service",
		"spec": {
			"none": "none"
		},
		"parameters": {
			"serviceType": "ClusterIP"
		}
	}
	count(violation) == 0 with input as input
}
  
test_service_type_is_nodeport {
    input := {
		"kind": "Service",
		"spec": {
			"type": "NodePort" 
		},
		"parameters": {
			"serviceType": "ClusterIP"
		}
	}
	count(violation) == 1 with input as input
}
  
test_service_type_is_loadbalancer {
    input := {
		"kind": "Service",
		"spec": {
			"type": "LoadBalancer" 
		},
		"parameters": {
			"serviceType": "ClusterIP"
		}
	}
	count(violation) == 1 with input as input
}
  

Ruleに対するテストが通ることも確認しておきます。

$ conftest verify --policy ./k8s-deployment-podtemplate-required-labels/
  
33 tests, 33 passed, 0 warnings, 0 failures, 0 exceptions, 0 skipped
  
$ conftest verify --policy ./k8s-service-required-type/
  
33 tests, 33 passed, 0 warnings, 0 failures, 0 exceptions, 0 skipped

テストケースが想定よりも多くでていますが、これはKonstraintのlibのテストケースが含まれているためです。

次にConstraintTemplateを作る必要があるのですが、lib含めたRegoを毎回手作業でConstraintTemplateにするのは負荷が高いです。
KonstraintにはRegoからConstraintTemplateを作成する機能がありますのでそれを使ってみましょう。

$ konstraint create k8s-deployment-podtemplate-requred-labels/
WARN[0000] Skipping constraint generation due to use of parameters  name=K8sDeploymentPodtemplateRequredLabels src=k8s-deployment-podtemplate-requred-labels/deployment.rego
INFO[0000] completed successfully                        num_policies=1
$ konstraint doc k8s-deployment-podtemplate-requred-labels/
WARN[0000] No kind matchers set, this can lead to poor policy performance.  name=K8sDeploymentPodtemplateRequredLabels src=k8s-deployment-podtemplate-requred-labels/deployment.rego
INFO[0000] completed successfully                        num_policies=1

ConstraintTemplateが作成されました。
Constraint自体は自動生成されないため手作業で作成します。今回はGatekeeperの検証で利用したものをそのまま流用しましょう。

以上、Konstraintでできることをざっくりまとめると下記の通りとなります。

  • RegoファイルからConstraintTemplateを生成する機能(konstraint create
  • ConstraintとGatekeeperのRegoの差異をある程度減らす
    • 正確に言うと「konstraintのgithubリポジトリでlibが提供されている」です
    • konstraintコマンドを利用しなくてもlibのみ使用というのも可能です

ConftestのRegoとGatekeeperのRegoの差異をなくし、ConstraintTemplateまで自動で作成することができました。
が、Coftestの賞で実行できていた下記コマンドは上手く動かなくなってしまっています。
input.parameters.~がないためですね。

今のままではKubernetesがない環境でのPolicy Testingができません。
それを解決するためのツールとして、最後にgatorを利用してみましょう。

(conftestだけでも外部のパラメータを読み込むことは可能ですが、そこでGatekeeperとの差異が生まれてしまうので今回は割愛します。また、conftest実行前にyqコマンドでinput.parametersなどを追加することもできますがそちらも今回は割愛します)

GatorでConstraintTemplate/Constraintも含めたPolicy Testing

gatorはGatekkeperのConstraintTemplateとConstraintsをK8sがないローカル環境で利用するためのCLIツールです。

ConstraintTemplate/Constraintを使ってDog AppのManifestをテストしてみます。

まずはDeploymentから

$ gator test --filename ./app-manifest/deployment-v1.yaml --filename k8s-deployment-podtemplate-required-labels/
$ echo $?
0
  
$ gator test --filename ./app-manifest/deployment-v2.yaml --filename k8s-deployment-podtemplate-required-labels/
$ echo $?
0
  
$ gator test --filename ./app-manifest/deployment-without-label.yaml --filename k8s-deployment-podtemplate-required-labels/
["podtemplate-must-have-app-version-label"] Message: "you must provide labels: {\"version\"}" 

Exit Codeが0になっていれば正常終了しています。

次にService

$ gator test --filename ./app-manifest/service.yaml --filename k8s-service-required-type/
$ echo $?
0
  
$ gator test --filename ./app-manifest/service-nodeport.yaml --filename k8s-service-required-type/
["service-required-clusterip"] Message: "you must set service type: ClusterIP" 
  
$ gator test --filename ./app-manifest/service-loadbalancer.yaml --filename k8s-service-required-type/
["service-required-clusterip"] Message: "you must set service type: ClusterIP" 

gatorを導入することでConstraintTemplate/Constraintを利用したPolicy Testをローカル環境でも実行することができました。

Suite fileでテストを書くこともできるので、それも試してみます。

kind: Suite
apiVersion: test.gatekeeper.sh/v1alpha1
tests:
- name: app-version-label
  template: template.yaml
  constraint: constraint.yaml
  cases:
  - name: app-version-label-exist-for-v1
    object: ../app-manifest/deployment-v1.yaml
    assertions:
    - violations: no
  - name: app-version-label-exist-for-v2
    object: ../app-manifest/deployment-v2.yaml
    assertions:
    - violations: no
  - name: version-label-missing
    object: ../app-manifest/deployment-without-label.yaml
    assertions:
    - violations: yes
$ gator verify ./k8s-deployment-podtemplate-required-labels/suite.yaml --verbose
=== RUN   app-version-label
    === RUN   app-version-label-exist-for-v1
    --- PASS: app-version-label-exist-for-v1    (0.005s)
    === RUN   app-version-label-exist-for-v2
    --- PASS: app-version-label-exist-for-v2    (0.004s)
    === RUN   version-label-missing
    --- PASS: version-label-missing     (0.006s)
--- PASS: app-version-label     (0.021s)
ok      ./k8s-deployment-podtemplate-required-labels/suite.yaml 0.022s
PASS

以上、gatorでできることをざっくりまとめると下記の通りとなります。

  • ConstrainteTemplate/Constraintを利用し、K8sがないローカル環境でPolicy Testを行う

各種ツールの利用方針(主観含む)

Conftest、OPA Gatekeeper、Konstraint、gatorを一通り触ってみました。できることできないことを下記の表にまとめます。

実施内容 Conftest Gatekeeper Konstraint gator
.regoのRule自体のTest - - -
K8sなし環境でManifestのPolicy Test - - 〇※1
K8sあり環境でManifestのPolicy Test - - -
ConstraintTemplate/Constraintの生成 - - -

※1 ConstraintTemplateに埋め込まれた.regoを使ったPolicy Test

主観ですが

  1. Konstraintのlibを利用して.regoを書く
  2. .regoのRule自体をconftest verifyでテスト
  3. konstraint createでConstraintTemplateを生成する
  4. 必要に応じてConstraintを書く
  5. gatorのSuiteを書き、gator verifyでCI環境などでのPolicy Test
  6. K8s環境にConstraintTemplate/Constraintをapplyし、GatekeeperでPolicy Test

の流れが良いのかなと思っています。
ただ、実際の現場ではConstraintTemplate/ConstraintとアプリのManifestの管理体系はことなるでしょうし権限や統制周りの話も含まれるため、そのあたりは柔軟に対応しておく必要があるのではないでしょうか。

参考リンク

denyルールとdeny[{"msg": msg}]のテストの書き方で小一時間悩んだときにたどり着いた情報。これはハマる。
https://stackoverflow.com/questions/60083793/rego-testing-how-to-test-not-deny

Discussion