Open Policy Agent(OPA)のRego言語を使う5つのヒント
2020年9月18日
0 分で読めます編集者注
このブログ記事は、もともとfugue.coに掲載されていました。Fugueは2022年にSnykの一員となり、Snyk IaCの重要な構成要素となっています。
FugueではOpen Policy Agent(OPA)をとても気に入っており、クラウドリソースを安全に保つために、これまで多くのRegoコードを書いてきました。そこで、取り組みを通じて得た特に役立つ知見をまとめました。OPAとRegoを使ってポリシーをコードとして実装すれば、コード化されたポリシーを自動的に適用することもできます。
1. ド・モルガンの法則は頼れる味方
このリストを1つだけに絞るなら、これを選びます。ド・モルガンの法則は、2つの変換規則です。Regoに適用すると、動作をまったく変えずにルールを変換できます。
一般に、次のように表されます。
コードで表すと、次のとおりです。
例を挙げると、さらに分かりやすいでしょう。次の2つの文は同じ意味で、1つ目の法則を適用した例です。
これは、ハムとマッシュルームがのっていないピザです。
このピザには、ハムがのっていない、またはマッシュルームがのっていません。
この法則を使えば、コードの動作をまったく変えずに、AND節をOR節に、またはその逆に変換できます。「コードの動作が変わらないのなら、なぜこの変換が役立つの?」と思うかもしれません。
重要なのは、クエリ言語であるRegoは、非常に選言(OR文)を中心に構成されているという点です。たとえば、グループの中にピザを切る資格がある人がいるかどうかは、次のように確認できます。
では、グループの全員にピザを切る資格があるかどうかは、どう確認すればよいでしょうか?まず、資格のある人たちのグループを作って、その人数を調べる……という方法もあります。
これはあまりよい方法ではありません。読みにくいだけでなく、こうした集合を数える必要があるため、クエリプランナーによる最適化も難しくなります。グループに未成年者が1人でもいれば、その時点で処理を打ち切れる場合でも同じです。
ここでド・モルガンの法則が役立ちます。いくつかの部分を否定すれば、クエリを常に選言として記述できるのです。元の文を否定する補助文を追加します。
あとはポリシーを簡単に記述できます。
これで完成です!効率的で読みやすくなりました。
まとめ:Regoポリシーの記述が難しいと感じたら、まず否定したポリシーのほうが簡単に書けないか考え、それをもう一度否定してみましょう。論理ってすばらしいですね!
2. or、それともany?
先ほど見たように、OR文を先頭に置き、ルール本文の中でAND文を順に並べると、Regoは読み書きしやすくなります。しかしプログラマーなら、慣用的な書き方がうまく機能しないケースに出くわすまで、それほど時間はかかりません。
Regoでは、ルール本文にすでに多くのクエリが含まれている場合によく起こります。たとえば、次のような場合です。
ピザのスライスも許可したい場合はどうすればよいでしょうか?ルール本文全体を複製する方法があります。
これはあまりよくありません。多くの場合、補助ルールを追加できます。
このケースではうまくいきます。しかし、ときには補助ルールが増えすぎて、適切な名前を付けることさえ難しくなることがあります。そんなまれなケースでは、RegoにanyというOR文があるかのように書けます。
この書き方なら、十分読みやすくなります。anyを使いすぎないようにすれば、作業がずっと楽になります。
3. anyとallの内包表記
同じ考え方をさらに進めると、anyとallは、特にリスト内包表記と組み合わせると、さまざまな場面で非常に役立ちます。この2つの機能を組み合わせると、単独で使う以上の効果が得られます。
最初の例に戻ってみましょう。allをリスト内包表記と組み合わせると、同じポリシーを次のように表せます。
ド・モルガンの法則に従って作ったものと同じくらい、読みやすいと思います。どちらがより慣用的かは、通常、文脈によって変わります。
否定したポリシーのほうが考えやすいでしょうか?その場合は、ド・モルガンの変換を使います。
それとも、取り締まりの対象をリストや集合として考えるほうが簡単でしょうか?その場合はリスト内包表記が適しています。
4. walkでドキュメントツリー全体をたどる
Regoのクエリは必ず終了します。これは優れた特性ですが、その代わり、一般に再帰ルールを記述することはできません。
再帰ルールが必要なのはなぜでしょうか?深くネストされたツリーのような、再帰的な入力ドキュメントを扱うのに非常に便利だからです。幸い、OPAにはこれを助ける組み込み関数があります。
たとえば、ディスカッションスレッドを監視するポリシーを作成するとします。
Regoでメッセージを取得するにはどうすればよいでしょうか?次のように書く方法があります。
ネストの深さが有限ならこれで問題ありませんが、常にそうだとは限りませんし、あまり読みやすい書き方でもありません。代わりにwalkを使えば、ツリーを再帰的にたどり、すべてのmessageフィールドを取得できます。
メッセージを通常のRegoルールで扱えるようになれば、禁止用語を含むディスカッションスレッドを拒否する、慣用的なポリシーを記述できます。
実際の例としては、Terraformの子モジュールがあります。Regulaではここでwalkを使い、ディスカッションスレッドのメッセージをフラット化するのとよく似た方法で、子モジュールをフラット化しています。
5. パッケージとルールを動的に読み込む
Regoの興味深い設計の1つは、ルールとデータからなる「世界」全体が、同じドキュメントの下にネストされていることです。ユーザー入力、JSONファイルやYAMLファイルのデータ、パッケージ内のルールのいずれにアクセスする場合も、すべて単なる参照として扱います。
Regoでは変数やワイルドカードを使って参照を列挙できるため、読み込まれたパッケージの集合を動的に調べられます。これにより、新しいポリシーファイルを追加するだけで、それらすべてをレポートにまとめられるアーキテクチャを構築できます。
ポリシーを別々のパッケージに置くと、個別にデバッグでき、ルール名が競合する心配もありません。すべてをdata.policies名前空間に配置します。最初のポリシーでは、ド・モルガンの法則を使ってピザにチーズがのっているかどうかを確認します。
2つ目のポリシーでは、最後の一切れをめぐって2人が争わないことを確認します。
ポリシーを配置する名前空間が決まれば、それらをまとめるのは比較的簡単です。data.policies内のすべてのdenyを取得し、人が読めるレポートとして整形します。
実際の値を抽出するjqと組み合わせれば、ターミナル上に見やすいレポートを出力できます。ここでは-Iを使ってstdinから入力を読み込みます。
パッケージをデータとして扱うことで、さらに高度なこともできます。たとえば、パッケージがinput_version = 1を宣言しているかinput_version = 2を宣言しているかに応じて、inputを別の形式に変換して渡すこともできます。すべてRegoの中で実現できます!



