TT Lab
はじめる
学ぶ 学習パス コース

KCA — Kyverno認定アソシエイト

match に見えないものを preconditions と context が見る

TT Labで続きを見る

一言でいうと

match/excludeは、種類・名前・ラベルのような外見だけで選びます。リソースのspecの中を調べたり、別のリソースと比べたりして、ルールを適用するかどうかを決めるには、preconditionsとcontextが必要です。この記事では、その2つがどんな順序で評価され、どこまでできるのかを、公式ドキュメントのpreconditionsの節と外部データソースの節に沿って説明します。

なぜ必要なのか

「NodePort Serviceは必ずexternalTrafficPolicy: Localでなければならない」というポリシーを書くとしましょう。matchブロックはkinds: [Service]までしか選べず、spec.typeがNodePortかどうかは見られません。そのため、すべてのServiceがいったんルールに引っかかり、その中からNodePortだけを選び出す2つ目のゲートが必要になります。そのゲートがpreconditionsです。

2つ目の要求は、もっと頻繁に来ます。「このネームスペースにすでにPodがいくつあるか」「許可するレジストリのリストがConfigMapにあるので、それと比べたい」「リクエストした人が実際に権限を持っているかを、SubjectAccessReviewで問い合わせたい」。こうした判断は、リクエスト本体(AdmissionReview)だけではできず、別の場所からデータを取得しなければなりません。それがcontextです。

どう動くのか

評価の順序

ドキュメントは、順序を明確に書いています。リソースがmatchに引っかかり、excludeから外れたあとにpreconditionsが評価され、全体がTRUEのときに、ルール本体(validate・mutateなど)が実行されます。match/excludeの中では、変数を使えません。変数のドキュメントが、その理由を「データを読み込まずにルールを素早く選ぶため」だと明かしています。一方、preconditionsでは、変数・JMESPath・演算子をすべて使えます。

結果の報告でも、2つは違います。excludeに引っかかったリソースはまったく無視されますが、matchに引っかかってpreconditionsで落ちたリソースは、skipとして採点されます。PolicyReportでskipが多く見えるなら、preconditionsがふるい落としたものです。

anyとall

preconditionsの式は、anyまたはallブロックの下に置きます。anyは論理OR、allは論理ANDで、1つのルールに両方を置くこともできます。ドキュメントの表現のとおり、「各any/allブロックが全体としてTRUEでなければ」ルールが進行し、1つでもTRUEでなければ、ルールは適用されません。denyルールのconditionsと同じ構造で、同じ方式でショートサーキット評価(short circuiting)をします。

preconditions:
  any:
  - key: "{{ request.object.metadata.labels.color || '' }}"
    operator: Equals
    value: blue
  - key: "{{ request.object.metadata.labels.app || '' }}"
    operator: Equals
    value: busybox
  all:
  - key: "{{ request.object.metadata.labels.env || '' }}"
    operator: Equals
    value: qa

式1つは、key・operator・valueで構成されます。演算子は、Equals・NotEquals、GreaterThan・GreaterThanOrEquals・LessThan・LessThanOrEquals、集合の比較であるAnyIn・AllIn・AnyNotIn・AllNotIn、そして期間の比較であるDurationGreaterThan系列です。上の例の|| ''は、ラベルがないときにJMESPathの評価が失敗しないよう、空文字列で受け取る決まり文句で、省略可能なフィールドを扱うときには、ほぼ必ず必要です。

リクエストに由来する変数

Kyvernoがあらかじめ用意してくれる変数は、AdmissionReviewから来ます。request.objectは作成または変更されるオブジェクト(DELETEではnull)、request.oldObjectは変更される前のオブジェクト(CREATEではnull)、request.operationはCREATE・UPDATE・DELETE・CONNECTのいずれか、request.userInfoはusernameとgroups、request.namespaceは対象のネームスペースです。ここに、serviceAccountName・serviceAccountNamespace、request.roles・request.clusterRoles、コンテナイメージの情報を持つimagesが加わります。

1つ落とし穴があります。ユーザー名のようにアドミッションのリクエストにしかない変数をルールに使うなら、ポリシーのbackgroundをfalseにしなければなりません。バックグラウンドスキャンは、すでにあるリソースをもう一度走査するもので、リクエストの情報がないからです。CRDドキュメントのkubectl explainの出力に、その条件がそのまま書かれています。

context: 別の場所からデータを取得する

contextの項目は、ルールの中で定義し、定義した順序で評価されます。前の変数は後ろから参照できますが、後ろのものを前から参照するとエラーです。種類は5つです。

種類 何をするか 参照方法
configMap name・namespaceでConfigMapを読み取ります {{ 이름.data.키 }}
apiCall Kubernetes APIや外部サービスを呼び出します urlPath + jmesPath
globalReference あらかじめキャッシュされたGlobalContextEntryを参照します name
imageRegistry OCIイメージのメタデータを取得します reference + jmesPath
variable JMESPathで計算した値を保存します jmesPath + default

表の「参照方法」欄の1行目のプレースホルダーは、名前とキーです。

apiCallは、kubectl get --rawとまったく同じパスを使います。そのためドキュメントは、ポリシーに入れる前に、次のように手で試してみることを勧めています。

kubectl get --raw /api/v1/namespaces/kyverno/pods | kyverno jp query "items | length(@)"

同じものをポリシーに移すと、次のようになります。urlPathの中でも変数を使え、デフォルトのメソッドはGETで、method: POSTとdataを与えると、SubjectAccessReviewのような書き込みAPIも呼び出せます。APIサーバーがエラーを返す場合に備えて、defaultで代替値を置けます。

context:
- name: podCount
  apiCall:
    urlPath: "/api/v1/namespaces/{{ request.namespace }}/pods"
    jmesPath: "items | length(@)"
    default: 0

configMapは、ConfigMapにcache.kyverno.io/enabled: "true"ラベルを付けると、Kyvernoが自動的にキャッシュするので、ポリシーの判断のたびにAPIサーバーを呼び出さずに済みます。globalReferenceはさらに一歩進んだもので、GlobalContextEntryリソースにkubernetesResource(group・version・resource・namespace)やapiCall(外部呼び出し + refreshInterval)を宣言しておくと、Kyvernoがinformerでキャッシュを維持し、複数のポリシーが同じキャッシュを使います。ただし、GlobalContextEntryが準備できていなければ、それを参照するポリシーも準備できていない状態になり、処理されません。

権限も忘れてはいけません。apiCallであるリソースを読み取るには、KyvernoコントローラーのClusterRoleにその権限が必要で、インストールのカスタマイズのドキュメントは、rbac.kyverno.io/aggregate-to-admission-controller: "true"のようなラベルを付けたClusterRoleを作成して集約(aggregate)する方式を案内しています。1.13からワイルドカードのview権限が外れたため、カスタムリソースは明示的に開ける必要があります。

現場での姿

あるチームが「ネームスペースあたりのLoadBalancer Serviceは2つまで」というルールを入れました。matchでは数を数えられないので、apiCallでそのネームスペースのServiceの一覧を取得し、items[?spec.type == 'LoadBalancer'] | length(@)で数え、preconditionsのGreaterThanOrEqualsで2以上のときだけdenyするようにしました。最初は、アドミッションの遅延が目に見えて増えましたが、原因は、リクエストごとにAPI呼び出しが1回ずつ出ていたことでした。ドキュメントが述べているとおり、API呼び出しはアドミッションのリクエストごとに実行されるので、よく使うデータをGlobalContextEntryに移してキャッシュするのが答えでした。

別のチームは、ユーザー名をpreconditionsに入れたポリシーが、PolicyReportで変に見える現象に遭遇しました。バックグラウンドスキャンにはrequest.userInfoがないので、そのポリシーはbackground: falseでなければなりませんでした。

次の記事で続けること

続く記事では、Podルールが、DeploymentやCronJobのルールとして自動生成されるautogenと、ポリシーがリソースを削除するcleanupを扱います。そのあとのクイズでは、この記事の評価の順序、skipとexcludeの違い、contextの5つの種類を確認します。