Regoとは?OPA 1.0(Rego v1)の書き方とv0コード移行・テストの実装ガイド

Regoは、Open Policy Agent(OPA)でポリシーを記述するための宣言的な言語です。ただし、いま日本語で「rego policy」を検索して出てくる解説記事のサンプルコードは、その大半が現行のOPAでは動きません。2024年12月20日のOPA 1.0で言語仕様(Rego syntax)がRego v1へ切り替わり、allow { ... } のような書き方が構文エラーになったためです。この記事は最新版のOPA 1.18.2(2026年7月2日リリース)で実際にコンパイルとテストを通したコードだけを載せ、Rego v1の書き方、動かなくなった旧コードの移行手順、テストと運用の要点までを扱います。

まとめ

実装に入る前に、押さえるべき結論を先に示します。

  • Regoの言語仕様はOPA 1.0(2024年12月20日)でv1へ切り替わった。ルール本体の前には if が必須、部分集合ルールには contains が必須になった。
  • 旧v0構文はOPA 1.xでそのまま動かない。allow { ... }rego_parse_error: `if` keyword is required before rule body で弾かれる。
  • 移行は opa fmt --v0-v1 で自動変換できる。すぐ直せない資産は --v0-compatible フラグで延命する。
  • import rego.v1 はもう不要(書いてもno-op)。ただし import future.keywords.not だけはOPA 1.17.0で新設された現役のimportで、公式が利用を推奨している。
  • Kubernetes向けのGatekeeperは例外。ConstraintTemplateのRegoは今もv0固定なので、if を付けると壊れる。

以下、Regoの構文からRBAC認可の実装、テスト、そしてv0からの移行手順の順に見ていきます。

RegoとOPAの関係と、評価の3要素

OPAは、アプリケーションから認可やバリデーションの判断ロジックを切り離して外部で評価する汎用ポリシーエンジンです。2021年1月29日にCNCFを卒業(Graduated)しており、Kubernetes、CI/CD、APIゲートウェイなど適用先を選びません。エンジン側の全体像や導入形態はOpen Policy Agent(OPA)とは?その概要と基本的な機能で扱っているため、ここではポリシーを書く言語であるRegoに絞ります。

input・data・policyの3要素と評価の流れ

Regoの評価モデルは3つの入力で決まります。input は判定のたびに渡されるリクエスト情報(誰が・何に・どの操作をするか)、data はOPAにあらかじめ読み込ませておく参照データ(ロール定義、許可リスト、外部から配信するJSON)、そしてpolicyが拡張子 .rego のファイルに書かれたルール本体です。OPAはこの3つを突き合わせて評価し、結果をJSONで返します。返るのは真偽値だけとは限らず、オブジェクトや集合を返すルールも書けます。

inputdata を混ぜないことは、後で効いてくる設計判断です。data 側はバンドルとして配信・キャッシュされる静的な情報なので、リクエストごとに変わる値を data に押し込むと、その都度の再読み込みが必要になります。

Policy as CodeにおけるRegoの位置づけ

ポリシーをコードとして記述し、Gitでバージョン管理してレビューとテストをCIに載せる——この運用がPolicy as Codeです。Regoはその記述層を担い、同じ言語のポリシーをKubernetesのAdmission Control、Terraformの計画ファイル検査、マイクロサービス間のAPI認可に使い回せる点が、用途特化のポリシー機能との差になります。IaC側のコード規約と組み合わせる場合はTerraformコードを一貫したスタイルで書くためのガイドラインのような静的なスタイル統一と、Regoによる意味的な検査を役割分担させると重複が出ません。

Rego v1の基本構文

この記事のコードはすべてOPA 1.18.2で opa checkopa test を通しています(データ定義を含む完全形は後掲のRBACの節)。v0時代の記事と見比べると、ifcontains の有無が最大の差分になります。

ルール本体の if と、部分集合ルールの contains

ルールは「頭部(何を決めるか)」と「本体(どんな条件のときか)」で構成されます。Rego v1では本体を持つ全てのルールで if が必須です。同じ名前のルールを複数書くと、それらはOR条件として評価されます(どれか1つでも本体が成立すれば真)。

package authz

default allow := false

allow if input.user == "admin"

allow if {
	input.method == "GET"
	input.path == ["public", "index"]
}

本体の中で改行して並べた条件はAND(すべて満たす必要がある)です。default allow := false は、どのルールにも当てはまらなかったときの既定値で、これを書かないと結果がundefinedになり「拒否」として扱えません。認可ポリシーでは必ず書きます。

集合を返すルール(partial set rule)には contains が必要です。v0の deny[msg] { ... } という書き方は1.xでは通りません。

package deploy

deny contains msg if {
	input.spec.replicas < 2
	msg := "replicas must be at least 2"
}

代入と比較の使い分け(:= と == と =)

Regoには似た記号が3つあり、混同がバグの温床になります。:= はローカル変数への代入で、同じスコープ内での再代入はコンパイルエラーになります。== は比較で、左右が等しくなければその条件は成立しません。単独の = は単一化(unification)で、代入と比較のどちらにも解決されうる曖昧な演算子です。古い記事に出てくる default allow = falseinput.path = ["users"] は動きますが、意図が読み取りにくいため、新規に書くなら :=== を使い分けてください。opa check --strict を使うと、未使用のローカル変数や未使用importが検出され、inputdata と同名の変数を定義したシャドーイングもエラーになります。CIに組み込む価値があります。

some・in・every・not によるイテレーションと否定

some x in collection は、コレクションの要素を1つずつ束縛して条件を試す書き方です。1つでも条件を満たす要素があればそのルール本体は成立します(存在量化)。すべての要素が条件を満たすことを求めるなら every を使います。

package deploy

deny contains msg if {
	some container in input.spec.containers
	not container.securityContext.runAsNonRoot
	msg := sprintf("container %v must set runAsNonRoot", [container.name])
}

deny contains "all images must be pinned by digest" if not all_images_pinned

all_images_pinned if {
	every container in input.spec.containers {
		contains(container.image, "@sha256:")
	}
}

every のように「全要素が条件を満たすこと」を要求するルールは、単体では真偽値を返すだけです。上のように not と組み合わせて deny の集合へ流し込むと、違反時のメッセージとして拾えます。

not の扱いには落とし穴があります。参照したキーが存在しない場合、その式はfalseではなくundefinedになり、not を通すと真になってしまいます。上の例で securityContext フィールドそのものが無いPodは not ... runAsNonRoot が成立してdenyされますが、これは意図どおりです。一方で「値がfalseであること」を検査したいのに、フィールドの欠落まで拾ってしまうケースでは A != Bnot A == B の違いが効きます。前者は両辺が存在して初めて評価され、後者は片方がundefinedでも成立します。

認可ポリシーとしてのRego実装(RBACの例)

OPAを最初に導入する場面は、アプリケーションの認可(RBAC)がほとんどです。ロールと権限の対応を data 側に持ち、リクエストの input と突き合わせる素直なRBACは、次のように書けます。

package rbac

default allow := false

allow if {
	some role in user_roles[input.user]
	some perm in role_permissions[role]
	perm.action == input.action
	perm.resource == input.resource
}

allow if "admin" in user_roles[input.user]

allowed_resources contains r if {
	some role in user_roles[input.user]
	some perm in role_permissions[role]
	r := perm.resource
}

user_roles := {"alice": ["admin"], "bob": ["developer"]}

role_permissions := {
	"admin": [{"action": "write", "resource": "server"}],
	"developer": [{"action": "read", "resource": "server"}],
}

allow は真偽値、allowed_resources は集合を返します。UIで「このユーザーが触れるリソース一覧」を出したいときは後者を評価すればよく、判定と一覧取得を同じポリシーから引ける点がRegoの利点です。

ここでは説明のためにロール定義をRegoファイル内に埋めていますが、実運用では data としてJSONで配信します。ロールが増減するたびにポリシーコードを再デプロイせずに済み、権限表の変更を人事システムやIdPの出力から生成できるためです。ただし data へ切り出したら、ポリシー側の参照を data.rbac.user_roles のように修飾する必要があります。未修飾のままJSONへ移すと rego_unsafe_var_error になります。ロール設計そのもの(ロール爆発の回避、ABACへ切り替える判断基準)はRBACとは?ロールベースアクセス制御の仕組みと導入判断を実装視点で解説で扱っています。

属性ベース(ABAC)へ広げる場合も、Regoなら同じルールに条件を足すだけです。「営業時間内のみ」「送信元IPが社内レンジ」といった条件は input に含めて渡し、本体でAND条件として並べます。判定ロジックを外部化しておくと、この種の要件追加でアプリケーションのデプロイが不要になります。

OPAでの実行方法:CLIとREST API

書いたポリシーを動かす経路は3つあります。開発中のCLI、サービスとして常駐させるREST API、そしてGoアプリへのライブラリ組み込みです。

opa eval によるCLI評価

opa eval -d rbac.rego -i input.json "data.rbac.allow"

-d でポリシーとデータ、-i で入力JSONを指定します。結果を素の値だけ見たいなら -f pretty、条件がなぜ成立しなかったのかを追うなら --explain fails を付けます。CIで「拒否されるべき入力が拒否されること」を検証するときは --fail を付けると、undefinedのときに終了コードが非ゼロになります。

opa run とREST APIによるサーバ常駐

opa run -s -b bundle.tar.gz

ここで一つ、OPA 1.0での非構文的な変更に注意が必要です。サーバの待受アドレスの既定値が全インターフェースから localhost:8181 に変わりました。コンテナで動かしてPodの外から叩く構成では、明示的に --addr 0.0.0.0:8181 を指定しないと接続できません。v0時代の手順書をそのままなぞると、ここで詰まります。

判定リクエストは POST /v1/data/<パッケージ名>/<ルール名> に投げます。

curl -X POST localhost:8181/v1/data/rbac/allow \
  -d '{"input": {"user": "bob", "action": "read", "resource": "server"}}' \
  -H 'Content-Type: application/json'

レスポンスは既定で {"result":true} が返り、decision logsを有効にすると {"decision_id":"...","result":true} のように判定IDが付きます。ポリシー自体の管理は /v1/policies(GET・PUT・DELETE)、部分評価は /v1/compile が担当します。/v1/compile は「入力の一部が未知のまま評価して残余条件を返す」機能で、データベース検索の絞り込み条件をポリシーから生成する用途に使われます。

ポリシーのテストとlint

Regoにはテスト機構が組み込まれており、test_ で始まるルールが opa test の対象になります。with input as で入力を差し替えるのが基本形です。

package rbac_test

import data.rbac

test_admin_allowed if {
	rbac.allow with input as {"user": "alice", "action": "write", "resource": "server"}
}

test_developer_write_denied if {
	not rbac.allow with input as {"user": "bob", "action": "write", "resource": "server"}
}
opa test . -v

拒否されるべきケースを not で書くのが要点です。許可ケースだけ通しても、「すべてを許可してしまうルール」を検出できません。カバレッジは opa test --coverage で計測でき、--threshold と組み合わせればCIで基準割れを落とせます。テスト名を todo_ で始めるとスキップ扱いになります。

静的解析は2段構えです。opa check --strict がコンパイル時の問題(未使用のローカル変数・未使用import・inputdata のシャドーイング)を、Regalがスタイルとアンチパターンを検出します。Regalは開発元のStyraからOPA公式のGitHub organizationへ移管され、2026年6月2日リリースのv0.41.1が最新でステータスはbetaです。opa コマンドには統合されていないため(opa lint は存在しません)、regal lint として別途導入します。

OPA 1.0への移行手順|動かないv0コードのv1変換

ここが、この記事でもっとも実用価値のある部分です。社内Wikiや過去の技術記事からコピーしたRegoが1.xで動かない場合、原因はほぼ次の2つに集約されます。

種類 v0(1.xではエラー) v1(現行)
条件付きルール allow { input.user == "admin" } allow if { input.user == "admin" }
部分集合ルール deny[msg] { ... } deny contains msg if { ... }
キーワードのimport import future.keywords.in 不要(言語標準)
移行期のimport import rego.v1 不要(書いてもno-op)
既定値の代入 default allow = false default allow := false

実際のエラーメッセージは rego_parse_error: `if` keyword is required before rule body および rego_parse_error: `contains` keyword is required for partial set rules です。このメッセージが出たら、記事やサンプルがOPA 1.0以前のものだと判断できます。

opa fmt による自動変換と、猶予のための互換フラグ

opa fmt --v0-v1 -w policy.rego

このコマンドがv0のファイルを読み、ifcontains を補って上書きします。変換後に import rego.v1 が付与されますが、1.xでは不要なので opa fmt --drop-v0-imports で除去できます。両バージョンで動く状態を保ちたい移行期間中は、opa check --v0-v1 で「v0とv1の両方に互換であること」を検査できます。

すぐに書き換えられない大量の資産がある場合の逃げ道が --v0-compatible です。evaltestrunbuild など主要サブコマンドに用意されており、これを付けるとv0構文のまま評価できます。ただし恒久策ではありません。バンドルは .manifestrego_version フィールドで言語バージョンを持ち、このフィールドが無いバンドルはv1として扱われる仕様なので、移行を先送りするほど混在の管理コストが増えます。

全廃ではない future.keywords(not の新設)

「OPA 1.0で future.keywords のimportは全部いらなくなった」という説明は、2026年時点では正確ではありません。OPA 1.17.0(2026年5月28日)で import future.keywords.not が新設されました。これは not f(g(input.x)) のように否定の中に合成式を書いたとき、中間値がundefinedだとルール全体が失敗してしまう長年の挙動を修正するものです。v1.17.0のリリースノートは「not キーワードを使うポリシーでは常にこのimportを入れることを推奨する」としています。inif のimportが不要になったことと、not の新しいimportが推奨されることは別の話です。

Regoでつまずく落とし穴と、選ばない判断

Rego v0固定のGatekeeper ConstraintTemplate

Kubernetes向けのOPA GatekeeperはOPA本体とは別プロジェクトで、v3.23.0(2026年7月9日)が現役です。内部ではOPA v1.17系を利用していますが、Regoドライバが ast.RegoV0 を明示指定しているため、ConstraintTemplateに書くRegoはv0構文のままです。violation[{"msg": msg}] { ... } と書き、if は付けません。「OPA 1.0でRegoが変わったのだからGatekeeperも同じはず」と考えて if を足すと壊れます。OPA本体とGatekeeperで書き分ける必要がある、というのが現時点の実情です。Kubernetes側のバージョン差分はKubernetes 1.34の新機能・変更点とEOL|DRA GA・再起動ルール・EKS対応を解説で確認できます。

実在しないPrometheusメトリクスと、正しい計測手段

日本語・英語を問わず、OPAの解説記事には opa_eval_duration_secondsopa_query_cache_hit といったメトリクス名が登場します。これらは実在しません。OPA 1.18.2を素の opa run -s で起動したとき、/metrics に出るOPA固有の指標は http_request_duration_seconds だけです。opa_infoplugin_status_gaugebundle_loaded_counter 系は、設定でstatusプラグインの prometheus: true を有効にして初めて公開されます。バンドルを読ませただけでは出ません。ポリシー評価そのものにかかった時間を見たいなら、リクエストに ?metrics=true を付けてレスポンスの timer_rego_query_eval_ns を読むか、decision logsの metrics フィールドを収集します。開発時のボトルネック特定には opa eval --profile があり、式ごとの実行時間と評価回数が表示されます。

Kubernetes専用ならRegoを避ける選択肢(VAPとKyverno)

Regoの学習コストは小さくありません。そして、用途がKubernetesのAdmission Controlだけなら、Regoを避けられる道が2つあります。1つはKubernetes標準のValidating Admission Policy(CEL)で、v1.30でGAし既定で有効になりました。追加コンポーネントなしで単純なフィールド検証を書けます。もう1つはKyvernoで、こちらもCNCFを卒業(2026年3月16日)しており、ポリシーをYAMLのKubernetesリソースとして宣言し、複雑な条件はCELで補います。いずれもRegoという独立した言語の習得を前提としません。

それでもRego(OPA/Gatekeeper)を選ぶ理由は、Kubernetesの外にも同じポリシー言語を適用したいとき、あるいは外部データ参照や参照型ポリシーのようにCELでは表現しきれない判定が必要なときに絞られます。Gatekeeper公式も、CELで書けるポリシーはVAPリソースとして生成し、外部データを使うRegoポリシーはwebhookで評価する、という二層構成を前提にしています。Kubernetes単体で完結する組織が学習コストを払ってRegoを導入する必然性は薄い、というのが公式の設計思想から読み取れる線引きです。Kubernetesそのものの基礎はKubernetes(クーベネティス)とは?読み方・K8sの意味から仕組みまで初心者向けに解説を参照してください。

本番運用でのポリシー配信と監査

ポリシーをファイルで直接読ませるのは開発時の話で、本番ではバンドル(bundle)で配信します。バンドルはポリシーとデータをまとめた .tar.gz で、OPAが指定のHTTPエンドポイントから定期的に取得して自動で有効化します。改ざん対策として .signatures.json によるJWT署名に対応しており、署名鍵を opa build --signing-key で指定します。データのみの差分更新(delta bundle)も可能ですが、ポリシーの差分更新はできません。

監査要件があるなら、decision logsを有効にします。判定ごとに decision_id、評価パス、input、結果、タイムスタンプが記録され、外部サービスへ送信するかコンソールへ出力できます。個人情報を含む input をそのまま出したくない場合は mask_decision でマスク対象をRegoで記述します。「誰のどのリクエストが、どのルールで拒否されたか」を後から追えることが、認可ロジックを外部化する実利の一つです。

評価性能が問題になる規模では、opa build -O による最適化(部分評価によるルールの事前展開)を検討します。ただし、多くの環境ではポリシーの評価時間よりバンドル配信の遅延やネットワーク往復のほうが支配的です。opa eval --profile で実測してから手を入れてください。

よくある質問

Regoの読み方は何ですか?

OPA公式ドキュメントは「ray-go(レイゴー)」と発音すると明記しています(日本語圏では「レゴ」と読まれることも多くあります)。Regoは、Datalogに着想を得てJSONのような構造化データを扱えるよう拡張した言語だと公式は説明しています。ファイルの拡張子は .rego です。

ネットのサンプルコードが opa eval でエラーになります

rego_parse_error: `if` keyword is required before rule body が出ているなら、そのコードはOPA 1.0より前のRego v0構文です。opa fmt --v0-v1 -w ファイル名 で自動変換するか、暫定的に opa eval --v0-compatible を付けて評価してください。日本語圏の解説記事は2020〜2022年に書かれたものが上位に多く残っており、その大半がv0構文のままです。

import rego.v1 は書くべきですか?

OPA 1.x では不要です。書いてもエラーにはなりませんが何もしません(no-op)。opa fmt --drop-v0-imports で除去できます。ただし import future.keywords.not は別で、OPA 1.17.0で新設された現役のimportであり、not を使うポリシーでは v1.17.0 のリリースノートが利用を推奨しています。

OPAの最新バージョンはいくつですか?

2026年7月時点の最新安定版はv1.18.2(2026年7月2日リリース)です。opa version を実行すると Rego Version: v1 が表示され、既定の言語バージョンを確認できます。リリース頻度が高いため、正確な最新版はGitHubのリリースページで確認してください。

RegoとKyvernoはどちらを選ぶべきですか?

Kubernetes内でのポリシー適用しか必要ないなら、YAMLとCELでポリシーを宣言するKyverno、または標準機能のValidating Admission Policy(v1.30でGA)で足ります。Kubernetesの外——CI/CD、API認可、Terraformの計画検査——にも同じポリシー言語を使いたい場合や、外部データを参照する判定が必要な場合にRego(OPA)を選ぶ価値が出ます。

関連記事

資料請求

RELATED POSTS 関連記事