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

ポリシーをコードで

validate — パターン、deny、そしてマッチの範囲

TT Labで続きを見る

一言でいうと

検証ポリシーの品質は、条件式ではなく、マッチ範囲と失敗メッセージで決まります。

なぜ必要なのか

ポリシーをはじめて書く人は、条件をどう表現するかから悩みます。しかし、実際に事故を起こすのは、条件ではありません。条件が間違っていればテストで見つかりますが、マッチ範囲が間違っていると、何も起きないため、誰も気づきません。クラスター全体にかけたポリシーが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をローカルで動かして確認します。