こんにちは。X(クロス) イノベーション 本部 クラウド イノベーション センターの柴田です。この記事では Rego の Linter である Regal を紹介します。 1 この記事は 電通総研 Advent Calendar 2024 の 5 日目の投稿です。前日の記事は井手さんの「入社 3 年目の業務内容紹介」でした。 Open Policy Agent / Rego とは? Regal とは? Regal を実行する Lint を実行する ローカルで実行する インストールする 実行する CI で実行する エディタと組み合わせて実行する コードを自動修正する ルール 組み込みルール カスタムルール Regal を設定する 設定ファイル 格納場所 サンプル 設定ファイルの書き方 rules ignores capabilities project デフォルトの設定値 ルールを抑制する inline directive CLI フラグ 設定ファイル プロジェクトルート カスタムルールを実装する カスタムルールの種類 雛形を作成する サンプルコード ファイル単位で検査する 複数のファイルをまとめて検査する テストコード カスタムルールの書き方 格納場所 パッケージ メタデータ description (必須) related_resources (任意) schema (任意) ルール report aggregate aggregate_report Regal が提供する主な補助関数 result.fail(metadata, details) result.location(x) result.aggregate(chain, aggregate_data) regal.parse_module(filename, policy) テストを実行する おわりに 参考資料 Open Policy Agent / Rego とは? Open Policy Agent (OPA) と Rego の概要については以下の記事をご参照ください。 2 Policy as Code を実現する Open Policy Agent / Rego の紹介 - 電通総研 テックブログ Regal とは? Regal は Styra 社が開発している Rego の Linter です。 3 Regal を使用して ベストプ ラク ティスに反したコード よくある間違い、バグ、非効率的な処理を含むコード プロジェクト独自のコーディング規約に反したコード などを 機械的 に発見することで Rego コードの品質や可読性を向上できます。 Regal を実行する Lint を実行する ローカルで実行する インストールする 以下の 3 つの方法があります。 パッケージマネージャーを使ってインストールする。 4 GitHub の release asset をダウンロード&インストールする。 Docker を使う。 具体的なインストール手順はここでは省略します。詳しくは以下の公式ドキュメントをご参照ください。 Download Regal | Styra Documentation Packaging | Styra Documentation 実行する 以下のような 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 は以下のルールに関してコードを自動的に修正できます。 opa-fmt use-rego-v1 use-assignment-operator no-whitespace-comment directory-package-mismatch 先ほどのサンプルコード 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 : ニーズに合わせてカスタマイズすべきルール。 具体的なルールの内容は以下の公式ドキュメントをご参照ください。 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 は以下の ディレクト リをプロジェクトルートとして認識します。 設定ファイルの .project.roots で指定された ディレクト リ .manifest ファイルが存在する ディレクト リ .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 コードの品質や可読性に関するこれらの問題を改善できています。 最後までお読みいただき、ありがとうございました。 参考資料 Regal | Styra Documentation 私たちは一緒に働いてくれる仲間を募集しています! クラウドアーキテクト 執筆: @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 に応じて有効化または無効化されるように実装されている必要があります。 ↩