Kubernetesの面倒なことといえばYAML、というのはよく聞くし私も同意です。 YAMLが面倒なことはいくつもありますが、その1つに「YAMLの定義がポリシーとして正しいかの検証」があります。
コードを書いているときのように、YAMLもポリシーにあっているのかユニットテストができれば、いざデプロイするときまでわからないという事態は避けられそうですね?
ということで、今回はKubernetesでポリシーチェックをする方法として良くあげられるOpen Policy AgentのRego言語とContestを使っていくメモです。
なお、ポリシーチェックというとConftestやGatekeeperがありますが、個人的にはローカルで気軽に始められるConftestから慣れていくのがいいでしょう。試行錯誤しやすいところから入るのが大事。
概要
- Regoのポリシーは、それぞれに名前つけたほうがテスト書いていて指定したポリシーと明示できるのがいい。デバッグのためにも判別できる工夫がかかせない
- Regoのポリシー自体が正しいか試行錯誤することになるので、先にテスト書いたほうがいい。テストを書いてから、実際のYAMLに流すの流れが効率的
- Regoのテストで引数使うの避けている。テストが通らなくなってしんどい。ダメな例:
test_foo[msg] { }。いい例:test_foo { } - Regoのポリシーと結果をサクッと試すにはPlaygroundが便利
- 公式サイトが充実しており、何度も公式サイトをリファレンスとして使うことになるので積極的に使っていきましょう
https://www.openpolicyagent.org/docs/latest/policy-language/
Rego基本情報
基本的なコンセプト、構文、関数はここを参照してください。
拡張子は .regoです。 DSLでの記述と宣言的な実行に割り切っています。
全般的にpackage mainやsprintf("%s", string)を含めてGolangっぽさがそこかしこにあります。
2つ特徴があります。
- 代入を除くと、式は同じポリシー内でも順不同に配置しても問題なく評価される
- iteleratorを使うため
foo = input.keys[_]のように書くと、fooにはarray/set/mapの要素1が入ってくる
エディタ環境
VS Code + Remote Containerで書くのがおすすめです。
VS CodeにOpen Policy Agent拡張を入れると、フォーマッタが効くようになり、構文解析まで行ってくれます。おまけで、コマンドパレットでvalidateとかも出てVS Codeでプレイグラウンド的に使えますがそこまではいりません。
ただし、Open Policy Agent拡張を動かすには、Open Policy Agent実行ファイルopaをインストールする必要があるので、Remote Container環境と相性がいいです。
インテリセンスもないなど、IDEとして機能十分で使いやすいかというと微妙ですが、正しい構文とフォーマットが維持できるのでないより圧倒的にいいです。
実行方法
conftest + Open Policy Agentで実行すると隙がありません。
- CIやローカルで実行するときは、conftestを用いることでサクッと実行できて便利
- Kubernetes Cluster内部でデプロイされる前にチェックする(GitOpsは典型) 場合、Open Policy Agent | Kubernetes Admission Controlを用いる
まずはconftestで実行できるようにすると、動作が把握できてカジュアルに試せるのでいいです。 もっとカジュアルに試すならPlaygroundを活用するといいでしょう。
ポリシーの基本形式
ポリシーの基本は次の通りです。
- ポリシーは複数のルール(=式) を持つ
- ルールはboolを返し、AND評価(すべてのルールがtrueになるとポリシーに該当しているとみなされる)
- ポリシーの必須要素は、package宣言、violationなどのポリシーの評価方法、msgの3つ
- main package以外は実行時に評価されません。このため、別名フォルダにおいて隔離しておいてimportで取り込むことで、 data.パッケージ名として必要な関数を呼び出すことができる
package main
# 評価結果は、violation / deny / warn / allow で選べます。名前を付ける場合、先頭の文字がいずれかに該当していれば ok です
# violation と deny は ポリシーを満たすとダメ扱い。warn は警告扱い。BlackListでのポリシー評価が楽なので、allow は使いません
violation[msg] {
# boolを返すルールを複数記述できます。入力は、外部ライブラリを使わない限り input に入ってきます。
msg := "ポリシー該当時のメッセージを記述する"
}
violation_何か名前をつけたり[msg] {}
violation_引数をマップにしたり[{"msg": msg}] {
式や組み込み関数の基本的な記述は公式をみるといいでしょう。
Open Policy Agent | Policy Language Open Policy Agent | Policy Reference | Built-in Functions
比較
比較だけややこしいので特記しておきます。
同値の評価に:=と==と=演算子がありますが、基本的に代入に:=、値比較に==を使うように公式にも記述があるので従っておくのがいいでしょう。
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
]
}
OR評価
ポリシーは基本的にAND評価です。 OR評価したい場合は2つ方法があります。
- 同名の関数を用意する
- 配列に対して評価する
関数の場合、同名の関数を用意するとそれぞれの関数が個別に評価され、trueが返った関数を利用します。
例えば、次のようなis_byte_format関数を用意すると、Gi、Miいずれも評価できます。
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で使うにあたってポイントです。
評価対象リソースの限定
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 = "ポリシー違反です"
}
複数のラベルに対して「どれか1つでもない場合にエラーとする」ことを評価するときは、配列 + notを使います。これで、どれか1つでもラベルにないとポリシーがひっかかります。
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 を読みやすくする
traceを使う注意点が2つあります。
- OR評価をするとtraceログが入れ子になってハイパー読みにくい
- dataに関数を逃がすとそれだけで入れ子になって読みにくい
このため、まず試す、というときは決め打ちでOR評価せず、インラインでポリシーに直接パスを指定して書くほうがtraceは圧倒的に追いやすいです。
「なんでテストが通らないかわからないけど、traceが読みにくくて追いきれない」そんなときは、dataもOR評価せず、決め打ちのinputに対するミニマムポリシーを用意してルールを直書きしてみるといいでしょう。
traceを見てもポリシーはあってるがテストが通らない
テストに引数がついていないでしょうか。 私はこれで数時間溶かしました。
# ダメな例
test_violation_labels_recommended_missing[msg] {}
# いい例
test_violation_labels_recommended_missing {}
Reference
考え方
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
- 正直気持ちが悪いしデバッグがしにくい原因の1つです。↩