OPA/Gatekeeperで特定条件にマッチするKuberentesリソースにMutationポリシーを適用する



本記事ではOPA/Gatekeeper (以下、Gatekeeper)を活用して特定条件にマッチするKuberentesリソースにMutationポリシーを適用する試験を行います。「[Kyvernoで特定条件にマッチするKuberentesリソースにMutationポリシーを適用する](https://qiita.com/yokawasa/items/6212ca86dd1e92f9dcd1)」のGatekeeper版です。

https://qiita.com/yokawasa/items/6212ca86dd1e92f9dcd1

# OPA/Gatekeeperとは?

GatekeeperはOpen Policy Agent (OPA)と呼ばれるポリシー定義のための標準化された仕様で書かれたルールをKubernetesで利用するためのCRDやk8sオペレーターを提供します。Gatekeeperは、Kyvernoと同様に[Admission Control](https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/)の仕組みを活用してKubernetes APIへのリクエスト処理をフックし、Kubernetesリソースの`Validation(検証)`や`Mutation(追加、更新、削除)`などの処理を追加できます。2022年7月現在、Gatekeeper v3.9.0が最新の[バージョン](https://github.com/open-policy-agent/gatekeeper/releases)となっています。

https://github.com/open-policy-agent/gatekeeper

## Mutationポリシー

[Mutation](https://open-policy-agent.github.io/gatekeeper/website/docs/mutation)は[v3.4.0](https://github.com/open-policy-agent/gatekeeper/releases/tag/v3.4.0)でようやくAlpha版がリリースされたGatekeeperにおいては比較的新しい機能になります。なお、2022年7月現在、最新のv3.9.0においてもベータリリース状態であるため、プロダクション環境での利用については十分に注意が必要といえるでしょう。

Mutaionポリシーの定義には次の3つのCRDが利用できます。

- **AssignMetadata**: リソースのmetadataセクションの変更を定義するCRD。metadataはとてもセンシティブであり変更により意図しない結果が生じる可能性があるため変更の自由度は制限されています。現時点で、AssignMetadataの変更は、`label`と`annotation`のみに制限されています。さらに、既存のlabelやannotationの変更できないのでご注意ください。
- **Assign**: リソースのmetadataセクション以外の変更を定義するCRD
- **ModifySet**: リストへのエントリーの追加や削除を定義するCRD。例えば、`spec.containers`の引数一覧にエントリーを追加・削除するとか。ModifySet CRDの[設定例](https://open-policy-agent.github.io/gatekeeper/website/docs/mutation/#modifyset)。

Mutation機能のサンプル集は[こちら](https://github.com/open-policy-agent/gatekeeper-library/tree/master/mutation/pod-security-policy)になります。

なお、本記事後半でAssignMetadataを活用したサンプルを紹介します。

## Validationポリシー

この記事の本題はMutationポリシーではありますが、Validationポリシーについて簡単に説明しておきます。

Validationポリシーには次の[ConstraintTemplate](https://open-policy-agent.github.io/gatekeeper/website/docs/constrainttemplates)とConstaint CRDを利用します。

- **ConstraintTemplate** 
    - ポリシーはConstraintとして定義しますが、そのConstraint CRDを定義するテンプレート
    - ポリシーの内容は[Rego言語](https://www.openpolicyagent.org/docs/latest/#rego)で記述しConstraintTemplateに埋め込みます
- **Constraint**
    - Constraintリソースを通じてGatekeeperはポリシーを実行します
    - Constraint CRDはConstraintTemplateで定義されます

実際のサンプルで`ConstraintTemplate`と`Constraint`を解説します。
これは、`K8sRequiredLabels`という名前のConstraint CRDを定義した`ConstraintTemplate`になります。Regoで必須ラベルポリシーのviolation（ポリシー違反）の定義をしています。また、`spec.crd.spec.validation.openAPIV3Schema`で関連するConstraintの`parameters`フィールドのスキーマ定義をしています。

```yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequiredlabels
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredLabels
      validation:
        # Schema for the `parameters` field
        openAPIV3Schema:
          type: object
          properties:
            labels:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredlabels

        violation[{"msg": msg, "details": {"missing_labels": missing}}] {
          provided := {label | input.review.object.metadata.labels[label]}
          required := {label | label := input.parameters.labels[_]}
          missing := required - provided
          count(missing) > 0
          msg := sprintf("you must provide labels: %v", [missing])
        }
```

次に、上記ConstraintTemplateで定義されたConstraint CRD、`K8sRequiredLabels`で、具体的にどのように
Gatekeeperはポリシーを実行するのかを定義したのが下記になります。これでGatekeeperがすべての名前空間で`gatekeeper`ラベルが設定されているか検査します。

```yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: ns-must-have-gk
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Namespace"]
  parameters:
    # Note that "labels" is now contained in an array item, rather than an object key under "parameters"
    - labels: ["gatekeeper"]
```

その他ConstraintTemplateとConstraintのサンプルについては[こちら](https://github.com/open-policy-agent/gatekeeper/tree/master/demo)を確認ください。



# 事前準備

それでは本題であるGatekeeperを活用して特定条件にマッチするKuberentesリソースにMutationポリシーを適用する試験を行っていきます。まずは、デプロイ先となるk8sクラスタとGatekeeperのインストールを行います。

## KINDでKubernetesクラスタ作成

k8sクラスタを[kind](https://github.com/kubernetes-sigs/kind) (Kubernetes in Docker)でローカルに構築します。
まずは、次のようなcontrol planeノード x 1、 workerノード x 1のk8sクラスタのためのkindクラスタ構成ファイル(cluster.yaml)を作成します。

```yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4 
nodes:
- role: control-plane
- role: worker
```

次に、kind cliコマンドで、k8s v1.21.10のノードイメージでk8sクラスタを作成します。

```bash
K8S_NODE_IMAGE=v1.21.10
kind create cluster --name my-kind-cluster \
--image=kindest/node:${K8S_NODE_IMAGE} \
--config cluster.yaml
```

クラスタ構築が完了したら念の為にクラスタにアクセスしてみます。

```bash
kubectl get node
NAME                            STATUS   ROLES    AGE   VERSION
my-kind-cluster-control-plane   Ready    master   37m   v1.21.10
my-kind-cluster-worker          Ready    <none>   37m   v1.21.10
```

## Gatekeeperのインストール

[こちらの手順](https://open-policy-agent.github.io/gatekeeper/website/docs/install)に従いGatekeeperをクラスタにインストールします。なお、他にもHelm chartからのインストールも可能です。

```bash
# Gatekeeper v3.8.xのインストール
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/release-3.8/deploy/gatekeeper.yaml

# 最新版のインストールはこちら
# 著者が2022/07時点で試したときはGatekeeperイメージ pullが失敗したためv3.8.xをインストール
# kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/master/deploy/gatekeeper.yaml
```

インストール後、gatekeeper-systemネームスペースに下記のようなPodが動いていることが確認できます。

```
kubectl get pods -n gatekeeper-system 

NAME                                             READY   STATUS    RESTARTS   AGE
gatekeeper-audit-84dd4759f5-584xg                1/1     Running   0          6m42s
gatekeeper-controller-manager-6548c75cf9-7kt9h   1/1     Running   0          6m42s
gatekeeper-controller-manager-6548c75cf9-m2bmw   1/1     Running   0          6m42s
gatekeeper-controller-manager-6548c75cf9-vrg9z   1/1     Running   0          6m42s
```

# 特定ネームスペース作成されるPodにラベルを追加する

いよいよ本題の試験を行います。GatekeeperのMutation用CRDの１つであるAssignMetadataをつかって特定ネームスペース作成されるPodにラベルを追加してみます。

## AssignMetadata (Mutationポリシー)をデプロイ

次のように、`set-label-policy`という名前のAssignMetadataリソースをクラスタにデプロイします。 このAssignMetadataリソースにはネームスペース`testns1`に作成されるPodに、`foo=bar`というラベルを追加するルールを定義しています。

```yaml
kubectl apply -f - << EOF
apiVersion: mutations.gatekeeper.sh/v1beta1
kind: AssignMetadata
metadata:
  name: set-label-policy
spec:
  match:
    scope: Namespaced
    kinds:
    - apiGroups: ["*"]
      kinds: ["Pod"]
    namespaces: ["testns1"]
  location: "metadata.labels.foo"
  parameters:
    assign:
      value: "bar"
EOF
```

なお、AssignMetadataの設定に関する詳細は[こちら](https://open-policy-agent.github.io/gatekeeper/website/docs/mutation/)のページが参照になります。


## サンプルPodをデプロイしポリシーが適用されることを確認する

まずは、テストで利用するネームスペース（`testns1`と`testns2`）を作成します。

```yaml
kubectl apply -f - << EOF
apiVersion: v1
kind: Namespace
metadata:
  name: testns1
---
apiVersion: v1
kind: Namespace
metadata:
  name: testns2
EOF
```

次に、サンプルアプリケーションのマニフェストをapp.yamlに出力します。

```yaml
cat << EOF > app.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: httpbin
---
apiVersion: v1
kind: Service
metadata:
  name: httpbin
  labels:
    app: httpbin
spec:
  ports:
  - name: http
    port: 8000
    targetPort: 80
  selector:
    app: httpbin
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: httpbin
spec:
  replicas: 1
  selector:
    matchLabels:
      app: httpbin
  template:
    metadata:
      labels:
        app: httpbin
    spec:
      serviceAccountName: httpbin
      containers:
      - image: docker.io/kennethreitz/httpbin
        imagePullPolicy: IfNotPresent
        name: httpbin
        command: ["gunicorn", "--access-logfile", "-", "-b", "0.0.0.0:80", "httpbin:app"]
        ports:
        - containerPort: 80
EOF
```

`testns1`と`testns2`の両ネームスペースにサンプルアプリケーションをデプロイします。

```
kubectl apply -f app.yaml -n testns1
kubectl apply -f app.yaml -n testns2
```

最後に、下記のように両ネームスペースにデプロイされたPodのラベルをチェックします。 

```
kubectl get po -n testns1 --show-labels

NAME                       READY   STATUS              RESTARTS   AGE   LABELS
httpbin-75bc8bcf78-7wlhv   0/1     ContainerCreating   0          5s    app=httpbin,foo=bar,pod-template-hash=75bc8bcf78

kubectl get pods -n testns2 --show-labels

NAME                       READY   STATUS              RESTARTS   AGE   LABELS
httpbin-75bc8bcf78-h9h22   0/1     ContainerCreating   0          3s    app=httpbin,pod-template-hash=75bc8bcf78
```

無事、AssignMetadata、`set-label-policy`のルールが適用され、testns1に作成されたPodに`foo=bar`というラベルを追加されていることがわかります。

# さいごに

[こちらの記事](https://qiita.com/yokawasa/items/6212ca86dd1e92f9dcd1)と本記事にてそれぞれKyvernoとGatekeeperのMutation機能を活用して特定条件にマッチするKuberentesリソースに変更を加える検証をしてみました。

両方使ってみた感想としては、簡単な処理であればどちらもさほど使用感は変わらないんじゃないかと（簡単な処理だからということは大いにあるが^^;）。Validation機能を含めて簡単に比較してみると、k8sネイティブのPolicy as CodeソリューションであるKyvernoのほうが直感的で学習カーブは低く、またMutationについては[Generateルール](https://kyverno.io/docs/writing-policies/generate/)などGatekeeperにはない有望な機能も充実していて、Gatekeeperより一歩進んでいる印象ではあります。一方、Validationについて不雑な検査をする場合はRegoによる記述が可能なGatekeeperに分があるかと思います。どちらをつかうかは要件次第ということになるかと思います。

さいごにGatekeeperのMutation機能について話を戻すと、先述のとおり2022年7月現在ベータリリース状態であり、リソースのmetadetaセクションの変更に利用するAssignMetadataについては`label`と`annotation`のみ（＋既存のlabelやannotationの変更はできない）という制限があります。[リリース頻度](https://github.com/open-policy-agent/gatekeeper/releases)も高いので今後の動向に注目しつつ、プロダクション環境で利用するには十分に注意を払う必要があるかとおもいます。