ポリシー試験 — 通過と拒否を同時に固定する
一言でいうと
ポリシーテストの最小単位は、入力サンプル・期待する判定・期待するメッセージの3列からなる表で、この表に拒否サンプルがなければ、そのテストは何も守れません。
なぜ必要なのか
ポリシーはコードだという言葉は、比喩ではありません。リポジトリに入り、レビューを経て、デプロイされ、修正されます。ところが、アプリケーションのコードと違う点が1つあります。ポリシーが壊れる最も一般的な形は、エラーではなく沈黙です。マッチ範囲がずれて何も見ていないと、ポリシーは黙ってすべてを通過させます。画面には何も起きず、ダッシュボードに赤い線もありません。そのため、「正常に動いているポリシー」と「死んだポリシー」が、見かけ上区別できません。
そこに、2つ目の力が重なります。ポリシーは、緩む方向にしか修正されません。誰かがブロックされて問い合わせをすると、条件を1つ緩め、例外のネームスペースを1つ追加します。それぞれの変更は、そのときどきでは妥当です。半年経つと、そのポリシーがもともと何をブロックしようとしていたのか、誰もわかりません。回帰テストは、まさにこの地点に打ち込んでおく釘です。拒否サンプルがテストにあれば、ルールを緩めた日に、テストがレッドになります。
どう動くのか
表ベース(ゴールデンケース)のテストです。テスト1つは、3つの列でできています。
| 列 | 内容 |
|---|---|
| 入力サンプル | 実際にクラスターに来そうなマニフェストの断片。通過すべきものとブロックされるべきもの |
| 期待する判定 | pass / fail / skip |
| 期待するメッセージ | 拒否のときに、人が受け取る文 |
3つ目の列を抜かすチームが多いのですが、メッセージも契約です。拒否メッセージは、ポリシーが開発者に語りかける唯一の経路で、その文が変わると、それをつかまえて自動化したパイプラインも変わります。メッセージをテストに入れておけば、「なぜブロックされたのかわからない」という問い合わせが、レビューの段階で表に出ます。
サンプルは、1回に1点だけ違うように作るのがよいです。通過サンプルをコピーして、違反フィールドを1つだけ変えたペアを複数置けば、テストが壊れたときに、何が違うのかがファイル名からすぐに読み取れます。
テストを実行する3つの場所です。
- クラスターに上げる前の、オフラインエンジンです。ポリシーファイルとサンプルファイルだけで、判定を回します。ネットワークもクラスターも必要ないので、CIで最も速く動きます。Kyverno CLIの
kyverno applyとkyverno test、Conftest、OPAが、ここに該当します。 - クラスターに上げる前の、サーバーdry-runです。
?dryRun=Allで送ったリクエストは、保存の段階だけを除いて、通常の経路をそのまま通ります。ドキュメントは、このとき関連するアドミッションコントローラーがすべて実行され、検証アドミッションはミューテーションが終わった後のオブジェクトを見て、デフォルト値が埋められ、スキーマ検証も行われると書いています。そのため、これは「真似」ではなく、本物の判定です。オフラインエンジンが見られないもの(ほかのポリシーとの相互作用、ミューテーション後の形)が、ここでだけ見えます。 - 上げた後です。バックグラウンドスキャンとレポートで、すでにあるリソースを走査します。アドミッションは、これから入ってくるリクエストだけを見るからです。
サーバーdry-runには、ペアになる事実が1つあります。副作用のあるアドミッションコントローラーにかかるリクエストは、dry-runなら、むしろ失敗させます。そのため、Webhookは、設定オブジェクトのsideEffectsをNoneまたはNoneOnDryRunと宣言しておかないと、dry-runの経路で正常に評価されません。内蔵のアドミッションプラグインは、すべてdry-runをサポートしています。
誤通過のよくある原因です。テストはグリーンなのにクラスターがブロックしないとき、原因は、たいてい次の3つのうちのどれかです。
- ポリシーがそのリソースをそもそも見ていません。マッチ範囲が
Deploymentなのに、サンプルはPodだったり、apiVersionが違ったりします。判定はskipなのに、テストがskipを失敗として数えないと、グリーンになります。skipをpassと区別して数えることが、最初の防衛線です。 - 終了コードをそのまま信じてしまいました。ツールが違反を見つけても0で終了することが、実際にあります。このラボ環境の
kyverno json scanが、そうです。そうなると、パイプラインは永遠にグリーンです。 - サンプルに拒否が1つもありません。通過だけを集めたテストは、ポリシーをまるごと削除してもグリーンです。テストファイルを開いたときに、failの期待が1つもなければ、そのテストはないのと同じです。
ポリシーのリポジトリの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も実際に動作します。ツールの名前が違っても、学ぶことは同じです。
参考ドキュメント
- kyverno applyとテストスイート: https://kyverno.io/docs/kyverno-cli/usage/apply/
- ポリシーレポート: https://kyverno.io/docs/policy-reports/
- Kubernetes APIの概念のうち、dry-run: https://kubernetes.io/docs/reference/using-api/api-concepts/
- 動的アドミッション制御とsideEffects: https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/
次のラボですること
ポリシー1つに付けるテストスイートを、最初から作ります。通過サンプルと拒否サンプルをペアで置き、期待する判定と期待するメッセージを表に書いたうえで、上の2つ目の場所(サーバーdry-run)を判定エンジンとするランナーを、自分で書きます。ポリシーがそもそも見ていないリソースを拒否サンプルとして置いて、誤通過を手で作ってみて、それを見つける検査を追加します。そのあと、ルールを1段階緩めて、拒否サンプルが通過してしまうのを見て、回帰テストがその変更をつかまえる場面を確認します。最後に、サンプルが1つもないテストがグリーンにならないように、終了コードの規約を決めます。