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

ポリシーをコードで

計画をポリシーで読む — 破棄・置換・公開範囲を適用前に捕まえる

TT Labで続きを見る

一言でいうと

TerraformとOpenTofuの計画(plan)は、JSONとして取り出せる構造化された変更仕様であり、そのJSONにポリシーをかければ、リソースが作られる前に、破壊・置き換え・公開範囲を押さえられます。

なぜ必要なのか

アドミッションコントロールは強力ですが、すでに作られたリクエストを見ます。そして、世の中の危険な変更のかなりの部分は、そもそもKubernetes APIを通りません。セキュリティグループを0.0.0.0/0で開くこと、データベースを置き換え(replace)ること、ストレージバケットを公開に切り替えること。これらは、クラウドAPIにまっすぐ行きます。アドミッションが見られる場所には、来ないのです。

ところが、これらの変更には共通点があります。適用される前に、計画の段階があります。tofu planとterraform planは、「何を作り、何を削除し、何を変更するのか」を先に計算します。人が読むために作られた出力ですが、機械が読める形でも取り出せます。そうすると、この計画は、ポリシーで検査できるドキュメントになります。アドミッションがクラスターのゲートキーパーなら、計画の検査は、クラウド全体のゲートキーパーです。しかも、ずっと安いです。元に戻すものが、まだないからです。

どう動くのか

手順は2行です。計画をファイルとして保存し、それをJSONに変換します。

tofu plan -out=tfplan.binary
tofu show -json tfplan.binary > plan.json

Terraformも同じです(terraform show -json)。出力されるJSONで、ポリシーが見る場所は、ほとんどがresource_changes配列で、項目ごとにaddress、type、name、そしてchangeオブジェクトが入っています。changeの中のactionsが、判定の中心です。ドキュメントが明言している有効な値は、次のとおりです。

actions 意味
["no-op"] 変わるものがない
["create"] 新しく作る
["read"] データソースを読む
["update"] その場で修正する
["delete"] 削除する
["delete", "create"] 置き換え。削除して作り直す
["create", "delete"] 置き換えだが、先に作って後で削除する

置き換えが2要素の配列で表現されているのは、意図的です。ドキュメントは、こう表現しておけば、呼び出す側が、リストにdeleteが入っているかどうかだけを走査しても、リソースが消える3つのケースをすべて捉えられると説明しています。ポリシーの最初のルールは、ここから出てきます。"delete" in actionsなら、人の承認を要求します。データベースやボリュームのような、ステートフルなリソースでは、特にそうです。

問えるのは、破壊だけではありません。

計画には、まだ不明な値があります。これが、計画ポリシーの最大の制約です。リソースID、生成されたARN、ランダムなサフィックスのような値は、適用してみないと決まりません。JSONは、これをafter_unknownで表現しますが、ドキュメントの説明は正確です。afterと同じ構造を持ち、不明なリーフの値はtrueに、わかっているリーフの値はそもそも抜けているオブジェクトです。そのため、ポリシーを書くときのルールは、こうなります。

判定をパイプラインの結果に変換する作業。終了コードが常に真実とは限りません。ここが、このモジュールで最も実用的な箇所です。ポリシーツールをCIに入れるとき、人々は自然に、if ! tool scan ...; then exit 1; fiを使います。ところが、違反を見つけても0で終了するツールが、実際にあります。このラボ環境のkyverno json scanが、そうです。違反は、人が読む出力にFAILEDとして出力され、--output jsonのViolationsにも残りますが、終了コードは、違反があってもなくても0です(ラボのイメージのkyverno CLI 1.13.2で、直接確認しました)。その事実を知らないまま、終了コードだけを信じると、パイプラインは永遠にグリーンです。

そのため、ポリシーのゲートを付けるときの順序は、こうです。

1) 도구를 일부러 실패할 입력으로 돌려 본다
2) 종료 코드를 확인한다 (echo $?)
3) 0 이면 종료 코드를 쓰지 않고 보고서를 파싱한다
4) 파싱 결과가 비어 있지 않은지도 확인한다 (형식이 바뀌면 0건으로 읽힌다)
5) 그 판정으로 파이프라인을 세운다

このコードブロックの韓国語は、5つの手順を順に、ツールをわざと失敗する入力で実行してみる、終了コードを確認する(echo $?)、0なら終了コードを使わずにレポートをパースする、パース結果が空でないかも確認する(形式が変わると0件として読まれる)、その判定でパイプラインを組む、と述べています。

手順2を飛ばすことが事故の始まりで、手順4を飛ばすと、ツールのバージョンアップの日に、ゲートが黙ってオフになります。

現場で使われるエンジンは、Conftest(OPAのRegoで任意のJSONを検査する)、OPA自体、kyverno-jsonのようなものです。このラボのPodには、conftestとopaがありません。その代わり、kyverno CLIのkyverno json scanで、同じことをします。任意のJSONドキュメントをペイロードとして与え、ポリシーで判定する構造は、同じです。ツールが違っても、学ぶこと(計画JSONの形、不明な値、終了コードの落とし穴)は、そのままです。

現場での姿

1つ目は、承認ゲートが実際に止めた日です。["delete", "create"]がRDSインスタンスに付いた計画がPRに上がり、ゲートがそれをつかまえて、人が確認しました。フィールドを1つ変更したことが置き換えを引き起こすかどうかは、コードだけを見てもわからず、計画にだけ出ます。

2つ目は、不明な値のせいで生じた誤検知です。必須タグの検査をafter.tagsだけで書いたところ、タグをローカル変数で計算するモジュールで、その値が計画の段階でまだなく、すべて違反として出ました。数日でゲートがオフになりました。after_unknownも一緒に見るルールに直すべきでした。

3つ目は、黙ってオフになっていたゲートです。ツールをバージョンアップしたら、レポートのJSONのキーが1つ変わり、パース結果が常に空の配列になりました。違反0件として読まれ、数週間、グリーンでした。「結果が空でないか」も一緒に見る1行が、これを防ぎます。

4つ目は、計画はステートに依存することです。同じコードでも、ステートファイルが違えば、計画が違います。PRで作った計画を数日後にそのまま適用すると、その間の変更が反映されません。計画の検査は、デプロイの直前にもう一度作った計画で行うのが、正しいです。

参考ドキュメント

次のラボですること

オフラインのプロバイダーで計画を作ってJSONとして取り出し、そのJSONにポリシーをかけます。resource_changesを走査して、deleteが含まれている項目を見つけ出し、置き換えと単純な更新を区別し、必須タグが抜けているリソースをつかまえるルールを書きます。値が計画の段階でまだない場所をわざと作って、after_unknownを見ないルールが誤検知を出すのを確認した後、直します。最後に、違反が明らかな入力をツールに入れて、終了コードを自分で出力してみて、0が出るのを確認し、レポートをパースしてパイプラインを組むゲートを作ります。