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

ポリシーをコードで

ポリシー試験 — 通過と拒否を同時に固定する

TT Labで続きを見る

一言でいうと

ポリシーテストの最小単位は、入力サンプル・期待する判定・期待するメッセージの3列からなる表で、この表に拒否サンプルがなければ、そのテストは何も守れません。

なぜ必要なのか

ポリシーはコードだという言葉は、比喩ではありません。リポジトリに入り、レビューを経て、デプロイされ、修正されます。ところが、アプリケーションのコードと違う点が1つあります。ポリシーが壊れる最も一般的な形は、エラーではなく沈黙です。マッチ範囲がずれて何も見ていないと、ポリシーは黙ってすべてを通過させます。画面には何も起きず、ダッシュボードに赤い線もありません。そのため、「正常に動いているポリシー」と「死んだポリシー」が、見かけ上区別できません。

そこに、2つ目の力が重なります。ポリシーは、緩む方向にしか修正されません。誰かがブロックされて問い合わせをすると、条件を1つ緩め、例外のネームスペースを1つ追加します。それぞれの変更は、そのときどきでは妥当です。半年経つと、そのポリシーがもともと何をブロックしようとしていたのか、誰もわかりません。回帰テストは、まさにこの地点に打ち込んでおく釘です。拒否サンプルがテストにあれば、ルールを緩めた日に、テストがレッドになります。

どう動くのか

表ベース(ゴールデンケース)のテストです。テスト1つは、3つの列でできています。

列 内容
入力サンプル 実際にクラスターに来そうなマニフェストの断片。通過すべきものとブロックされるべきもの
期待する判定 pass / fail / skip
期待するメッセージ 拒否のときに、人が受け取る文

3つ目の列を抜かすチームが多いのですが、メッセージも契約です。拒否メッセージは、ポリシーが開発者に語りかける唯一の経路で、その文が変わると、それをつかまえて自動化したパイプラインも変わります。メッセージをテストに入れておけば、「なぜブロックされたのかわからない」という問い合わせが、レビューの段階で表に出ます。

サンプルは、1回に1点だけ違うように作るのがよいです。通過サンプルをコピーして、違反フィールドを1つだけ変えたペアを複数置けば、テストが壊れたときに、何が違うのかがファイル名からすぐに読み取れます。

テストを実行する3つの場所です。

  1. クラスターに上げる前の、オフラインエンジンです。ポリシーファイルとサンプルファイルだけで、判定を回します。ネットワークもクラスターも必要ないので、CIで最も速く動きます。Kyverno CLIのkyverno applyとkyverno test、Conftest、OPAが、ここに該当します。
  2. クラスターに上げる前の、サーバーdry-runです。?dryRun=Allで送ったリクエストは、保存の段階だけを除いて、通常の経路をそのまま通ります。ドキュメントは、このとき関連するアドミッションコントローラーがすべて実行され、検証アドミッションはミューテーションが終わった後のオブジェクトを見て、デフォルト値が埋められ、スキーマ検証も行われると書いています。そのため、これは「真似」ではなく、本物の判定です。オフラインエンジンが見られないもの(ほかのポリシーとの相互作用、ミューテーション後の形)が、ここでだけ見えます。
  3. 上げた後です。バックグラウンドスキャンとレポートで、すでにあるリソースを走査します。アドミッションは、これから入ってくるリクエストだけを見るからです。

サーバーdry-runには、ペアになる事実が1つあります。副作用のあるアドミッションコントローラーにかかるリクエストは、dry-runなら、むしろ失敗させます。そのため、Webhookは、設定オブジェクトのsideEffectsをNoneまたはNoneOnDryRunと宣言しておかないと、dry-runの経路で正常に評価されません。内蔵のアドミッションプラグインは、すべてdry-runをサポートしています。

誤通過のよくある原因です。テストはグリーンなのにクラスターがブロックしないとき、原因は、たいてい次の3つのうちのどれかです。

ポリシーのリポジトリのCIは、たいていこの順序です。

1) 정책 파일 스키마·문법 검사
2) 오프라인 엔진으로 골든 케이스 표 실행 (pass·fail·메시지)
3) skip 이 0 인지 확인 — 매치가 어긋나지 않았다는 증거
4) 거부 기대 건수가 0 이 아닌지 확인
5) 스테이징 클러스터에 서버 dry-run 으로 대표 표본 몇 개
6) 배포

このコードブロックの韓国語は、6つの手順を順に、ポリシーファイルのスキーマ・文法の検査、オフラインエンジンでゴールデンケースの表を実行する(pass・fail・メッセージ)、skipが0であることの確認(マッチがずれていない証拠)、拒否の期待件数が0でないことの確認、ステージングクラスターへサーバーdry-runで代表的なサンプルを数件、デプロイ、と述べています。

手順3と手順4が、この一覧で最もよく抜け落ち、最も静かに裏切ります。

現場での姿

1つ目は、グリーンなのに本番でブロックされないポリシーです。サンプルのapiVersionが1つ古いバージョンで、すべてskipだった事例がよくあります。テストの出力の要約行で、passの数だけを見て、skipの数を見なかったのです。要約でskipを0に強制する1行が、この事故をまるごと防ぎます。

2つ目は、ルールを緩めた日です。「このネームスペースだけ除外してください」が繰り返されるうちに、ある日、例外の条件が広くなりすぎて、もともとブロックしていたものまで通過します。その瞬間、拒否サンプルがテストにあれば、PRでレッドになり、なければ、半年後に事故として知ることになります。

3つ目は、メッセージが黙って変わった日です。ポリシーをリファクタリングしながら、拒否メッセージの文言を整えたら、その文字列をつかまえてSlackの通知を作っていたパイプラインが、何も言わずに止まります。メッセージを期待値として固定しておけば、リファクタリングする人が、その場で気づけます。

4つ目は、この環境の正直な限界です。ラボのPodには、conftestとopaがありません。その代わり、kyverno CLIがあるので、同じ形の表ベースのテストを実行でき、kwokが起動した本物のAPIサーバーがあるので、サーバーdry-runも実際に動作します。ツールの名前が違っても、学ぶことは同じです。

参考ドキュメント

次のラボですること

ポリシー1つに付けるテストスイートを、最初から作ります。通過サンプルと拒否サンプルをペアで置き、期待する判定と期待するメッセージを表に書いたうえで、上の2つ目の場所(サーバーdry-run)を判定エンジンとするランナーを、自分で書きます。ポリシーがそもそも見ていないリソースを拒否サンプルとして置いて、誤通過を手で作ってみて、それを見つける検査を追加します。そのあと、ルールを1段階緩めて、拒否サンプルが通過してしまうのを見て、回帰テストがその変更をつかまえる場面を確認します。最後に、サンプルが1つもないテストがグリーンにならないように、終了コードの規約を決めます。