Chapter 09

📝 パッケージ・データ・クエリ

Masayoshi MIZUTANI
Masayoshi MIZUTANI
2022.01.31に更新

Regoにはコードを分割して管理するための仕組みとして「パッケージ」という概念を持ちます。今回はパッケージふくめ、ポリシーまわりの用語の説明や整理をしたいと思います。

パッケージとモジュール

Regoは名前空間を分割するために パッケージ という仕組みがあります。*.rego ファイルの冒頭にある package キーワードで宣言される名前がそのままパッケージ名になります。

policy1.rego
package mypkg1

allow = {
    input.user == "blue"
}

上記の例だとパッケージ名が mypkg1 になります。このポリシーは評価されるとPolicyとData編で紹介したとおり、virtual documentとして data.mypkg1 にマッピングされ、アクセスできるようになります。上記の場合は input.user が blue だった場合のみ data.mypkg1.allow に true がセットされます(値の返し方については結果の出力編を参照してください)

そしてこの上記例のように package <パッケージ名> から始まるテキストデータのことを モジュール と呼びます。OPAのコマンドから利用する場合は、ほぼ「1つの .rego ファイル == 1モジュール」という認識で間違っていません。公式の定義では以下のようになっています。

  • 必ず1つの package 宣言があり
  • 0個以上の import 文があり
  • 0個以上のルールがある

1つのパッケージには複数のモジュールが存在できるため、1つのパッケージのコードを複数のファイルで管理できます。

また、(Go言語で開発したことがある人は特に混乱しやすいのですが)Go言語と似たようなパッケージ宣言の仕方をしながら、OPAは モジュールの配置されているディレクトリ構造には一切関知しません。そのため同じパッケージのモジュール( .rego ファイル)が全く違うディレクトリ下にあっても良いし、同じディレクトリに異なるパッケージのモジュールが混在することも可能です。あくまでも package キーワードで宣言されたパッケージ名によってのみ、名前空間が分割されます。

パッケージ名の詳しい表記方法については公式ドキュメントにいくつか方法が記載されていますが、通常はドット . 区切りでサブパッケージのようなものを定義していきます。例えば

package mypkg.subpkg.subsubpkg

のような表記が可能で、これを data.mypkg.subpkg.subsubpkg としてアクセスすることができます。ただし見ての通り、ドットで区切る表記はパッケージ内の変数へのアクセス方法と同じです。したがって

policy1.rego
package mypkg

sub := "some data"

と

policy2.rego
package mypkg.sub

foo := "bar"

のようなファイルを両方読み込むと名前が衝突してエラーになります。

データ

一方でデータ(base documentのこと。data 変数ではない)については名前空間はディレクトリに完全に依存します。例えば ./blue/magic/hoge.json のようなディレクトリに入っているJSONデータは data.blue.magic によってアクセスできるようになります。

したがってポリシーだけを扱う場合は管理するためのディレクトリ構造に気を使いすぎる必要はありませんが、データを扱う場合はディレクトリ構造に気を払う必要があります。ポリシー内で直接利用しない場合でも、例えばテスト用の大きなJSONファイルのようなものを取り扱いたい場合はポリシーファイル(厳密にはモジュール)内に直接書き込みたくないこともあると思うので、データを取り扱う前提でディレクトリを設計する必要があります。

データはOPAが読み込むディレクトリにファイルとして置いておくだけでいいのですが、読み込まれる際には ファイル名は無視される という挙動に注意する必要があります。具体的に説明すると、

./blue/magic/foo.json
{
    "user": "blue",
}

というファイルがあった場合、読み込んだ際には data.blue.magic.foo.user == "blue" となるのではなく、 data.blue.magic.user == "blue" となり、foo.json の情報は消えてしまいます。では複数ファイルが有った場合どうなるかというと、1つのオブジェクト型にマージされます。そのため以下の制約が発生します。

  • データを置くディレクトリに配置できるのはトップレベルがオブジェクト型として扱えるもののみ(例えばトップが配列型などはNG)
  • オブジェクト型でも複数ファイルで同じキーを持つものがあってはいけない

そのため、ディレクトリの構造は配置したいデータの内容も含めて検討する必要がありそうです。

クエリ

最後にポリシーやデータを実行するにあたって重要な概念であるクエリについて紹介します。

クエリは最終的に計算されたドキュメント群( data )を参照して必要な結果を取得します。data の一部だけを抽出したり、data の結果を一部変換して取得すると言ったことも可能です。

ただし、結果判定に必要なデータ(例えば評価対象であるリクエストの構造化データ)は input によって渡すことを想定されており、現実的には都度クエリを変更しながら利用する、というユースケースは筆者視点だとあまりおもいつきませんでした。考えられるとすると、data に含まれる情報が大きくなり、クエリする際に一部だけを限定して取得したい、かつ取得する場所を都度変更したい、などの場合には利用できそうです[1]。

具体的な利用場面としてはOPAのevalコマンドやrunコマンドのインタラクティブモードでの入力で使用します。

% opa eval -b ./policy data

上記のコマンドを実行すると、評価された結果のドキュメント( data.* に格納された変数)がすべて返ってきます。必要に応じて data.policy1 のように区切ると結果の範囲が抑制されるので、大量のポリシーをまとめて運用する際にはクエリを選択するという使い方がありそうです。

パッケージ・モジュール・クエリの関係

ここまで解説してきた、パッケージ、モジュール、データの関係を図に表すと以下のようになります。

矢印としては記載していませんが、パッケージ間も data.* 層(?)に出力された結果を参照することで、相互に連携することができます(ただし循環参照はNG)。

まとめ

パッケージとデータでパス名の決定方法が違いつつ、最終的に同じ空間(data.* 以下)にマッピングされるため、ポリシーを管理するリポジトリの構成はかなり慎重に設計する必要がありそうです。筆者もまだ経験値が浅く、ベストプラクティスを探っていきたいと考えています。

脚注
  1. data に含まれるデータ量が大きくなる場面としてOPAサーバにすべてのポリシーを集約して運用する、という使い方が想定できますが、そのようなユースケースでは /v1/data/xxx というエンドポイントを使って取得するデータの絞り込みをするほうがシンプルになります。 ↩︎