NIKKEI TECHNOLOGY AND CAREER

Gatekeeper/conftestのRegoをDRYに管理する

はじめまして、情報サービスユニットの鶴田 @dulltz です。これは Nikkei Advent Calendar 2020 12月24日の記事です。

Gatekeeper と conftest

Kubernetes 管理者のみなさん、 Gatekeeper と conftest は使っていますか?

使ってない方のために軽く説明しておくと、Gatekeeperconftest はどちらも OpenPolicyAgent というポリシーエンジンのファミリーです。 どちらも Kubernetes クラスタへの不適切なリソースの作成防止に使えるツールです。Gatekeeper は admission webhook としてポリシーを施行するツールで、conftest は CI などで動かすのに丁度いい CLI ツールです。(正確に言うと conftest は Kubernetes 以外にも使えます) どちらも Rego という言語でポリシーを記述することになります。

似たようなことをするなら使うのはどちらか片方で良くない?という意見もあるかもしれませんが、 conftest だけだと GitOps システムを通さない無作法な kubectl apply や、プログラムによる動的なリソース作成をカバーできず、 Gatekeeper だけだといざクラスタに apply される段になるまでバリデーションチェックできないので、GitOps でマージされた PR をリバートする羽目になったりします。 なので自分は conftest と Gatekeeper は両方使って同じポリシーを施行しておくのがベターだと思っています。

しかし、Gatekeeper / conftest で書くコードは似ているにも関わらず、インターフェースに微妙な差異があるんですよね…。 このせいで、共通化について特に考えずポリシーをがりがり書いていくとほぼ同じ Rego コードを二重管理することになってしまいがちです。

更に言うと Gatekeeper では ConstraintTemplate というカスタムリソースに埋め込む形で Rego を読み込ませるのですが、ConstraintTemplate に Rego を埋め込んだ状態で管理すると conftest verifyopa testコマンドでユニットテストを実行できません。Rego から ConstraintTemplate を生成するツールは公式提供されていないので、特に策を講じなければ手動でコピペすることになります。Rego コード内でライブラリを import している場合は依存ライブラリ側の変更のたびに ConstraintTemplate を更新する必要があるので、この作業が手作業だと後々辛くなってきます。

これらの問題はどのように解決するのが良いと思いますか? おそらく多くの人が思う理想的なやり方は、conftest で動く Rego コードを書いたら、それを元に Gatekeeper 用カスタムリソースが生成される仕組みを作ることではないでしょうか。そしてまさにこのやり方で Rego 管理を行うための OSS が存在します。

Konstraint

prexsystems/konstraint は Gatekeeper の Rego コードを効率的に管理するための CLI ツールです。 これを使うと Rego コードから Gatekeeper 用カスタムリソースを生成することができます。

さらに konstraint のリポジトリ内で conftest と Gatekeeper の差異を吸収するための Rego ライブラリも提供されています。

これら konstraint CLI ツールとライブラリを組み合わせると、 conftest で動く Rego コードから Gatekeeper のカスタムリソースを作れるようになります。

ハンズオン

普通の conftest 用 Regoコードを元に、Konstraint を用いて Gatekeeper と conftest 双方に使えるようにする手順を説明します。

準備

v1.14 以上の Kubernetes を準備した上で、Gatekeeper v3.2 をインストールしてください。今回の記事では v1.18.9 の Kubernetes を使用します。

$ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/release-3.2/deploy/gatekeeper.yaml

conftest 用 Rego コード例

例として次の conftest 用 Rego コードを Gatekeeper でも使えるようにしていきます。

src.rego

package run_as_non_root

deny[msg] {
  target := ["Deployment", "StatefulSet"]
  input.kind == target[_]
  not input.spec.template.spec.securityContext.runAsNonRoot

  msg := "Containers must not run as root"
}

conftest でライブラリを取得

まずは Rego ライブラリを pull します。

$ conftest pull github.com/plexsystems/konstraint/examples/lib -p lib

lib ディレクトリに各種 Rego ファイルが追加されます。

$ ls -1 lib
core.rego
core_test.rego
measurements.rego
pods.rego
pods_test.rego
policies
psp.rego
psp_test.rego
rbac.rego
rbac_test.rego
security.rego
security_test.rego

ディレクトリを分割

次に ConstraintTemplate カスタムリソースにしたい粒度でディレクトリを分けます。これは、konstraint がディレクトリ名を ConstraintTemplate の名前に使うためです。

$ mkdir container-run-as-non-root
$ mv src.rego container-run-as-non-root

Gatekeeper 互換となるよう改修

次に Rego ライブラリを用いて前述の src.rego が Gatekeeper と互換性を持つように書き換えます。

package run_as_non_root

import data.lib.core

violation[{"msg": msg}] {
  target := ["Deployment", "StatefulSet"]
  core.kind == target[_]
  not core.resource.spec.template.spec.securityContext.runAsNonRoot

  msg := "Containers must not run as root"
}

↑の改修ポイントは以下のとおりです。

  • input を直指定する代わりに data.lib.core に定義されている resource を使いました。これは Gatekeeper と conftest の input の構造の差異を吸収してくれるライブラリです。実装は薄いので中を読むとすぐわかるのですが、 input.review.object の有無を元に動作環境が Gatekeeper なのか conftest なのか判定しています。なお今回の例では data.lib.pods を使うほうがベターなのですが、例としてのわかりやすさを重視して data.lib.core を使っています。
  • deny[msg]violation[{"msg": msg}] に置き換えました。Gatekeeper は msg を持つ violation をバリデーションに使います。

ユニットテスト

ついでにテスト src_test.rego も用意します。

package run_as_non_root

test_invalid_run_as_non_root {
    input := {
        "kind": "Deployment",
        "spec": {
            "template": {
                "spec": {
                    "securityContext": {}
                }
            }
        }
    }
    violation[{"msg": "Containers must not run as root"}] with input as input
}

test_valid_run_as_non_root {
    input := {
        "kind": "Deployment",
        "spec": {
            "template": {
                "spec": {
                    "securityContext": {
                        "runAsNonRoot": true
                    }
                }
            }
        }
    }
    not violation[{"msg": "Containers must not run as root"}] with input as input
}

ユニットテストを実行するとテストケース数が多く表示されますが、これは lib/ の下にもテストコードがあるからです。

$ ls
container-run-as-non-root       lib

$ conftest verify -p .

31 tests, 31 passed, 0 warnings, 0 failures, 0 exceptions

conftest test を実行するときは、input を単一の resource に対応させたいので --combine オプションはつけないで下さい。

$ cat deployment.yaml | conftest test -p . --all-namespaces -
FAIL - Containers must not run as root

1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions

konstraint のためのアノテーションコメントを追加

次は src.rego に konstraint のためのアノテーションとなるコメント行を追加します。

# @title Containers must run as root
# @kinds apps/Deployment apps/StatefulSet
package run_as_non_root
...

@title はドキュメント生成コマンド実行時に使用されます。@kinds はポリシーを実施したい kind を指定するためのアノテーションで、カスタムリソース生成時・ドキュメント生成時いずれでも使われます。

他にも @parameters @matchlabels などのアノテーションがあります。ちなみに @matchlabels は自分が実装しました。これはただの自慢です。

カスタムリソースとドキュメントを生成

ここまでの編集で、実際に konstraint で Gatekeeper のカスタムリソースを生成できるようになっています。 実行すると以下のようにファイルが生成されます。

$ konstraint create .

$ git status -s
M  container-run-as-non-root/src.rego
?? container-run-as-non-root/constraint.yaml
?? container-run-as-non-root/template.yaml

template.yaml には ConstraintTemplate が記されており、 constraint.yaml には ContainerRunAsNonRoot カスタムリソースが記されています。ContainerRunAsNonRoot は ConstraintTemplate 作成時に Gatekeeper が動的に作成する CRD です。 この CRD の名前はポリシーを配置したディレクトリの名前から決定されます。

また、konstraint はカスタムリソースだけでなくポリシー内容を自然言語で記述した policies.md を生成できます。

$ konstraint doc .

data.parameters が受理されない問題のワークアラウンド

YAML を Gatekeeper に apply する前に、あと少しやることがあります。 というのも実は今のままだと kubectl apply が通りません。

$ kubectl apply -f container-run-as-non-root/template.yaml
Error from server (check refs failed on module {libs.admission.k8s.gatekeeper.sh.ContainerRunAsNonRoot["lib_0"]}: disallowed ref data.parameters): error when creating "container-run-as-non-root/template.yaml": admission webhook "validation.gatekeeper.sh" denied the request: check refs failed on module {libs.admission.k8s.gatekeeper.sh.ContainerRunAsNonRoot["lib_0"]}: disallowed ref data.parameters

なぜ弾かれるのかというと、konstraint は互換性のために Gatekeeper の parameters と同等のデータを conftest の --data を使って再現可能にしているのですが、 OpenPolicyAgent が data.lib から始まらない data を静的解析で弾くせいで、data.parameters というトークンが現れるコードが ConstraintTemplate に埋め込まれている状態では Gatekeeper に弾かれてしまうからです。

これを回避するために、lib から data.parameters に関する記述を消してしまいます。

--- a/lib/core.rego
+++ b/lib/core.rego
@@ -44,10 +44,6 @@ parameters = input.parameters {
     is_gatekeeper
 }

-parameters = data.parameters {
-   not is_gatekeeper
-}
-
 has_field(obj, field) {
     not object.get(obj, field, "N_DEFINED") == "N_DEFINED"
 }

ConstraintTemplate を再作成します。今度は apply が成功します。

$ konstraint create .
$ kubectl apply -f container-run-as-non-root/template.yaml
constrainttemplate.templates.gatekeeper.sh/containerrunasnonroot created

ちなみにもし conftest で data.parameters を使う場合は、lib からではなく、生成された後の ConstraintTemplate から該当行を逐次消して下さい。

data.parameters 問題について詳しくはこちらを御覧ください。https://github.com/plexsystems/konstraint/issues/86

Gatekeeper 用カスタムリソースを適用

ContainerRunAsNonRoot を改めて apply し、 所望の validating webhook が実施されるようになったか確認します。 ContainerRunAsNonRoot は ConstraintTemplate が apply されたあとに CRD が作られるので、template.yaml を constraint.yaml より先に apply する順序は守って下さい。

$ kubectl apply -f container-run-as-non-root/constraint.yaml
containerrunasnonroot.constraints.gatekeeper.sh/containerrunasnonroot created

ここで適当な deployment を apply してみます。spec.template.spec.securityContext.runAsNonRoottrue にしない限り以下のような出力になります。src.rego に記述した validating admission webhook が機能していることが確認できました。

$ cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test
spec:
  selector:
    matchLabels:
      app: test
  template:
    metadata:
      labels:
        app: test
    spec:
      containers:
      - image: nginx
        name: nginx
        resources: {}
EOF

Error from server ([denied by containerrunasnonroot] Containers must not run as root): error when creating "STDIN": admission webhook "validation.gatekeeper.sh" denied the request: [denied by containerrunasnonroot] Containers must not run as root

クリーンアップ

$ kubectl delete deployment test
$ kubectl delete -f container-run-as-non-root/constraint.yaml
$ kubectl delete -f container-run-as-non-root/template.yaml
$ kubectl delete -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/release-3.2/deploy/gatekeeper.yaml

運用のための小ネタメモ

konstraint 運用にまつわる小ネタを紹介します。

  • 生成コマンドの実行忘れが起きそうなので、 CI の中で konstraint createkonstraint doc を実行し、もし git diff --quiet が1を返したら CI を落とすようにしています。
  • template.yaml を適用してからでないと constraint.yaml の適用は失敗するので、それぞれ別の kustomization.yaml で管理し、Flux2 の Kustomization.spec.dependsOn で順序を制御しています。
  • 複数の ConstraintTemplate を同時に生成するとき konstraint は constraint_<name>.yaml template_<name>.yaml という名前のファイルをそれぞれ作るのですが、これらを kustomization.yaml に追記するときは kustomize edit add resource を使うと、冪等に kustomization.yaml を更新でき、さらにターゲットとなるファイル名を glob 指定できるので便利です。
  • 制限事項になりますが、conftest では --combine オプションで複数 k8s リソースを input とするようなポリシーを書くことがありますが、これを ConstraintTemplate に変換するのは難しいです。逆に Gatekeeper の Config リソースから得られる cache inventory を使ったポリシーを conftest に持ってくるのは難しいでしょう。

まとめ

konstraint で conftest/Gatekeeper 両対応の Rego 管理を行う方法を説明しました。

conftest をしばらく運用したあと Gatekeeper も入れたくなるのはよくあることだと思うので、似たような課題感を抱く方は多いのではないでしょうか? 今後より良い管理方法が登場する可能性はありますが、とりあえず現時点ではこのやり方で Rego を管理しています。もしもっと良いやり方があれば教えて下さい。

鶴田貴大
ENGINEER鶴田貴大

Entry

各種エントリーはこちらから

キャリア採用
Entry
新卒採用
Entry
カジュアル面談
Entry