Open Policy Agent (OPA) でKubernetesにポリシーを強制する[Mutation]


前回は、Kubernetesにポリシーを強制するツールOpen Policy Agent (OPA)の概要を紹介しました。
その中で、OPAでKubernetesを制御するユースケースが2つあることを紹介しました。
前回検証した1つ目のユースケースValidationは、不正なリクエストを停止するというケースでした。
今回は、2つ目のユースケースであるMutationを紹介します。
また、Rego言語で記載したポリシーの内容を確認し、そのデバッグ方法を紹介します。

OPAの第二の役割: 自動修正を実現するMutation

Mutationは、「自動修正」の機能を提供します。
Mutationは、Validationにはない以下のような価値を提供します。

  • 【メリット1】 Kubernetesユーザの負担を軽減する
    • 例: 新規作成されたリソース(PodやServiceなど)にデフォルト値を自動的に挿入する。
  • 【メリット2】 面倒な環境依存の知識の習得を不要化する
    • 環境(Namespace)に応じて自動で適切に値を変更する。

イメージはこのような形です。

たとえば、所属部署が「ちゃんとユーザ名をuser: hiroosakiの形式でアノテーションに入れること」といったポリシーを設定していたとします。
このようなポリシーが1つ2つであれば、ルールをしっかり読んでユーザが守るということは容易かと思いますが、ポリシー数が数十という数まで増加したり、会社や階層的な部署がそれぞれのポリシーを定義したりすると、全部確認して適用するのは困難になっていきます。
OPAのMutationは、ユーザのリクエストが入力されたタイミングで、「どのポリシーを適用すべきかを検出し、ポリシーに従う形に自動的に更新する」ということが可能です。

文献は少ないですが、以下のMr. Torin Sandall氏のドキュメントが参考になります。

まずはMutationのデモ

まずMutationの動きを把握するため、「ある条件を満たすDeploymentに自動でfoo:barというラベルを追加する」という自動ラベル追加ポリシーの動作を検証します。

環境構築

Kubernetesがインストールされた環境を用意します。

OPAをHelmを使ってインストールしますが、そのための設定ファイルを以下のように作ります。

今回は、OPAがDeploymentを更新できるように設定ファイルのRBAC部分を少しだけ変更します。

value.yaml
admissionControllerKind: MutatingWebhookConfiguration
opa: null
mgmt:
  configmapPolicies:
    enabled: true
    namespaces: [opa, opa-example]
    requireLabel: true
  replicate:
    cluster:
    - v1/namespaces
    namespace:
    - extensions/v1beta1/ingresses
rbac:
  create: true
  rules:
    cluster:
    - apiGroups:
        - "*"
      resources:
        - configmaps
      verbs:
        - get
        - list
        - watch
        - patch
        - update
    - apiGroups:
      - "*"
      resources:
      - namespaces
      - ingresses
      verbs:
      - get
      - list
      - watch
    - apiGroups:
      - "*"
      resources:
      - deployments
      verbs:
      - get
      - list
      - watch
      - patch
      - update
sar:
  enabled: true
authz:
  enabled: false

インストール方法自体は前回と一緒で、helmコマンドを用いて一つだけチャートをインストールするだけで済みます。

kubectl create namespace opa
helm repo add stable https://kubernetes-charts.storage.googleapis.com
helm repo up
helm install opa stable/opa -f helm-values.yaml --namespace opa

ポリシーの追加

以下のようなRego言語のポリシーファイル「自動ラベル追加ポリシー」を作成し、OPAに読み込ませます。

mainという関数があって、自動ラベル追加が記載されています。詳しい内容は後段にて説明します。

main.rego
package system

main = {
    "apiVersion": "admission.k8s.io/v1beta1",
    "kind": "AdmissionReview",
    "response": {
        "allowed": true,
        "patchType": "JSONPatch",
        "patch": patch_bytes,
    }
} {
    is_create_or_update

    input.request.object.metadata.annotations["test-mutation"]

    patch = [
        {"op": "add", "path": "/metadata/annotations/foo", "value": "bar"},
    ]
    patch_json = json.marshal(patch)
    patch_bytes = base64url.encode(patch_json)
}

is_create_or_update { is_create }
is_create_or_update { is_update }
is_create { input.request.operation == "CREATE" }
is_update { input.request.operation == "UPDATE" }

このポリシーファイルをOPAに登録するには、KubernetesのConfigMapに格納し、OPAにポリシーとして認識させるためのアノテーションopenpolicyagent.org/policy=regoを付与します。これも前回同様です。

kubectl create  -n opa configmap policy --from-file ./main.rego
kubectl label -n opa configmap policy openpolicyagent.org/policy=rego

ポリシーによる自動修正の検証結果

それでは、テストのために一つのDeploymentを起動します。

最初はあえて何もポリシーが適用されていないまっさらなDeploymentを作ります。

kubectl run nginx --image nginx 
kubectl get deployment nginx -o json | jq '.metadata.labels'

上記のコマンドでラベルを確認すると、デフォルトで生成されたrunしか表示されません。

{
  "run": "nginx"
}

それでは、ポリシーを適用するきっかけを与えるために、専用のアノテーションを付けてみます。

kubectl annotate deployment nginx test-mutation=true 

test-mutationの存在を確認した自動ラベル追加ポリシーは、このDeploymentに新たなラベルを追加しているはずです。
先ほどのラベルをもう一度確認すると、以下のように新規ラベルfoo: barが自動で追加されていることがわかります。

kubectl get deployment nginx -o json | jq '.metadata.labels' 
{
  "foo": "bar",
  "run": "nginx"
}

このことから、OPAのポリシーが適用されて、Deploymentが自動修正され、ラベルが追加されたことが確認できました 👍

ポリシーの内容の解説(ラベル自動追加はどう実現されたか)

上記のデモで実現したラベル自動追加は、main.regoにどう記載されていたのかを以下のように理解できます。

ポリシーの構成

まず、main.regoは以下のような構成になっています。

package *** # パッケージの読み込み

main = 値 {
  条件
}

この値 {条件}という書き方は、ルールというものになります。一般的な関数とは少し違います。

公式ドキュメントBasicsでは、 rule-name IS value(値) IF body(条件)と理解する、と書いてあります。

mainというのは、ルールを実行した最終的な出力の名前になります。出力のJSONはこうなります。

{
  "main": 
}

ポリシー内部の記載

mainの中身を見ると、以下のように記載されており、前半は返り値、後半が条件を記載していると分かります。

main = { #ここから値
    "apiVersion": "admission.k8s.io/v1beta1",
    "kind": "AdmissionReview",
    "response": {
        "allowed": true,
        "patchType": "JSONPatch",
        "patch": patch_bytes,
    }
} #ここまで値
{   #ここから条件文
    is_create_or_update

    input.request.object.metadata.annotations["test-mutation"]

    patch = [
        {"op": "add", "path": "/metadata/annotations/foo", "value": "bar"},
    ]
    patch_json = json.marshal(patch)
    patch_bytes = base64url.encode(patch_json)
}   #ここまで条件文

まず、前半の値は { "apiVersion":.... }という形になっています。
これはOPAではなくKubernetesのDynamic Admission Controlの仕様が定義しているAdmissionReviewというデータ形式です。
Kubernetes公式ドキュメント記載→ Dynamic Admission Control と見比べると、そのままであることが分かります。

{
  "apiVersion": "admission.k8s.io/v1beta1",
  "kind": "AdmissionReview",
  "response": {
    "uid": "<value from request.uid>",
    "allowed": true,
    "patchType": "JSONPatch",
    "patch": "W3sib3AiOiAiYWRkIiwgInBhdGgiOiAiL3NwZWMvcmVwbGljYXMiLCAidmFsdWUiOiAzfV0="
  }
}

OPAがこのJSONの値を返すことによって、KubernetesはそれをDynamic Admission Controlの結果として読み込み、Deploymentなどの情報にパッチを当てているということが分かります。

次に、後半の条件文です。
Rego言語は、各行の結果がANDで結合されて解釈されるので、どれかの条件の結果がtrueでないなら何も値を返しません。
つまり、以下の条件文

    is_create_or_update
    input.request.object.metadata.annotations["test-mutation"]
    ...

は、このように解釈されます。

is_create_or_update
AND  input.request.object.metadata.annotations["test-mutation"]
AND  ...

したがって、以下の2つがいずれもtrueになったときだけ、値を返します。

  • 最初の条件文(CREATEUPDATEならtrue = Kubernetesに対するリクエストがDeployment作成か更新のどちらかであること)
  • 2つ目の条件文(アノテーションに"test-mutation"が含まれること)

条件文の最後3行は値を生成している部分です。Kubernetesの仕様通り、JSONPatch型で記載したパッチ内容を、BASE64エンコードしたものをpatch_bytesに挿入します。

    patch = [
        {"op": "add", "path": "/metadata/annotations/foo", "value": "bar"}, # パッチ内容
    ]
    patch_json = json.marshal(patch) # JSONマーシャル化
    patch_bytes = base64url.encode(patch_json) #BASE64エンコード

このpatch_bytesが最終的な値の一部に挿入されていることも分かります。

{ #ここから値
    "apiVersion": "admission.k8s.io/v1beta1",
    "kind": "AdmissionReview",
    "response": {
        "allowed": true,
        "patchType": "JSONPatch",
        "patch": patch_bytes, # <-- HERE
    }
} #ここまで値

これによって、条件に合致するときのみ、最終的にKubernetesにこのパッチ内容が送信される、という仕掛けになっているわけです。

Rego言語の振る舞いを手軽に確認する

上記のようなポリシーを自作しようとした場合に、どのようにデバッグするかが一つの課題になります。

解決策としては、実際のKubernetesの環境を生成しなくてもRego言語のデバッグができる、Rego Playgroundを利用できます。

左にさきほどのポリシーの内容をコピペし、右上に入力(Kubernetesから送信されるAdmissionReviewのRequestを模擬したJSONデータ)を入れると、右下のOUTPUTに返答が生成されます。

上記のデモをRego Playgroundで確認した結果が以下になります。

Rego Playground (saved)

もし構文が間違っていたら、以下のようにデバッグ結果が出力されます。

Kubernetes環境に適用する前にRego Playgroundでデバッグをしておくと、手戻りや予期せぬ事故を減らすことができます。

まとめ

OPAを使ったKubernetesへのポリシー強制の第二の方法として、Mutationを紹介しました。
簡単なデモとして、Deploymentが特定の条件を満たした場合に、自動的にラベルを追加するポリシーを作成し、Kubernetes上で実際にその動作を確認しました。
最後に、Rego言語というOPAのポリシー記述の方法を確認し、デバッグ方法としてRego Playgroundを紹介しました。

OPAを使うためには、ポリシーを学習するコストはかかると思います。しかし、OPAというツールがKubernetesへのプラグインとしての機能やRego言語のインタープリタをすべて包含していることから、構築準備は非常に簡単に済みます。したがって、ポリシーという利用者にとって最も大事なコンテンツを作成することに注力できるというメリットがあります。

ポリシーを書くためのRego言語のポイントは、また別の機会にまとめたいと思います。