こんにちは。X(クロス)イノベーション本部クラウドイノベーションセンターの柴田です。この記事では Rego の Linter である Regal を紹介します。1この記事は 電通総研 Advent Calendar 2024 の 5 日目の投稿です。前日の記事は井手さんの「入社 3 年目の業務内容紹介」でした。
- Open Policy Agent / Rego とは?
- Regal とは?
- Regal を実行する
- ルール
- Regal を設定する
- カスタムルールを実装する
- おわりに
- 参考資料
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 からの素早いフィードバック
- コード補完
- マウスオーバー時のツールチップの表示
- 定義への移動
などの恩恵を受けることが可能です。
本記事ではこれらの詳細は扱いません。詳しくは以下の公式ドキュメントをご参照ください。
- Editor support | Styra Documentation
- Language Server | Styra Documentation
- Debug Adapter | Styra Documentation
コードを自動修正する
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
- directory-package-mismatch に従ってファイルが
policy/authz.rego
からpolicy/authz/authz.rego
へ移動されました。 - opa-fmt と use-assignment-operator に従ってファイルの中身が以下のように修正されました。
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 : ニーズに合わせてカスタマイズすべきルール。
具体的なルールの内容は以下の公式ドキュメントをご参照ください。
カスタムルール
独自のルールを定義することもできます。詳しくは後ほど説明します。
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
: ルールを無効化します。
ルールによっては上記に加えて固有の設定項目があります。
詳しくは以下の公式ドキュメントに記載された各ルールの説明文をご参照ください。
ignores
ignores
にはすべてのルールの適用対象外とするファイルを指定します。
capabilities
capabilities
には OPA のランタイムバージョンなどを指定します。
指定したランタイムバージョンでは利用できない機能や関数に依存したルールは無効化されます。8
主な設定項目を以下に示します。
キー | 説明 |
---|---|
capabilities.from |
OPA のランタイムバージョンを指定します。このバージョンでは利用できない機能や関数に依存したルールは無効化されます。[^8]指定しない場合は Regal がリリースされた時の最新の OPA のランタイムバージョンが使用されます。 |
capabilities.plus |
利用できる機能や関数を追加で指定します。 |
capabilities.minus |
利用できない機能や関数を追加で指定します。 |
詳しくは以下の公式ドキュメントをご参照ください。
Capabilities | Styra Documentation
project
プロジェクトのルートディレクトリを指定します。詳しくは後ほど説明します。
デフォルトの設定値
ユーザが設定ファイルで明示的に値を設定していない設定項目については以下の値が使用されます。
ルールを抑制する
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
related_resources
(任意)
カスタムルールのドキュメントの URL を記載します。 description
を documentation
にしないと regal lint
の実行結果には表示されません。
related_resources: - description: documentation ref: https://www.acmecorp.example.org/docs/regal/package
schema
(任意)
スキーマについては以下の公式ドキュメントをご参照ください。
Open Policy Agent | Policy Language | Schema
Regal では以下のスキーマが提供されています。
schema.regal.ast
:report
,aggregate
ルールのinput
のスキーマschema.regal.aggregate
:aggregate_report
ルールのinput
のスキーマ
詳しくは後ほど説明します。
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(), {...}) }
input
は report
ルールと同じです。
必要に応じて 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で執筆されました)
- この記事では Regal v0.28.0 を使用します。↩
- 3 年前に書かれた記事のため記載内容の一部は古くなっています(特に文法など)。最新の情報は 公式ドキュメント をご参照ください。↩
- Regal は β 版のため仕様が予告なく変更される可能性があります。↩
- 公式ドキュメントでは Homebrew , asdf , pkgsrc , Nix , mason.vim が紹介されています。↩
-
Regal は Rego コードが構文的に正しくコンパイル可能であることを前提としています。そのため
regal lint
コマンドの実行前にopa check --strict
コマンドで Rego コードが構文的に正しくコンパイル可能か検証することが推奨されています。詳しくは OPA Check and Strict Mode | Styra Documentation をご参照ください。↩ - コードの自動修正を実行する際は、何か問題が発生した場合に簡単に変更を元に戻せるよう、Git などの VCS を使って変更をコミットまたはスタッシュしておくことを推奨します。↩
-
regal fix
コマンドの実行結果を見ると directory-package-mismatch , use-rego-v1 の 2 つに関する修正が行われたと出力されていますが、実際には directory-package-mismatch , opa-fmt , use-assignment-operator の 3 つに関する修正が行われています。なぜこうなっているかは不明です。↩ -
ルールが
capabilities
に応じて有効化または無効化されるように実装されている必要があります。↩