conftest学んで試してみた
記事一覧はこちら
要約
conftestというOPAのツールを使って、k8sのdeploymentをテストしてみた話です。 OPAとは?というところから、その中で利用するRego言語についても軽くまとめてみました。
Conftestとは
構造化された設定データに対してテストを書くのに役立つツールとのことです。
例えば、KubernetesのマニフェストやTerraformのコードをターゲットにテストを書くことが可能なようです。僕は配信で他社さんが使っていることを発表していて知りました。
Conftestはポリシーの宣言にOpen Policy AgentのRego言語を利用します。
Open Policy Agentとは
OPAはスタック全体でのポリシー施行を統一する、OSSのポリシーエンジンとのこと(参考)。発音は"oh-pa"になる。 OPAはポリシーをコードで書ける宣言型言語とポリシーの決定をソフトウェアからオフロードするシンプルなAPIを提供してくれるもの。
例えば、マイクロサービスかつpolyglotで実装していると、色んな言語でそれぞれポリシーを書かなきゃいけなくなるけど、それを避けられるというという認識を僕はしました。
下記の図は公式サイトより引用させていただきました。
OPAの実行方法はとしては下記のようなものがある(参考)。
Conftestの go.mod
を見る限り、ConftestはOPAのGoのライブラリをよんでポリシーの評価を行っているようです。
OPA Document Modelについて
外部からOPAにデータをロードされる全てのデータを Base Document
と呼びます。また、OPAではルールによって生成された値を Virtual Document
と呼びます。
これら2つはRegoで、同じ dot/bracket-style
で参照することができます。 data
というグローバル変数を使って両方にアクセスすることが出来ます。
Base Document
はOPAの外部から来るものなので、 data
配下のどこに置かれるかどうかは読み込むソフトウェアによって制御されます。その一方で、 Virtual Document
の data
配下の位置は package
宣言によって制御されます。
ポリシー決定のためにOPAに問い合わせを行う際には、非同期 / 同期の2つの方法があります。
conftestは後者です。同期的にpushされた Base Document
は "input" というグローバル変数の下にぶら下がります。ポリシーではこれらにアクセスすることができます。
Rego Languageとは
公式リファレンスはこちら。 プレイグラウンドも用意されている。
Datalogという言語にインスパイアされた言語らしいです。Datalogを拡張してJSONのような構造化ドキュメントをサポートします。
Scalar値
主に複数の場所から参照される定数を定義するために利用する構文です Strings, numbers, booleans, null値をサポートするそうな。バッククォートで囲めば、escape sequencesが解釈されない生の文字列として扱うことが可能です
greeting := "Hello" raw_greeting := `Hello\t` max_height := 42 pi := 3.14159 allowed := true location := null
複合値
こちらも定数定義で利用されることが主な構文。 ObjectはKey-Value形式で、Keyはどの型も許容する。 SetもRegoはサポートする。なお、JSONとして出力された際には、配列として表現される(JSONにSetの概念が無いため)。
# Object point_p := {"x": 14, "y": 4, "z": 27} port := {80: "http", 443: "https"} # port[80] のようにフィールドにアクセス # Set s := {point_p.x, point_p.y, point.z} s == {4, 27, 14} # true 順序が違ってもOK!
変数
ルールの先頭に現れる変数(例えば deny[msg]
の msg
)は、ルールの入力と出力を兼ねます。Regoでは多くのプログラミング言語と違い、入力と出力を同時に持ちます。
値がバインドされていない変数の場合は出力となるし、そうでなければ出力になります。
参照
参照はネストされたドキュメントで利用されます。 一般的には"dot-access"で表現しますが、canonicalなものとしてはdotを排除したPythonのdictionary lookupに近い形になるそうな どちらの形式も有効ですが、下記4つのケースではcanonical formを利用する必要があります。
- [a-z], [A-Z], [0-9], _ (underscore) 以外の文字を含む文字列キー
- numbers, booleans, null などの文字列以外のキー
- 変数キー
- 複合キー
# dot-access sites[0].servers[1].hostname # canonical form sites[0]["servers"][1]["hostname"] # variable key # indexがi,jとバインディングされつつ、ループしてすべての要素を列挙する sites[i].servers[j].hostname sites[_].servers[_].hostname # i, jを利用する必要がない場合には _ を利用する # composite key s[[1, 2]]
Module
Regoにおいて、policyはModuleの内部で宣言されるものです。 Moduleは以下の3つから構成されるものです。
- 1つのPackage宣言
- 0個以上のImport文
- 0個以上のRule定義
Packageは1個以上のModuleで定義されたRuleを特定の名前空間にグループ化します。 同じPackageのモジュールは同じディレクトリに配置しなくてもよい。 Moduleで定義されたRuleは自動的にエクスポートされる。
Import文はModuleがPackage外で定義されたドキュメントへの依存関係を宣言するものです。これでエクスポートされた識別子を参照することができるようになる。
全てのModuleは data
input
ドキュメントの暗黙のImport文を含みます。
OPA Document Modelで述べましたが、 data
に他のModuleはぶら下がることになりますので、 package servers
と宣言したModuleにアクセスする時のimport文は下記のようになります。
package opa.examples import data.servers http_servers[server] { server := servers[_] server.protocols[_] == "http" }
Operators
結構大事なのに、ドキュメントの下の方にあって拾えてなくて苦労した項目 こちらを参照ください ベストプラクティスとしては、なるべくassignmentとcomparisonを利用したほうが良いとのことです
# assignment(:=) はローカル変数の宣言 x := 100 # comparison(==) は比較 x == 100 # Unification(:=) はassignmentとcomparisonの組み合わせで、比較を真にする値を代入します。 input.kind = "Service"
Rule
ルールを評価するときにOPAは全ての式が真になるかどうかを見ます 下記のように理解することができます
expression-1 AND expression-2 AND ... AND expression-N
Ruleは同名で定義することが可能で、その場合の評価は下記のように理解できます
<rule-1> OR <rule-2> OR ... OR <rule-N>
インストール方法
https://www.conftest.dev/install/#brew
# Macの場合 $ brew tap instrumenta/instrumenta $ brew install conftest $ conftest -v Version: 0.23.0 Commit: 6190ded Date: 2021-01-09T10:26:15Z # DockerHubにもある(Officialマークは無いけど) # https://hub.docker.com/r/openpolicyagent/conftest docker pull penpolicyagent/conftest
実行方法
デフォルトではconftest
コマンドの実行されたディレクトリ直下ののpolicyディレクトリを見に行きます(該当コード)。さらに.rego
ファイルの中で、デフォルトではmain
パッケージのruleを見に来ます(該当コード)。これは—-policy
フラグと--namespace
フラグで上書きすることができます。
$ pwd .../conftest/examples/kubernetes $ ll total 24 -rw-r--r-- 1 44smkn staff 546B 2 12 11:42 deployment+service.yaml -rw-r--r-- 1 44smkn staff 576B 2 12 11:42 deployment.yaml drwxr-xr-x 8 44smkn staff 256B 2 12 11:42 policy -rw-r--r-- 1 44smkn staff 172B 2 12 11:42 service.yaml $ conftest test deployment.yaml FAIL - deployment.yaml - main - Containers must not run as root in Deployment hello-kubernetes FAIL - deployment.yaml - main - Deployment hello-kubernetes must provide app/release labels for pod selectors FAIL - deployment.yaml - main - hello-kubernetes must include Kubernetes recommended labels: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels FAIL - deployment.yaml - main - Found deployment hello-kubernetes but deployments are not allowed 5 tests, 1 passed, 0 warnings, 4 failures, 0 exceptions
Conftestが認識するルール
Conftestが探しに行くルールは、deny, violation, warn です(該当コード)。それぞれのルールには、suffixにunderscoreとidentifierをくっつけることができます。例えば、deny_myrule
のように記述できる。
violation
はdeny
ルールと同じように評価されるが、文字列だけでなく構造体のエラーを返すことができる。
ConftestでKubernetesのマニフェストをテストする
試しにk8sのマニフェストに自分なりのポリシーを用意してテストしてみようと思います。k8sのマニフェストはkustomizeを利用して定義します。
kustomize build | conftest test -
のようにパイプで渡してもconftestを流すことができます(該当コード)
Deploymentにポリシーを設定してみる
policyは下記のような感じ
package main name := input.metadata.name # deployment かつ Root以外のユーザで起動していなければ 拒否する # not -> https://www.openpolicyagent.org/docs/latest/policy-language/#negation deny[msg] { input.kind == "Deployment" not input.spec.template.spec.securityContext.runAsNonRoot msg := sprintf("Containers must not run as root in Deployment %s", [name]) } # deployment かつ タグ付けされていないコンテナイメージを利用している場合は拒否する deny[msg] { input.kind == "Deployment" image := input.spec.template.spec.containers[_].image not contains(image, ":") msg := sprintf("You must use tagged container images in Deployment %s", [name]) }
コマンドで実行するとこう
$ kustomize build overlays/production | conftest test - FAIL - - main - Containers must not run as root in Deployment production-prod-sbdemo FAIL - - main - You must use tagged container images in Deployment production-prod-sbdemo 6 tests, 4 passed, 0 warnings, 2 failures, 0 exceptions
想定通りになりました!Regoは結構深ぼっていくと面白そうですね(ちょっと理解するのに時間がかかりました)。
以上!