電通総研 テックブログ

電通総研が運営する技術ブログ

Regal を使って Rego を Lint する

こんにちは。X(クロス)イノベーション本部クラウドイノベーションセンターの柴田です。この記事では Rego の Linter である Regal を紹介します。1この記事は 電通総研 Advent Calendar 2024 の 5 日目の投稿です。前日の記事は井手さんの「入社 3 年目の業務内容紹介」でした。

Open Policy Agent / Rego とは?

Open Policy Agent (OPA) と Rego の概要については以下の記事をご参照ください。2

Policy as Code を実現する Open Policy Agent / Rego の紹介 - 電通総研 テックブログ

Regal とは?

Regal は Styra 社が開発している Rego の Linter です。3Regal を使用して

  • ベストプラクティスに反したコード
  • よくある間違い、バグ、非効率的な処理を含むコード
  • プロジェクト独自のコーディング規約に反したコード

などを機械的に発見することで Rego コードの品質や可読性を向上できます。

Regal を実行する

Lint を実行する

ローカルで実行する

インストールする

以下の 3 つの方法があります。

  • パッケージマネージャーを使ってインストールする。4
  • GitHub の release asset をダウンロード&インストールする。
  • Docker を使う。

具体的なインストール手順はここでは省略します。詳しくは以下の公式ドキュメントをご参照ください。

実行する

以下のような Rego のソースコードpolicy/authz.rego にあるとします。

package authz

import rego.v1

default allow = false

allow if {
    isEmployee
    "developer" in input.user.roles
}

isEmployee if regex.match("@acmecorp\\.com$", input.user.email)

以下のコマンドで Lint を実行します。5

regal lint policy/

するとルールに違反している箇所が表示されます。

Rule:           opa-fmt
Description:    File should be formatted with `opa fmt`
Category:       style
Location:       policy/authz.rego:1:1
Text:           package authz
Documentation:  https://docs.styra.com/regal/rules/style/opa-fmt

Rule:           directory-package-mismatch
Description:    Directory structure should mirror package
Category:       idiomatic
Location:       policy/authz.rego:1:9
Text:           package authz
Documentation:  https://docs.styra.com/regal/rules/idiomatic/directory-package-mismatch

Rule:           non-raw-regex-pattern
Description:    Use raw strings for regex patterns
Category:       idiomatic
Location:       policy/authz.rego:12:27
Text:           isEmployee if regex.match("@acmecorp\\.com$", input.user.email)
Documentation:  https://docs.styra.com/regal/rules/idiomatic/non-raw-regex-pattern

Rule:           use-assignment-operator
Description:    Prefer := over = for assignment
Category:       style
Location:       policy/authz.rego:5:15
Text:           default allow = false
Documentation:  https://docs.styra.com/regal/rules/style/use-assignment-operator

Rule:           prefer-snake-case
Description:    Prefer snake_case for names
Category:       style
Location:       policy/authz.rego:12:1
Text:           isEmployee if regex.match("@acmecorp\\.com$", input.user.email)
Documentation:  https://docs.styra.com/regal/rules/style/prefer-snake-case

1 file linted. 5 violations found.

Hint: 2/5 violations can be automatically fixed (directory-package-mismatch, use-assignment-operator)
      Run regal fix --help for more details.

CI で実行する

CI で Regal を実行することもできます。例えば GitHub Actions であれば StyraInc/setup-regal action を使って以下のようなワークフローを設定できます。

name: Regal Lint
on:
  pull_request:
jobs:
  lint-rego:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: StyraInc/setup-regal@v1
        with:
          # For production workflows, use a specific version, like v0.22.0
          version: latest
      - name: Lint
        run: regal lint --format=github ./policy

詳しくは以下の公式ドキュメントをご参照ください。

Using Regal in your build pipeline | Styra Documentation

エディタと組み合わせて実行する

Regal は Language Server および Debug Adapter の機能を提供します。普段使っているエディタと Regal を連携させることで

  • Linter からの素早いフィードバック
  • コード補完
  • マウスオーバー時のツールチップの表示
  • 定義への移動

などの恩恵を受けることが可能です。

本記事ではこれらの詳細は扱いません。詳しくは以下の公式ドキュメントをご参照ください。

コードを自動修正する

Regal は以下のルールに関してコードを自動的に修正できます。

先ほどのサンプルコード policy/authz.rego を修正してみましょう。以下のコマンドでコードを自動的に修正します。6 なお --dry-run フラグを設定して dry run することもできます。

regal fix policy/
2 fixes applied:
In project root: /home/ubuntu/projects/regal/policy
authz.rego -> authz/authz.rego:
- directory-package-mismatch
- use-rego-v1

サンプルコードがルールに従って以下のように修正されました。7

 package authz

 import rego.v1

-default allow = false
+default allow := false

 allow if {
-    isEmployee
-    "developer" in input.user.roles
+       isEmployee
+       "developer" in input.user.roles
 }

 isEmployee if regex.match("@acmecorp\\.com$", input.user.email)

詳しくは以下の公式ドキュメントをご参照ください。

Fixing Violations | Styra Documentation

ルール

組み込みルール

Regal にはデフォルトで複数のルールが組み込まれています。

組み込みルールは以下の 7 つのカテゴリに分類されています。custom カテゴリ以外のルールはデフォルトで有効です。^6

  • bus : 一般的な間違い、潜在的なバグ、非効率的な書き方に関するルール。
  • idiomatic : 慣用的な書き方に関する
  • imports : インポートに関するルール。
  • performance : パフォーマンスに関するルール。
  • style : スタイルに関するルール( Rego Style Guide )。
  • testing : テストに関するルール。
  • custom : ニーズに合わせてカスタマイズすべきルール。

具体的なルールの内容は以下の公式ドキュメントをご参照ください。

Rules | Styra Documentation

カスタムルール

独自のルールを定義することもできます。詳しくは後ほど説明します。

Regal を設定する

設定ファイル

格納場所

Regal は設定ファイルを以下のように探索します。

  • カレントディレクトリからプロジェクトルートへトラバースしつつファイル .regal/config.yaml を探します。最初に見つかったファイルを設定ファイルとして参照します。
  • --config-file フラグにファイルパスが指定されている場合はそれを参照します。

サンプル

rules:
  style:
    todo-comment:
      # don't report on todo comments
      level: ignore
    line-length:
      # custom rule configuration
      max-line-length: 100
      # warn on too long lines, but don't fail
      level: warning
    opa-fmt:
      # not needed as error is the default, but
      # being explicit won't hurt
      level: error
      # files can be ignored for any individual rule
      # in this example, test files are ignored
      ignore:
        files:
          - "*_test.rego"
  custom:
    # custom rule configuration
    naming-convention:
      level: error
      conventions:
        # ensure all package names start with "acmecorp" or "system"
        - pattern: '^acmecorp\.[a-z_\.]+$|^system\.[a-z_\.]+$'
          targets:
            - package

capabilities:
  from:
    # optionally configure Regal to target a specific version of OPA
    # this will disable rules that has dependencies to e.g. built-in
    # functions or features not supported by the given version
    #
    # if not provided, Regal will use the capabilities of the latest
    # version of OPA available at the time of the Regal release
    engine: opa
    version: v0.58.0

ignore:
  # files can be excluded from all lint rules according to glob-patterns
  files:
    - file1.rego
    - "*_tmp.rego"

project:
  roots:
    # declares the 'main' and 'lib/jwt' directories as project roots
    - main
    - lib/jwt

設定ファイルの書き方

rules

rules には各ルールに関する設定を記述します。主な設定項目を以下に示します。

キー 説明
rules.default.level すべてのルールのデフォルトの level を指定します。
rules.<category>.default.level あるカテゴリのデフォルトの level を指定します。
rules.<category>.<rule>.level あるルールの level を指定します。
rules.<category>.<rule>.ignore.files あるルールの適用対象除外とするファイルを指定します。

level には以下の値を指定できます。

  • error : 違反を表示して lint コマンドをゼロ以外の終了コードで終了します。
  • warning : 違反を表示して lint コマンドをゼロの終了コードで終了します。
  • ignore : ルールを無効化します。

ルールによっては上記に加えて固有の設定項目があります。
詳しくは以下の公式ドキュメントに記載された各ルールの説明文をご参照ください。

Rules | Styra Documentation

ignores

ignores にはすべてのルールの適用対象外とするファイルを指定します。

capabilities

capabilities には OPA のランタイムバージョンなどを指定します。
指定したランタイムバージョンでは利用できない機能や関数に依存したルールは無効化されます。8

主な設定項目を以下に示します。

キー 説明
capabilities.from OPA のランタイムバージョンを指定します。このバージョンでは利用できない機能や関数に依存したルールは無効化されます。[^8]指定しない場合は Regal がリリースされた時の最新の OPA のランタイムバージョンが使用されます。
capabilities.plus 利用できる機能や関数を追加で指定します。
capabilities.minus 利用できない機能や関数を追加で指定します。

詳しくは以下の公式ドキュメントをご参照ください。

Capabilities | Styra Documentation

project

プロジェクトのルートディレクトリを指定します。詳しくは後ほど説明します。

デフォルトの設定値

ユーザが設定ファイルで明示的に値を設定していない設定項目については以下の値が使用されます。

data.yaml

ルールを抑制する

Regal のルールがプロジェクトに適さない場合はそのルールを無効化できます。ルールを無効化する方法は以下の 3 種類があります。

inline directive

ソースコードに以下のようなコメントを記述することで、ソースコードの特定の箇所に対する特定のルールの適用を無効化できます。

# regal ignore:<rule>

利用例は以下のとおりです。

package policy

import rego.v1

# regal ignore:prefer-snake-case
camelCase := "yes"

list_users contains user if { # regal ignore:avoid-get-and-list-prefix
    some user in data.db.users
    # ...
}

CLI フラグ

以下のフラグで指定したルールを有効化または無効化できます。

  • --enable , --disable : 指定したルールを有効化または無効化します。
  • --enable-category , --disable-category : 指定したカテゴリのルールを有効化または無効化します。
  • --enable-all , --disable-all : すべてのルールを有効化または無効化します。

また指定したファイルを無視することもできます。

  • --ignore-files : 指定したファイルを無視します。

設定ファイル

  • 指定したルールを無効化します。
rules:
  style:
    prefer-snake-case:
      level: ignore
  • 指定したカテゴリのルールを無効化します。
rules:
  style:
    default:
      level: ignore
  • すべてのルールを無効化します。
rules:
  default:
    level: ignore
  • 指定したファイルに対して指定したルールを無効化します。
rules:
  style:
    line-length:
      level: error
      ignore:
        files:
          - "*_test.rego"
          - "scratch.rego"
  • 指定したファイルに対してすべてのルールを無効化します。
ignore:
  files:
    - file1.rego
    - "*_tmp.rego"

プロジェクトルート

Regal は以下のディレクトリをプロジェクトルートとして認識します。

一部のルール(例えば directory-package-mismatch )を正しく評価するためにはプロジェクトルートを正しく設定する必要があります。
もしプロジェクトルートが通常と異なる場合はプロジェクトルートを明示的に設定しておきましょう。

詳しくは以下の公式ドキュメントをご参照ください。

Project Roots | Styra Documentation

カスタムルールを実装する

カスタムルールの種類

カスタムルールには以下の 2 種類の実装方法があります。

  • ファイル単位で検査する方法
  • 複数のファイルをまとめて検査する方法

ファイル単位ではルール違反の有無を判断できない場合は後者の実装方法を採用します。

雛形を作成する

regal new rule コマンドを実行します。

regal new rule --category <category> --name <rule>

すると Regal はカスタムルールのソースコードの雛形を以下に作成します。

.regal/rules/custom/regal/rules/<category>/<rule>/<rule>.rego
.regal/rules/custom/regal/rules/<category>/<rule>/<rule>_test.rego

サンプルコード

ファイル単位で検査する

例えば各ファイルの package

  • acme.corp
  • system.log

のいずれかで始まっていることを検査するカスタムルールは以下のようになります。

# METADATA
# description: All packages must use "acme.corp" base name
# related_resources:
# - description: documentation
#   ref: https://www.acmecorp.example.org/docs/regal/package
# schemas:
# - input: schema.regal.ast
package custom.regal.rules.naming["acme-corp-package"]

import rego.v1

import data.regal.result

report contains violation if {
    not acme_corp_package
    not system_log_package

    violation := result.fail(rego.metadata.chain(), result.location(input["package"].path[1]))
}

acme_corp_package if {
    input["package"].path[1].value == "acme"
    input["package"].path[2].value == "corp"
}

system_log_package if {
    input["package"].path[1].value == "system"
    input["package"].path[2].value == "log"
}

複数のファイルをまとめて検査する

例えば

default allow := false

のように

  • 名前が allow
  • デフォルト値が false

なルールがプロジェクト全体で 1 つ以上存在することを検査するカスタムルールは以下のようになります。

# METADATA
# description: |
#   There must be at least one boolean rule named `allow`, and it must
#   have a default value of `false`
# related_resources:
# - description: documentation
#   ref: https://www.acmecorp.example.org/docs/regal/aggregate-allow
# schemas:
# - input: schema.regal.ast
package custom.regal.rules.organizational["at-least-one-allow"]

import rego.v1

import data.regal.ast
import data.regal.result

aggregate contains entry if {
    # ast.rules is input.rules with functions filtered out
    some rule in ast.rules

    # search for rule named allow
    ast.ref_to_string(rule.head.ref) == "allow"

    # make sure it's a default assignment
    # ideally we'll want more than that, but the *requirement* is only
    # that such a rule exists...
    rule["default"] == true

    # ...and that it defaults to false
    rule.head.value.type == "boolean"
    rule.head.value.value == false

    # if found, collect the result into our aggregate collection
    # we don't really need the location here, but showing for demonstration
    entry := result.aggregate(rego.metadata.chain(), {"package": input["package"]}) # optional metadata here
}

# METADATA
# description: |
#   This is called once all aggregates have been collected. Note the use of a
#   different schema here for type checking, as the input is no longer the AST
#   of a Rego policy, but our collected data.
# schemas:
#   - input: schema.regal.aggregate
aggregate_report contains violation if {
    # input.aggregate contains only the entries collected by *this* aggregate rule,
    # so you don't need to worry about counting entries from other sources here!
    count(input.aggregate) == 0

    # no aggregated data found, so we'll report a violation
    # another rule may of course want to make use of the data collected in the aggregation
    violation := result.fail(rego.metadata.chain(), {"message": "At least one rule named `allow` must exist, and it must have a default value of `false`"})
}

テストコード

例えば先ほどの custom.regal.rules.naming["acme-corp-package"] パッケージをテストするテストコードは以下のようになります。

package custom.regal.rules.naming["acme-corp-package_test"]

import rego.v1

import data.custom.regal.rules.naming["acme-corp-package"] as rule

test_acme_corp_package_allowed if {
    module := regal.parse_module("example.rego", "package acme.corp.foo")
    r := rule.report with input as module
    count(r) == 0
}

test_system_log_package_allowed if {
    module := regal.parse_module("example.rego", "package system.log.foo")
    r := rule.report with input as module
    count(r) == 0
}

test_foo_bar_baz_package_not_allowed if {
    module := regal.parse_module("example.rego", "package foo.bar.baz")
    r := rule.report with input as module
    r == {{
        "category": "naming",
        "description": "All packages must use \"acme.corp\" base name",
        "level": "error",
        "location": {
            "col": 9,
            "end": {
                "col": 12,
                "row": 1,
            },
            "file": "example.rego",
            "row": 1,
            "text": "package foo.bar.baz",
        },
        "related_resources": [{
            "description": "documentation",
            "ref": "https://www.acmecorp.example.org/docs/regal/package",
        }],
        "title": "acme-corp-package",
    }}
}

カスタムルールの書き方

格納場所

カスタムルールのソースコード.regal/rules 配下に格納します。特別な要件がなければ regal new rule コマンドに倣って以下のパスに格納するのがよいでしょう。

.regal/rules/custom/regal/rules/<category>/<rule>/<rule>.rego

もし regal/rules 以外の場所に格納する場合は regal の実行時に --rules フラグでパスを指定します。

パッケージ

カスタムルールのパッケージは以下の命名規則に従う必要があります。

custom.regal.rules.<category>.<rule>

メタデータ

メタデータについては以下の公式ドキュメントをご参照ください。

Open Policy Agent | Policy Language | Metadata

description (必須)

カスタムルールの概要を記述します。

description: All packages must use "acme.corp" base name

カスタムルールのドキュメントの URL を記載します。 descriptiondocumentation にしないと regal lint の実行結果には表示されません。

related_resources:
  - description: documentation
    ref: https://www.acmecorp.example.org/docs/regal/package

schema (任意)

スキーマについては以下の公式ドキュメントをご参照ください。

Open Policy Agent | Policy Language | Schema

Regal では以下のスキーマが提供されています。

詳しくは後ほど説明します。

schemas:
  - input: schema.regal.ast

ルール

実装方法ごとに必要なルールを実装します。

ファイル単位で検査する場合は以下のルールが必要です。

  • report

複数のファイルをまとめて検査する場合は以下のルールが必要です。

  • aggregate
  • aggregate_report

report

ファイル単位でカスタムルールの検査を行います。

report contains violation if {
    # 1. ファイル単位でカスタムルールの検査を行う。
    # ...

    # 2. 違反が発見された場合は report の要素に result.fail が生成するオブジェクトを加える。
    violation := result.fail(rego.metadata.chain(), result.location(obj))
}

input には各ファイルの Rego コードの抽象構文木を Regal 用に最適化したものが格納されます。スキーマschema.regal.ast です。具体的な内容は以下のコマンドで確認できます。

regal parse <検査対象ファイル>

ルール違反が発見された場合は report ルールの要素に result.fail 関数が生成するオブジェクトを加えます。

aggregate

各ファイルからデータを収集します。

aggregate contains entry if {
    # 1. 各ファイルからデータを収集する。
    # ...

    # 2. aggregate の要素に result.aggregate が生成するオブジェクトを加える。
    entry := result.aggregate(rego.metadata.chain(), {...})
}

inputreport ルールと同じです。

必要に応じて aggregate ルールの要素に result.aggregate 関数が生成するオブジェクトを加えます。このデータは後述する aggregate_report ルールに渡されます。

aggregate_report

aggregate ルールの結果をもとにカスタムルールの検査を行います。

aggregate_report contains violation if {
    # 1. aggregate の結果に対してカスタムルールの検査を行う。
    # ...

    # 2. 違反が発見された場合は aggregate_report の要素に result.fail が生成するオブジェクトを加える。
    violation := result.fail(rego.metadata.chain(), {...})
}

inputスキーマschema.regal.aggregate です。 input.aggregate には aggregate ルールの結果が配列として格納されます。

ルール違反が発見された場合は aggregate_report ルールの要素に result.fail 関数が生成するオブジェクトを加えます。

Regal が提供する主な補助関数

Regal が提供する補助関数のうち主に利用するものを以下に紹介します。

result.fail(metadata, details)

# METADATA
# description: |
#   helper function to call when building the "return value" for the `report` in any linter rule —
#   recommendation being that both built-in rules and custom rules use this in favor of building the
#   result by hand
# scope: document
fail(metadata, details) := ...

report , aggregate_report ルールの要素となるオブジェクトを生成します。第一引数 metadata には rego.metadata.chain() を、第二引数 details には result.location が生成するオブジェクトを指定します。

result.location(x)

# METADATA
# description: |
#   returns a "normalized" location object from the location value found in the AST.
#   new code should most often use one of the ranged_ location functions instea, as
#   that will also include an `"end"` location attribute
# scope: document
location(x) := ...

fail の第二引数に渡すオブジェクトを生成します。第一引数 x には input のうちルール違反が発見された要素を指定します。

result.aggregate(chain, aggregate_data)

# METADATA
# description: |
#  The result.aggregate function works similarly to `result.fail`, but instead of producing
#  a violation returns an entry to be aggregated for later evaluation. This is useful in
#  aggregate rules (and only in aggregate rules) as it provides a uniform format for
#  aggregate data entries. Example return value:
#
#  {
#      "rule": {
#          "category": "testing",
#          "title": "aggregation",
#      },
#      "aggregate_source": {
#          "file": "policy.rego",
#          "package_path": ["a", "b", "c"],
#      },
#      "aggregate_data": {
#          "foo": "bar",
#          "baz": [1, 2, 3],
#      },
#  }
#
aggregate(chain, aggregate_data) := ...

aggregate ルールの要素となるオブジェクトを生成します。第一引数 chain には rego.metadata.chain を、第二引数 aggregate_data には任意のオブジェクトを指定します。

regal.parse_module(filename, policy)

Rego コードの抽象構文木を Regal 用に最適化したものを返します。第一引数 filename にはファイル名、第二引数 policy には Rego コードを指定します。

この補助関数は主にテストで使用します。この補助関数の戻り値に対してカスタムルールが期待した結果を生成するかテストします。

rule.report == want with input as regal.parse_module(filename, policy)

テストを実行する

カスタムルールのテストコードを評価するには以下のコマンドを実行します。

regal test <path>

カスタムルールおよびそのテストコードは Regal が提供するライブラリ data.regal.*スキーマ schema.regal.ast を利用しています。そのため opa test コマンドではテストを実行できません。

おわりに

この記事では Rego の Linter である Regal を紹介しました。

個人的にはプロジェクトで Rego を採用する際は Regal も一緒に導入することを強く推奨します。Rego は事例が少なく、実装経験が豊富なエンジニアはあまり多くありません。Regal を導入することで Rego の経験の浅いエンジニアでもベストプラクティスに則ったコードを書くことができます。

私自身、 OPA / Rego を使って Kubernetesマニフェストファイルや Terraform の構成ファイルに対するポリシーを書くことがあるのですが、今までは経験が浅く我流で Rego コードを書いていたため以下のような課題がありました。

  • よくある間違い、バグ、非効率的な処理を含むコードを誤って書いてしまう。
  • 他のメンバーにとって可読性の低いコードを書いてしまう。

現在はプロジェクトに Regal を導入してベストプラクティスやコーディング規約への準拠を強制することで Rego コードの品質や可読性に関するこれらの問題を改善できています。

最後までお読みいただき、ありがとうございました。

参考資料

私たちは一緒に働いてくれる仲間を募集しています!

クラウドアーキテクト

執筆:@shibata.takao、レビュー:Ishizawa Kento (@kent)
Shodoで執筆されました


  1. この記事では Regal v0.28.0 を使用します。
  2. 3 年前に書かれた記事のため記載内容の一部は古くなっています(特に文法など)。最新の情報は 公式ドキュメント をご参照ください。
  3. Regal は β 版のため仕様が予告なく変更される可能性があります。
  4. 公式ドキュメントでは Homebrew , asdf , pkgsrc , Nix , mason.vim が紹介されています。
  5. Regal は Rego コードが構文的に正しくコンパイル可能であることを前提としています。そのため regal lint コマンドの実行前に opa check --strict コマンドで Rego コードが構文的に正しくコンパイル可能か検証することが推奨されています。詳しくは OPA Check and Strict Mode | Styra Documentation をご参照ください。
  6. コードの自動修正を実行する際は、何か問題が発生した場合に簡単に変更を元に戻せるよう、Git などの VCS を使って変更をコミットまたはスタッシュしておくことを推奨します。
  7. regal fix コマンドの実行結果を見ると directory-package-mismatch , use-rego-v1 の 2 つに関する修正が行われたと出力されていますが、実際には directory-package-mismatch , opa-fmt , use-assignment-operator の 3 つに関する修正が行われています。なぜこうなっているかは不明です。
  8. ルールが capabilities に応じて有効化または無効化されるように実装されている必要があります。