- Rego のポリシーは、それぞれに名前つけたほうがテスト書いていて指定したポリシーと明示できるのがいい。けど、いちいち名前指定するのが面倒という意見もありそう。
- Rego のポリシー自体が正しいか試行錯誤することになるので、先にテスト書いたほうがいい。テストを書いてから、実際のYAMLに流すの流れが効率的。
- Rego のテストは、引数禁止、テストが何をしても通らなくなる。ダメな例:
test_foo[msg] { }
。いい例:test_foo { }
- Rego のポリシーと結果をサクッと試すにはPlayground が便利。
基本的なコンセプト、構文、関数はここを参照
拡張子は .rego。
言語的にはDSLに割り切っている。
全般的に package main
や sprintf("%s", string)
を含めて Golang っぽさがそこかしこにある。
2つ特徴がある。
- 代入を除くと式は同じポリシー内でも順不同に配置しても問題なく評価される。
- itelator が
foo = input.keys[_]
のように書くと、foo
にはarray/set/mapの要素が入ってくること。キモい。
VS Code に Open Policy Agent 拡張を入れると、フォーマッタが効くようになり、構文解析まで行ってくれる。おまけで、コマンドパレットで validate とかも出て VSCodeでプレイグラウンド的に使えるがそこまではいらない。
ただ、Kustomize とは相性悪いので微妙なライン。インテリセンスもないししょぼいが正しい構文とフォーマットが維持できるのでないよりマシ。
conftest + Open Policy Agent が隙がない。
- CI やローカルで実行するときは、conftest を用いることでサクッと実行できる、便利、よい。
- Kubernetes Cluster 内部でデプロイされる前にチェックする(GitOpsは典型) 場合は、Open Policy Agent | Kubernetes Admission Control を用いる。
まずは conftest で実行できるようにすると、動作が把握できてカジュアルに試せるのでいい。 もっとカジュアルに試すなら Playground を活用するといい。
ポリシーの基本は次の通り。
- ポリシーは複数のルール(=式) を持つ。
- ルールは bool を返し、AND評価(すべてのルールが true になるとポリシーに該当しているとみなされる)を行う。
- ポリシーの必須要素は、package宣言、violation などのポリシーの評価方法、msg。
- pacakge はmain以外は実行時に評価されないので、別名においておくと importで取り込むことで、data.パッケージ名 として参照したりできる。
package main
# 評価結果は、violation / deny / warn / allow で選べる。名前を付ける場合、先頭の文字がいずれかに該当していれば ok
# violation と deny は ポリシーを満たすとダメ扱い。warn は警告扱い。BlackListでのポリシー評価が楽なので、allow は使わない
violation[msg] {
# boolを返すルールを複数記述できる。入力は、外部ライブラリを使わない限り input に入ってくる。
msg := "ポリシー該当時のメッセージを記述する"
}
violation_何か名前をつけたり[msg] {}
violation_引数をマップにしたり[{"msg": msg}] {
式の基本的な記述は、公式をみるといい。
組み込み関数の基本的な記述も、公式をみるといい。
ポリシーの名称は deny[msg]
や violation[msg]
だけでもいいが、violation_some_explanation[msg]
のように明示的に重ならない名前を与えることができる。
どちらも機能する。しかし、個人的には明示的にviolation_nanika_suru[msg]
のように一意な名前を与えるほうが好ましいと思っている。
理由は、テストでのtraceのかかり方、対象ポリシー以外が毎度評価されるため。
Rego は、同一関数名をすべて評価して回るので、もしポリシー名がすべて deny[msg]
だったりすると、テストでも全ポリシーを実行する。
これは正直トレースログを見た時にやばいほど読みにくいし、評価されてほしいポリシーだけ機能してほしいのに他のポリシーも影響することを意味している。
明示的にこのポリシーをテストしたいのに他が評価されるというのは、ちょっと受け入れがたい気持ち悪さがある。
Compare だけややこしいので特記しておく。
同値の評価に :=
と ==
と =
演算子があるが、基本的に代入に :=
、値比較に ==
を使うように公式にも記述があるので従っておくのがいい。
Equality Applicable Compiler Errors Use Case
-------- ----------- ------------------------- ----------------------
:= Inside rule Var already assigned Assign local variable
== Inside rule Var not assigned Compare values
= Everywhere Values cannot be computed Express query
例外的に、式の結果を受けるときに =
を使って評価と代入を同時に行うケースがある。
[image_name, "latest"] = split_image(container.image)
split_image(image) = [image, "latest"] {
not contains(image, ":")
}
split_image(image) = [image_name, tag] {
[image_name, tag] = split(image, ":")
}
なお、数値の比較演算子には次のものがある、普通。
a == b # `a` is equal to `b`.
a != b # `a` is not equal to `b`.
a < b # `a` is less than `b`.
a <= b # `a` is less than or equal to `b`.
a > b # `a` is greater than `b`.
a >= b # `a` is greater than or equal to `b`.
関数は、戻り値を指定しない限りは bool が返る前提になっている。
is_foo {
input.name == "foo"
}
パラメーターを与える場合は、 関数名(引数名){}
と書く。
is_foo(value) {
value == "foo"
}
bool ではなく、関数から特定の返り値をも耐える場合は 関数名() = 返り値{}
と書く。複数の返り値があるなら、関数名() = [返り値1, 返り値2]{}
となる。
get_name_age() = [name, age] {
name := input.name
age := input.age
}
例えば次のような input とポリシーが書ける。PlayGround で試しておみるといい。
Input
{
"name": "John Doe",
"age": 100
}
ポリシー
package play
deny[msg] {
[name, age] = get_name_age
msg := sprintf("name: %s, age: %v", [name, age])
}
get_name_age() = [name, age] {
name := input.name
age := input.age
}
Output
{
"deny": [
"name: John Doe, age: 100"
],
"get_name_age": [
"John Doe",
100
]
}
ポリシーは基本的に AND評価。 OR評価を行いたい場合は、関数を用意するか、配列に対しておこなう。
関数の場合、同名関数を用意するとそれぞれの関数が個別に評価され、true が返った関数が利用される。
例えば、次のような is_byte_format
関数を用意sruto
is_byte_format(size) {
endswith(size, "Gi")
}
is_byte_format(size) {
endswith(size, "Mi")
}
配列を使う場合、配列に許可する要素を記述しておいて、itelator を使って順次結果が評価したい値と一致するかを判定する。 イテレーターきもい。
workload_resources := ["Deployment", "StatefulSet"]
input.kind == workload_resources[_] # kind が Deployment / StatefulSet の時だけ true になる。
Kubernetes で使うにあたってポイント。
Kubernetes で利用する場合、様々なリソースYAML に同じルールが適用されることを前提に書くことになる。 例えば、pod や container に関する記述は、 Pod / Deployment / StatefulSet / DaemonSet / Job / Cronjob に適用してほしいだろうが、Service や Ingress には適用してほしくない。
そのため、ポリシーの AND評価を利用して先に kind
や apiVersion
で絞りこむことになる。
violate[msg] {
input.kind == "Deployment"
}
だがこのような記述にすると、StatefulSet と Deployment にー、などが書きにくい。そのため、先に配列で許可リストを用意するといい。
workload_resources := ["Deployment", "StatefulSet"]
is_deployment_or_statefulset {
input.kind == workload_resources[_]
}
ポリシーでis_deployment_or_statefulset
関数を呼び出すことで、そのポリシーは DeploymentかStatefulSetでのみ評価されることが保障できる。
violation[msg] {
kubernetes.is_deployment_or_statefulset
# 何かルール
msg = "ポリシー違反です"
}
「特定のラベルがないときにエラーにする」といったキーがないことを評価するときは not
を使う。
violation[msg] {
kubernetes.is_deployment_or_statefulset
not input.metadata.labels["not-found"] # ココ
msg = "ポリシー違反です"
}
複数のラベルに対して、どれか一つでもない場合にエラーにすることを評価するときは、配列 + not を使う。これで、どれか一つでもラベルにないとポリシーがひっかかる。
recommended_labels {
input.metadata.labels["app.kubernetes.io/name"]
input.metadata.labels["app.kubernetes.io/instance"]
input.metadata.labels["app.kubernetes.io/version"]
input.metadata.labels["app.kubernetes.io/component"]
input.metadata.labels["app.kubernetes.io/part-of"]
input.metadata.labels["app.kubernetes.io/managed-by"]
}
violation[msg] {
kubernetes.is_deployment_or_statefulset
not recommended_labels # ここ
msg = "ポリシー違反です"
}
これを kubernetes.rego として保存しておいて、使いたいポリシーで、import data.kubernetes
してから kubernetes.is_service
などのようにして使っている。
package kubernetes
# properties
name := input.metadata.name
kind := input.kind
is_service {
kind == "Service"
}
workload_resources := ["Deployment", "StatefulSet"]
environment_labels := ["development", "staging", "production"]
is_deployment_or_statefulset {
input.kind == workload_resources[_]
}
is_pod {
kind == "Pod"
}
is_service {
kind == "Service"
}
is_not_local {
input.metadata.labels.environment == environment_labels[_]
}
is_ingress {
kind == "Ingress"
}
pod_containers(pod) = all_containers {
keys := {"containers", "initContainers"}
all_containers = [c | keys[k]; c = pod.spec[k][_]]
}
containers[container] {
pods[pod]
all_containers := pod_containers(pod)
container := all_containers[_]
}
containers[container] {
all_containers := pod_containers(input)
container := all_containers[_]
}
pods[pod] {
is_deployment_or_statefulset
pod := input.spec.template
}
pods[pod] {
is_pod
pod := input
}
volumes[volume] {
pods[pod]
volume := pod.spec.volumes[_]
}
# image functions
split_image(image) = [image, "latest"] {
not contains(image, ":")
}
split_image(image) = [image_name, tag] {
[image_name, tag] = split(image, ":")
}
# security functions
dropped_capability(container, cap) {
container.securityContext.capabilities.drop[_] == cap
}
added_capability(container, cap) {
container.securityContext.capabilities.add[_] == cap
}
ポリシーは、それ正常に評価されているのか、使ってはだめなINPUT例と使っていいINPUT例 で繰り返し試しながら書くことになる。 実際の YAML に対して変更かけつつ試すと、普段のYAMLに対してどう追加すればいいのかを試す / ポリシー評価が意図通りか試す、試したものを戻す、といった余計は作業が多く発生する。
このため、ポリシーは「想定するINPUTを用意」して、「INPUTに対してポリシーを記述」して、「ポリシーをテストで通るか確認」して、「実際のYAMLに対して実行」するという流れがうまく当てはまる。
テストでは、次の3つを行う。
- 想定するINPUTを用意
- INPUTに対してポリシーを記述
- ポリシーをテストで通るか確認
conftest を用いる場合、テストは次の形式で実行できる。
# テスト結果概要だけ表示
$ conftest verify --policy ./path/to/policy
# テスト一つずつの評価経過をtrace表示
$ conftest verify --policy ./path/to/policy --trace
# テスト結果概要 + 失敗したテストのみ評価経過を表示
$ conftest verify --policy ./path/to/policy --report failed
# テスト結果を一つずつPASS/FAILED表示 + 失敗したテストは評価経過を表示
$ conftest verify --policy ./path/to/policy --report full
テストは、「ポリシーファイル_test.rego」が定番なので従っておく。(_test
は必須じゃないが、ポリシーと同階層におくので自然とそうなる)
テストには注意が3つある。
- テストは
test_
でポリシー命名が始まる。 - テストは、ポリシーに引数がない。
- テストの msg は、評価するポリシーと同じ出力になるようにしないといけない。
引数がないのは忘れがち。万が一引数があるとポリシーが正常に評価されてもテストは通らない。
# ダメな例
test_violation_labels_recommended_missing[msg] {}
# いい例
test_violation_labels_recommended_missing {}
テストの msg はポリシーで固定文字列なら同じものを入れればいい。
だが、どのリソースでポリシーが違反したか判別のために input.kind
や input.metadata.name
を使っている場合は、テストでも同じ結果になるように注意が必要だ。
# 固定のメッセージならポリシーとテストで同じものを指定でok
msg = "推奨ラベルを付与してください。(https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels)"
# 動的なmsgなポリシー
violation_some_policy[msg] {
input.kind == "Deployment"
input.metadata.name == "test-data"
msg = sprintf("推奨ラベルを付与してください。(https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels): [Kind=%s,Name=%s]", [input.kind, input.metadata.name])
}
# ポリシーに該当するテスト
test_violation_some_policy {
# テストのmsgはinput に合わせる必要がある。
msg = "推奨ラベルを付与してください。(https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels): [Kind=Deployment,Name=test-data]"
violation_some_policy[msg] with input as {
"kind": "Deployment",
"metadata": {
"name": "test-data",
},
}
}
例えばKubernetes推奨ラベルがあることを保証するポリシーがあるとしよう。
package main
recommended_labels {
input.metadata.labels["app.kubernetes.io/name"]
input.metadata.labels["app.kubernetes.io/instance"]
input.metadata.labels["app.kubernetes.io/version"]
input.metadata.labels["app.kubernetes.io/component"]
input.metadata.labels["app.kubernetes.io/part-of"]
input.metadata.labels["app.kubernetes.io/managed-by"]
}
workload_resources := ["Deployment", "StatefulSet"]
is_deployment_or_statefulset {
input.kind == workload_resources[_]
}
# recommented labels must exists
violation_labels_recommended_exists[{"msg": msg}] {
is_deployment_or_statefulset
not recommended_labels
msg = sprintf("推奨ラベルを付与してください。(https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels): [Kind=%s,Name=%s]", [input.kind, input.metadata.name])
}
これに対して、labelsがないテストとあるテストを用意すれば、実際のYAMLで確認することなくポリシーが妥当かユニットテストできる。
ポリシー違反しない場合は、not
を付けることで正常にとおったことをテストできる。
test_violation_labels_recommended_missing {
msg := "推奨ラベルを付与してください。(https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels): [Kind=Deployment,Name=test-data]"
input := {
"kind": "Deployment",
"metadata": {"name": "test-data"},
}
violation_labels_recommended_exists[{"msg": msg}] with input as input
}
test_violation_labels_recommended_exists {
msg := "推奨ラベルを付与してください。(https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels): [Kind=Deployment,Name=test-data]"
input := {
"kind": "Deployment",
"metadata": {
"name": "test-data",
"labels": {
"app.kubernetes.io/name": "",
"app.kubernetes.io/instance": "",
"app.kubernetes.io/version": "",
"app.kubernetes.io/component": "",
"app.kubernetes.io/part-of": "",
"app.kubernetes.io/managed-by": "",
},
},
}
not violation_labels_recommended_exists[{"msg": msg}] with input as input # 正常評価なら not を付ける
}
デバッガで止まるわけじゃないので、2つ使って評価を試す。
- Playground でポリシーとinput を用意して結果を見る
conftest verify --report failed
やconftest verify --trace
で評価経過を追いかける
ポリシーがそもそもおかしいかもしれない場合は、Playground で仮Input に対してポリシーがちゃんとかかっているか試行錯誤するのが手っ取り早い。なので、ポリシー取り合えず書いてみて、うまく当たるか見てみたい、という場合は Playground でサクッとやってみるのは結構オススメ。
プレイグラウンドでポリシーは当たったけど、どう評価されているか確認しながらやりたいだろう。そういうときは --trace
したり、--report
で見る。--trace
は、全部のテストの trace が出るので注意。(最後のテストになるようにするといい)
trace は正直読みにくいが、一行一行追っていけば、なるほど確かにという感じで評価されているのがわかるので、困ったら読む価値は十分ある。
traceを使う注意点が2つある。
- OR評価をすると trace ログが入れ子になってハイパー読みにくくなる。
- data に関数を逃がすとそれだけで入れ子になって読みにくい。
このため、まず試す、というときは決め打ちでOR評価せず、インラインでポリシーに直接パスを指定して書くほうが trace は圧倒的に追いやすい。
「なんでテストが通らないかわからないけど、traceが読みにくくて追いきれない」そんなときは、dataもOR評価もせず、決め打ちのinputに対するミニマムポリシーを用意してルールを直書きしてみるといい。
テストに引数がついていないだろうか。 私はこれで数時間溶かした。
# ダメな例
test_violation_labels_recommended_missing[msg] {}
# いい例
test_violation_labels_recommended_missing {}
考え方
Conftest で CI 時に Rego で記述したテストを行う - @amsy810's Blog
ポリシーサンプル
conftest/examples/kustomize/policy at master · open-policy-agent/conftest
GitHub - redhat-cop/rego-policies: Rego policies collection
GitHub - swade1987/deprek8ion: Rego policies to monitor Kubernetes APIs deprecations.
Collecting together Kubernetes rego examples, including porting the https://kubesec.io rules to rego