Conftest で CI 時に Rego を用いたテストを行う
こんにちは。青山(@amsy810)です。 実は少しだけ PLAID さんでお手伝いをしており、CI に Conftest を組み込んで Kubernetes マニフェストのポリシーチェックを行うようにしたので、その時の備忘録を書いておきます。 PLAID さんでも GKE を基盤として選定して開発しています。
Conftest とは?
Conftest は Rego 言語で記述したポリシーを用いて、JSON や YAML などがポリシーに合致しているかをチェックする OSS です。 今回は Kubernetes のマニフェストがポリシーに合致しているかどうかを判別するために利用します。
例えば下記の例では、Deployment や StatefulSet などの Workloads リソースの Selector や起動してくる Pod のラベルに app ラベルが付与されるかをチェックしています。このように、比較的記述しやすい言語を利用してポリシーを定義していくことが可能です。
deny[msg] { input.kind == workload_resources[_] not (input.spec.selector.matchLabels.app == input.spec.template.metadata.labels.app) msg = sprintf("Pod Template 及び Selector には app ラベルを付与してください(spec.template.metadata.labels.app、spec.selector.matchLabels.app): [Resource=%s, Name=%s, Selector=%v, Labels=%v]", [input.kind, input.metadata.name, input.spec.selector.matchLabels, input.spec.template.metadata.labels]) }
ポリシーチェックの実行は下記のコマンドを実行するだけなため、容易に CI に組み込むことが可能です。
# サンプルは https://github.com/MasayaAoyama/conftest-demo に配置してあります conftest test --policy ./policy --input yaml manifests/*
Rego 言語については OPA の公式サイトを見てください。
Built-in Function については下記のソースコードも参考になります。
OpenPolicyAgent / Gatekeeper との違い
類似プロダクトとして、OpenPolicyAgent / Gatekeeper もありますね。 Gatekeeper を利用したポリシーチェックでは、Kubernetes にマニフェストが登録する際に AdmissionWebhook の ValidatingWebhook が実行されるタイミングで評価されます。そのため、Mutating されたあとの状態のチェックも行うことが可能です。
GitOps の場合はマニフェストリポジトリの状態がクラスタの状態と同じ状態になるように構成します。Gatekeeper を利用していて GitOps などを行っている場合には、マニフェストのリポジトリに対してコミットした後、実際にクラスタに反映する際に失敗して初めてポリシーに違反していることが確認できます。
一方で Conftest の場合には、CI の際にチェックすることが可能です。そのため、マニフェストが適切かどうかはリポジトリにコミットする前、すなわちマニフェストファイルに対する更新時にもチェックするようにしましょう。
Gatekeeper もクラスタへの登録時に安全に防ぐようにし、GitOps などを行っている場合には Conftest で CI でも前倒しして事前にチェックするように 2 段構えにすると良いと思います。
余談ですが、GCP の Anthos Config Management で提供されている Anthos Policy Controller も Gatekeeper を利用しているようです。 https://cloud.google.com/anthos-config-management/docs/concepts/policy-controller
設定しているポリシーについて
下記のようなポリシーを設定しています。一部は PodSecurityPolicy でも設定可能ですが、PSP も Gatekeeper 同様 Kubernetes API 登録時にチェックが行われるため、Conftest でもチェックすることに意味はあるでしょう。
- 単一リソース内でのチェック
- 特権コンテナの利用制限
- 各種リソースや Selector に特定のラベルが付与されているかのチェック
- コンテナのイメージタグの制限
- TerminationGracePeriodSeconds の設定時間が既定値以下かのチェック
- Resource Requests が設定されているかのチェック
- Limits/Requests の差のチェック
- Probe の failureThreshold などの設定値が既定値の範囲内かのチェック
- Inter-pod anti-affinity が設定されているかのチェック
- hostPath 利用時に Readonly になっているかのチェック
- Service の ClusterIP が静的に設定されていないかのチェック
- Ephemeral Storage の利用上限設定がされているかのチェック
- etc
- クラスタ全体での横断チェック
- ラベルがバッティングしていないか
- etc
Gatekeeper とのポリシーファイルの違い
Gatekeeper の場合には、現在既に Kubernetes に登録されているデータを data.inventory
から参照することが可能です。
クラスタオブジェクトの場合: data.inventory.cluster[<groupVersion>][<kind>][<name>] (例: data.inventory.cluster["v1"].Namespace["gatekeeper"]) Namespace オブジェクトの場合: data.inventory.namespace[<namespace>][groupVersion][<kind>][<name>] (例:data.inventory.namespace["gatekeeper"]["v1"]["Pod"]["gatekeeper-controller-manager-d4c98b788-j7d92"])
これを利用すると、下記のように既に登録されている Ingress リソースの host 名と衝突していないかといったチェックも行うことができます。
# https://github.com/open-policy-agent/gatekeeper/blob/master/library/general/uniqueingresshost/src.rego package k8suniqueingresshost identical(obj, review) { obj.metadata.namespace == review.object.metadata.namespace obj.metadata.name == review.object.metadata.name } violation[{"msg": msg}] { input.review.kind.kind == "Ingress" re_match("^(extensions|networking.k8s.io)$", input.review.kind.group) host := input.review.object.spec.rules[_].host # 既にクラスタに登録されているデータを参照 other := data.inventory.namespace[ns][otherapiversion]["Ingress"][name] re_match("^(extensions|networking.k8s.io)/.+$", otherapiversion) other.spec.rules[_].host == host not identical(other, input.review) msg := sprintf("ingress host conflicts with an existing ingress <%v>", [host]) }
Conftest による横断のチェック
さて、本ブログ記事で一番お伝えしておきたい内容です。 実際マイクロサービスが複雑化してくると、Conftest の段階で「Deployment に対して PodDisruptionBudget が設定されているか」「Service の Selector にマッチする Deployment 等が存在するか」「横断でラベルがバッティングしていないかのチェック」といった横断的なチェックを機械的に行わなければ予期せぬ障害が発生するかもしれません。
Conftest でも --combine
オプションをつけることで複数のファイルをロードして横断でチェックすることが可能です。
conftest test --policy ./policy --input yaml ./manifests/* --combine
このとき、conftest にはリソース自体が入ってくるのではなく、「ファイル」「リソース」の配列で入ってくるため注意してください。なお、ひとつのファイルに単一リソースしか書かれていない場合は、「ファイル」の配列の後にそのままリソースが入ってきてしまい、ファイルによって構造が変わってきてしまいます。そのため、一つのファイルに単一のリソースしかない場合でも ---
を 1 つ目のリソースの後に明示的に記載するようにしましょう。
deny[msg] { files := input[_] resources := files[_] other_files := input[_] other_resources := other_files[_] resources.kind == workload_resources[_] other_resources.kind == workload_resources[_] resources.spec.template.metadata.labels == other_resources.spec.template.metadata.labels resources.metadata.name != other_resources.metadata.name msg = sprintf("リソースのラベルが衝突しています: [%s/%s <=> %s/%s]", [resources.kind, resources.metadata.name, other_resources.kind, other_resources.metadata.name]) }
まとめ
Conftest は非常に手軽に CI にマニフェストのチェックを導入できるためおすすめです。また、OPA のポリシーファイルを使い回しやすいため、OPA と合わせて利用する際もそこまでコストを掛けずに利用することが可能です。ぜひセキュリティや組織ポリシーの徹底のためにも、OPA / Gatekeeper と合わせて Conftest の利用を検討してみてください。
今回利用したサンプルファイル類は下記に上げています。