Open Policy Agentへ入門

CNCFのプロジェクトにOpen Policy Agentっというものがあります。 Open Policy Agentはポリシーエンジンとして動作し、指定したポリシーに準じているかどうかをチェックしてくれます。 そんなOpen Policy Agentについて軽く触ってみたのでどういった機能があるのか、気になった部分を中心に書いていきたいと思います。

www.openpolicyagent.org

目次

  1. Open Policy Agent
  2. Regoの特徴
  3. Regoの記述
  4. Regoのテスト
  5. 終わりに

1. Open Policy Agentについて

上記で簡単に説明しましたが、Open Policy Agentとはポリシーエンジンとして動作し、ポリシーのチェックを行います。

利用方法としてはコマンドラインで利用したり、デーモンで動かすパターンとライブラリとして利用するパターンがあります。

KubernetesやEnvoy、Kafkaと連携したり、Terraformのプラン結果をチェックできたりと、いろんな用途で扱うことができそうです。

基本的にはJSONデータであればポリシーチェックを行える感じですかね。 Terraformのポリシーチェックに関しても plan した結果を JSON形式に出力してチェックをしてますし、Kubernetesに関してもマニュフェストファイルをJSON形式に変換してチェックをしているようです。

次は、Open Policy Agentでポリシーを記述する「Rego」についてみて行きたいと思います。

2. Regoの特徴

Open Policy Agentではポリシーを定義するため、「Rego」っという言語で記述していきます。 宣言的にポリシーを記述でき、強力な表現方法を持っていきます。 特に配列に対する操作が特殊な部分があり理解するのに少々手間取ってしまいました。。。 例えば下記のような記述があったとします。

example.rego

package example

default allow = false

allow {
  has_environment["prod"]
}

has_environment [name] {
  sites = [
    {
      "name": "prod"
    },
    {
      "name": "dev"
    },
  ]
  name := sites[_].name
}

まずは詳細は理解せず雰囲気で見てもらいたいのですが、 allow から has_environment を呼び、引数として prod っという文字列を渡しています。

これをコマンドラインから実行すると下記のような結果が返ってきます。 下記のコマンドでは上記のRegoのファイルを引数として渡し、 allow を実行しています。

$ ./opa eval --data example.rego  'data.example.allow'
{
  "result": [
    {
      "expressions": [
        {
          "value": true,
          "text": "data.example.allow",
          "location": {
            "row": 1,
            "col": 1
          }
        }
      ]
    }
  ]
}

"value": true, が返ってきていますね。これはRegoの評価した結果が true になった証拠です。 ためしに、 has_environment の引数を stg にしてみます。すると結果が false になってしまいます。

これは、 sites の要素で name 属性の値を網羅的に引数の値(name)と比較して同じものがあるかどうかをチェックしてるっという感じです。

また、Regoでは先ほどの has_environmentallow はルールをまとめるルールとして定義しています。 has_environment では引数の値が sites の要素で name 属性の値と比較し評価しています。allow では has_environment の結果を評価しています。

そして、allow ではすべての評価対象が true のときのみ true を返します。

Regoではこうしてルールを定義し、評価していくことでポリシーに準じているかどうかをチェックしています。 また、こうして宣言的に記述したほうがポリシーの評価を簡単に行えます。

どちらかというとAlloyとか形式手法の言語に近いんですかね。

3. Regoの記述

基本的な記述方法

ここからREPLを使ってRegoの記述について触れていこうかと思います。 一般的な記述として以下のように値を扱えます。

x := 42
rect := {"width": 2, "height": 4}

ここでは x を42にとし、 rect{"width": 2, "height": 4} として扱えます。

> x == 42
true
> x == 43
false

等価の評価は == で行うことができます。 また、下記のように記述することでルールを作成することができます。

> t { x := 42; y := 41; x > y }
> t
true

仮にルールを評価したときに false になった時は下記のようになります。

> t2 { x := 42; y := 41; x < y }
> t2
undefined

Regoではルールの中の評価する順序について特殊な扱いをすることができ、下記のように記述することが可能となります。

> s { x > y; y = 41; x = 42 }

ここでは := ではなく = を扱っています。こうすることで記述する順番を気にすることなくルールを定義することも可能です。

次に配列を見ていきたいと思います。 上記でも記載しましたが、配列は下記のように記述することが可能です。

> sites = [{"name": "prod"}, {"name": "smoke1"}, {"name": "dev"}]
> r { sites[_].name == "prod" }
> r
true

sites[_].namesites の要素を網羅的に name 属性の値をチェックし、prod がないかチェックしています。

[_] が配列の中を網羅的に参照するので下記のようなに記述した場合、sites の要素の name 属性のみが返ってきます。

> q[name] { name := sites[_].name }
> q
[
  "prod",
  "smoke1",
  "dev"
]

ここで、 q のルールを包括している p のルールを作成して実行してみたいと思います。

> p { q["prod"] }
> p
true

true が返ってきましたね。ここで sites 要素の name 属性に存在しない値を入れてみたいと思います。

> w { q["smoke2"] }
> w
undefined 

true ではなくなりましたね。ちゃんと評価できているのが確認できます。

他にも配列ではイテレート処理に関して下記のように記述することも可能です。

> some i; sites[i]
+---+-------------------+
| i |     sites[i]      |
+---+-------------------+
| 0 | {"name":"prod"}   |
| 1 | {"name":"smoke1"} |
| 2 | {"name":"dev"}    |
+---+-------------------+

見てわかるように、ほかの言語(PHPとか)と同じように変数っぽく値を扱えたり、スカラー値や配列なども表現することができます。

違いとしては何度も言っていますが、宣言的な記述で手続きではないっというところですかね。

4. Regoのテスト

もう一つ、Open Policy Agentで気になった部分として先ほどのRegoに対してテストを書くことができます。 まずはサンプルを見てみたいと思います。

example.rego

package authz

allow {
    input.path == ["users"]
    input.method == "POST"
}

allow {
    some profile_id
    input.path = ["users", profile_id]
    input.method == "GET"
    profile_id == input.user_id
}

そして、先ほどのRegoをテストするコードを記述していきます。

example_test.rego

package authz

test_post_allowed {
    allow with input as {"path": ["users"], "method": "POST"}
}

test_get_anonymous_denied {
    not allow with input as {"path": ["users"], "method": "GET"}
}

test_get_user_allowed {
    allow with input as {"path": ["users", "bob"], "method": "GET", "user_id": "bob"}
}

test_get_another_user_denied {
    not allow with input as {"path": ["users", "bob"], "method": "GET", "user_id": "alice"}
}

ではこれを実行してみます。

$ ./opa test . -v
data.authz.test_post_allowed: PASS (785.051µs)
data.authz.test_get_anonymous_denied: PASS (443.895µs)
data.authz.test_get_user_allowed: PASS (526.708µs)
data.authz.test_get_another_user_denied: PASS (362.388µs)
--------------------------------------------------------------------------------
PASS: 4/4

おぉーテストできましたね(サンプルをコピペしただけですが)。 これは、ルールのプレフィックスtest_ で始まっているものを実行していっています。 テストは実行した結果が true になる場合のみ PASS となり、それ以外は FAIL、もしくは ERROR となります(そもそもランタイムエラーとか)。

サンプルとして下記のテストを実行してみます。

pass_fail_error_test.rego

package example

# This test will pass.
test_ok {
    true
}

# This test will fail.
test_failure {
    1 == 2
}

# This test will error.
test_error {
    1 / 0
}
$ ./opa test pass_fail_error_test.rego
data.example.test_failure: FAIL (414.31µs)
data.example.test_error: ERROR (400.987µs)
  pass_fail_error_test.rego:15: eval_builtin_error: div: divide by zero
--------------------------------------------------------------------------------
PASS: 1/3
FAIL: 1/3
ERROR: 1/3

test_failure で結果が FAIL となり、 test_error ではランタイムエラーとなり ERROR となりましたね。

テストを書けることでポリシーの品質の担保を行えるので、非常にありがたいですね。

4. 終わりに

Open Policy Agentについて軽く触ってみて、Regoの記述の仕方など興味深いものがありました。 ポリシーを評価するにあたって配列の扱いについてかなり表現の仕方が柔軟でほかの言語と書き方がだいぶ違うので戸惑うかと思いますが、慣れてくればすごく扱いやすいものになるんじゃないかと思います。

テストについてもポリシー自体の品質を担保してくれるので、とてもありがたい機能ですね。

次はOpen Policy Agentをアプリケーションの中で使ってみるか、Terraformの出力結果とかで使ってみたいと思います。