validate — パターン、deny、そしてマッチの範囲
一言でいうと
検証ポリシーの品質は、条件式ではなく、マッチ範囲と失敗メッセージで決まります。
なぜ必要なのか
ポリシーをはじめて書く人は、条件をどう表現するかから悩みます。しかし、実際に事故を起こすのは、条件ではありません。条件が間違っていればテストで見つかりますが、マッチ範囲が間違っていると、何も起きないため、誰も気づきません。クラスター全体にかけたポリシーがkube-systemのシステムPodをブロックして、ノードの起動を妨げたり、逆にネームスペースを1つ抜かして、そこだけがルールの外にある状態が、何か月も続いたりします。
そのため、検証ポリシーを書く順序は、条件ではなく範囲からです。どの種類(kinds)、どのネームスペース(namespaces)、どのラベル(selector)にかけるのか。そして、必ずセットになる質問があります。何を除外するのか(exclude)です。クラスターコンポーネント、ポリシーエンジン自身、そしてまだ整理されていないレガシーのネームスペースは、最初から除外しておくほうがよいです。
どう動くのか
検証ルールには、条件を表現する方式がいくつかあり、表現力と読みやすさは反比例します。
| 方式 | 使う場面 | 性格 |
|---|---|---|
validate.pattern |
フィールドが存在する必要がある / 値が特定の形である必要がある | オブジェクトと同じ形で書くため、読みやすい |
validate.deny.conditions |
リストの比較、文字列のマッチなど、パターンでは書けない条件 | 明示的な拒否。anyは1つでも真、allはすべて真 |
validate.cel |
計算が必要な条件 | 表現力は高いが、チーム全体が読める必要がある |
validate.foreach |
コンテナのように、リストの各項目ごと | 項目ごとに判定し、メッセージも項目ごとに出す |
パターンには、演算子が付きます。?*は「空でない任意の値」、*は「任意の値(なくてもよい)」、X|Yは択一、!Xは否定、>=256Miのような数値比較もできます。metadata.labelsの下にteam: "?*"と書くと、「teamラベルが存在し、空でない」という意味になります。
アンカーは、パターンの意味を変える接頭辞です。条件アンカー()は「この値が合っているときだけ、下を検査する」、等価アンカー=()は「このキーがあれば、値が同じでなければならない」、否定アンカーX()は「このキーがあってはならない」です。特に、否定アンカーを値の比較と勘違いするミスが多いです。X(privileged)は、privilegedがfalseでなければならないという意味ではなく、そのキーが存在してはいけないという意味です。
preconditionsとdeny.conditionsは、形が似ているため、よく混同されます。違いは結果ではなく、評価されるかどうかです。preconditionsが偽なら、そのルールはそもそも実行されずに飛ばされます(skip)。deny.conditionsが真なら、ルールは実行されており、その結果が拒否です。レポートに残る姿も違います。前者は痕跡がなく、後者はfailとして残ります。そのため、「このルールはCREATEリクエストにだけ適用」のようなものはpreconditionsで、「このイメージはだめ」はdenyで書きます。
最後に、強制レベルです。以前は、ポリシーレベルのspec.validationFailureAction1つが、ポリシー内のすべてのルールを支配していました。今は、ルール単位のvalidate.failureActionへ移行しつつあり、値はEnforce(違反リクエストをブロック)とAudit(通過させるが、レポートに残す)の2種類です。ルール単位になったことで、1つのポリシーの中で、あるルールはすでに強制し、あるルールはまだ観察だけ、という状態が可能になりました。ポリシーを分割しなくても、新しいルールを静かに載せて、数日様子を見られるという意味です。
現場での姿
1つ目は、メッセージがポリシーの半分であることです。拒否される人は、ポリシーのYAMLを読めません。その人が見るのは、kubectl applyが出力した1行だけです。「Validation error」のようなメッセージは問い合わせを生み、「Podにはteamラベルが必要です。所有チームを書いてください」のようなメッセージは、自分で直させます。メッセージの質が、そのままポリシーの運用コストです。
2つ目は、何にもマッチしないポリシーです。マッチブロックのタイプミスや、誤ったネームスペースのせいで、ポリシーがまるごと空になっているケースはよくあります。これを見つける方法は、1つだけです。通過すべきリソースとブロックされるべきリソースの両方を入れてみることです。通過だけを確認しても、「何にもマッチしないポリシー」と「正常に動いているポリシー」を区別できません。
3つ目は、ポリシーよりも先に例外が必要な場合があることです。すでに動いているワークロードに新しいルールをかけると、必ず引っかかるものが出てきます。すべて直すまで待っていると、ポリシーは永遠に導入されません。PolicyExceptionでポリシー名・ルール名・リソース名まで絞って例外をドキュメントとして残し、その一覧が減っていくのを管理するほうがよいです。例外をポリシー全体にかけるのは、ポリシーをオフにするのと同じです。
次のラボですること
/root/policy/validate/require-labels.yamlに、必須ラベルを要求するClusterPolicyを書き、マッチと除外を明示します。そのあと、通過するPodと引っかかるPodをそれぞれ入れて、判定が分かれるのを確認します。続いて、レジストリを制限するdenyルールとpreconditionsを書き、最後に、複数のリソースを一度に検査したPolicyReportと、範囲の狭いPolicyExceptionを作ります。この環境では、クラスターがリクエストをブロックしてくれないため、判定は、kyverno applyをローカルで動かして確認します。