オープンポリシーエージェント(OPA)とは?
システム内で設定し、維持管理しなければならない、いわゆるポリシー(またはルール)がどれほど多いか、考えたことはありますか?例えば、アプリケーション、ネットワーク、コードリポジトリ、コードデプロイメント、CI/CDパイプラインなど、システム内の様々な場所でポリシーを設定しているかもしれません。
朗報です!これらのポリシーを管理するより良い方法があります。さっそくご紹介しましょう!
OPAをご紹介いたします。OPA(オーパと発音)はOpen Policy Agentの略称で、スタック全体にわたるポリシー適用を統一するオープンソースの汎用ポリシーエンジンです。例えば、管理者ユーザーと一般ユーザーで異なるアクセスレベルを設定するアプリケーションの場合、OPAを活用してユーザータイプごとに異なるアクセス権を付与することが可能です。GitHub Actionsのようなツールをご利用の場合、OPAを活用して、コードがイメージを取得するリポジトリが正しく定義され許可されているかどうかの確認なども行えます。
オープンポリシーエージェント(OPA)とは?
ここで学ぶ内容
-
オープンポリシーエージェントとは何か、そしてその仕組みについて
-
Regoでのポリシー記述方法
-
KubernetesでOPAを有効化する方法
関心事の分離または分離設計
separation of concerns(関心事の分離)、model-view-controller (モデル・ビュー・コントローラ(MVC))、あるいはmodel-view-presenter (モデル・ビュー・プレゼンタ(MVP))といった用語をご存知でしょうか?
これらの手法が導入される以前には、スパゲッティコードと呼ばれる状態が存在しておりました。プログラミングにおいて、これはスタイルシート(CSS)がアプリケーションロジック(PHP、Python、Perlなどで記述される場合あり)と混在していたり、データ操作(データベースへのレコード追加・削除・更新など)がアプリケーションロジックやビジネスロジック内で定義されていた状態を指します。要するに、全てが一箇所に混在していたのです。
このようなスパゲッティコードが存在すると、プレゼンテーション層、データ層、アプリケーション層のいずれも相互に密接に関連しているため、変更を加えることが困難になります。また、本番環境への変更のデプロイも非常に困難な作業となる可能性があります。
この問題を解決するのに役立つのが分離(デカップリング)です。関心事の分離、MVC、MVPはいずれも分離の例と言えます。OPAでは、ポリシーの意思決定において分離(関心事の分離)が行われます。つまり、ポリシーをアプリケーションロジックから切り離すのです。これにより、分散した複雑なシステム群におけるポリシー管理の難しい課題を軽減することが可能となります。
ポリシーとは?
OPAは、ポリシー決定を必要とするあらゆるサービスやアプリケーション向けの汎用ポリシーエンジンとして機能します。
では、ポリシーとは何でしょうか?冒頭で述べたように、システム内には「許可される行為と禁止される行為」が定義されているでしょう。例えば、アプリケーション、データベースロジック、プレゼンテーション層、ネットワーク(ファイアウォール)、ストレージへのアクセス権限など、様々な領域においてです。ポリシーとは、システムやアプリケーション、ネットワーク、インフラストラクチャを特定の方法で動作させる(他の方法では動作させない)ために作成・実装されるルールやガイドラインに他なりません。
例えば、アプリケーションに一般ユーザーと管理者の2種類のユーザーが存在するとします。管理者には管理画面へのアクセスを許可し、一般ユーザーには非管理画面のみへのアクセスを許可するポリシーを作成できます。また、特定のIPアドレスやヘッダー情報へのアクセス制限、あるいは特定のコンテナレジストリ(GCR.io、Quay.io、または自社内のコンテナレジストリなど)のみへのアクセス制限も可能です。
この時点で、ポリシーエンジンを別途用意せず、そのロジックをアプリケーション内に組み込めないかとお考えかもしれません。その答えは「はい、可能です」となります。ただし繰り返しになりますが、ポリシーを分離して関心を切り離すことで、あるチームがポリシーに、別のチームがアプリケーション構築に集中できるようにすることが目的です。
オープンポリシーエージェントはどのように機能しますか?
ポリシーについてご理解いただけたところで、OPAの仕組みについてご説明いたします。まずは簡単な例から始めましょう。
以下のイメージは、アプリケーションのフローを示しており、ポリシーとOPAがユーザーのリクエストをどのように処理するかが含まれています:

上記のイメージにおけるOPAの動作は次のとおりです:
- ユーザーがアプリケーションまたはサービスにリクエストを送信します。
- リクエストには、
「role」: 「admin」
という値を持つポリシー入力(JSON)が含まれます。 - OPAは入力を受け取り、処理します。
- OPAで定義されたポリシーに基づき、出力として
「admin_allowed」: true
が返されます。これは、このユーザーがすべての管理者ページへのアクセスを許可されていることを意味します。
定義されたポリシーに基づき、入力ドキュメントと正しいポリシー決定を表す出力ドキュメントとの間には対応関係があります。この対応関係はOPAポリシーによって定義されます。
オープンポリシーエージェントはどのように実装されるのでしょうか?
ポリシーとは何か、またリクエストを処理し結果を返す仕組みをご理解いただいたところで、次にポリシーの記述方法をご説明いたします。
図1の例を用いてみましょう。最も基本的な(そして最もシンプルな)ポリシーは以下のようになります:
admin_allowed := true
Code language: Perl (perl)
この変数代入により、入力内容に関わらずadmin_allowed
は常にtrue
となります。その結果、ユーザーの役割に関係なく、すべてのユーザーが管理ページにアクセスできるようになります。これは無条件代入と呼ばれます。
しかし、これは望ましい状態ではありません。管理ページにはクレジットカード番号などの請求情報や、特定のユーザーのみがアクセスできるべき機密情報が含まれる可能性があるためです。私たちが求めるのは条件付き代入と呼ばれるものです。具体的には、管理ページへのアクセスを管理者ロールを持つユーザーに限定したいのです。具体的には以下のようになります:
admin_allow := true if {
input.role == "admin"
}
Code language: Perl (perl)
上記は条件付き変数割り当てであり、条件ブロックが成功した場合にのみ実行されます。ただし、ポリシー入力でadmin
ロールが指定されていない場合、条件ブロックは失敗し、ルールは適用されません。ルールが適用されないため、出力にはadmin_allow
フィールドが含まれません。したがって、設定されたポリシーに基づいてポリシーエンジンからの期待される出力を理解することは、アプリケーションまたはサービスの責任となります。
これがOPAポリシー作成の核心となります。
OPA言語
ポリシーについてご説明いたしましたので、次にOPA言語についてもう少し詳しく見ていきましょう。
前のセクションでは、条件ブロックが満たされた場合に変数への代入を行う条件付きルールについてご説明いたしました。では、OR条件についてはどうでしょうか。OR条件は、同じ変数名を持つ複数のルールがある場合にご利用いただけます。
admin_allow := true if {
input.role == "admin"
}
admin_allow := true if {
input.is_billing_enabled == "yes"
}
Code language: Perl (perl)
例えば、入力ロールが admin
であり、is_billing_enabled
が no
であるとします。すると、出力は admin_allow
が true
となります。
次にAND条件について見ていきましょう。早速例を見てみましょう:
admin_allow := true if {
input.role == "admin"
}
can_see_billing := true if {
input.is_billing_enabled == "yes"
}
Code language: Perl (perl)
ここでは、2つの条件があります。1つはadmin_allow
、もう1つはcan_see_billing
です。両方の割り当て変数が真の場合(input.role
がadmin
かつinput.is_billing_enabled
がyes
の場合に発生します)、この出力はtrue
となります。しかし、両方が偽の場合(例えば、input.role
が regular
であり、かつ input.is_billing_enabled
が no
である場合)、出力は false
となります。基本的に、両方の代入変数が真であること(この場合、両方の変数代入に対する条件も真であること)が真の出力を得るための条件であり、そうでない場合は偽となります。
OPAではルールチェーニングも可能です。ルールチェーニングとは何でしょうか?ご説明いたします。
admin_allow := true if {
is_billing_enabled == true
}
is_billing_enabled := true if {
input.cc_info_onfile == "yes"
}
is_billing_enabled := true if {
input.cc_not_expired == "yes"
}
Code language: Perl (perl)
この状況では、出力変数を用いて他の出力変数を生成することが可能です。当ケースでは、is_billing_enabled
変数が中間変数として機能し、admin_allow
変数を決定する役割を果たします。上記の例におけるis_billing_enabled
変数は、補助ルールと呼ばれることがあります。
もう一点重要な点として、ルールの順序は問題になりません。例えば、最上位のルールでは、後続のルールによって定義されるis_billing_enabled
変数を使用しています。この順序が逆になるシーケンスも、OPAでは問題となりません。OPAは、Kubernetesの出力やTerraformプランのような階層的なデータとも完全に連携します。これについては後ほど詳しく説明します。
多くのプログラミング言語と同様に、OPAもパッケージ
をサポートしております。パッケージ
は、ポリシールールをモジュールと呼ばれるファイルに整理する方法です。各モジュールについて、これらのルール群が属するパッケージパスを指定するために、パッケージ宣言を定義する必要があります。例を見てみましょう:
package policy.access
admin_allow := true if {
input.role == "admin"
}
admin_allow := true if {
input.is_billing_enabled == true
}
regular_allow := true if {
input.role == "regular"
}
Code language: Perl (perl)
上記の例では、policy.access
パッケージパス内にモジュール宣言しました。別のパッケージでは、以下のようにdata.policy.access
という参照を使用できます:
package main
import data.policy.access
can_see_billing := true if {
access.admin_allow == true
}
can_place_order := true if {
access.regular_allow := true
}
Code language: Perl (perl)
ご覧の通り、OPAでは.
(ドット)演算子を用いてデータのクエリやアクセスを行います。
Rego
Regoは、OPAポリシーを表現するために用いられる高水準の宣言型言語です。その宣言的な性質ゆえ、Regoでポリシーを記述する際には少々注意が必要です。Regoを最初から最後まで解説することは本記事の範囲を超えますので、ページ末尾の参考文献セクションにRego理解のための完全ガイドへのリンクを追加いたしました。ここでは、すぐに本題に入らせていただきます。
前のセクションで取り上げた条件付き代入またはルール(AND)の例を用いて説明いたします:
admin_allow if {
input.role == "admin"
input.is_billing_enabled == true
}
Code language: Perl (perl)
期待される出力は以下の通りです:
input.role
がadmin
であり、かつinput.is_billing_enabled
がtrue
である場合、Trueを返します。- {}または、
input.role
がadmin
でない場合、またはinput.is_billing_enabled
がfalse
である場合、もしくはinput.role
がadmin
でない場合、またはinput.is_billing_enabled
がfalse
である場合、Falseを返します。
コード、入力、出力はOPAプレイグラウンドこちらでご確認いただけます。
次に、条件付き割り当てまたはルール(OR)の例を見てみましょう:
admin_allow := true if {
input.role == "admin"
}
admin_allow := true if {
input.is_billing_enabled == true
}
Code language: Perl (perl)
期待される出力は以下の通りです:
input.role
がadmin
である場合、またはinput.is_billing_enabled
がtrue
である場合に真(True)を返します。input.role
がadmin
でない場合、かつinput.is_billing_enabled
がfalse
である場合に、空({})または偽(false)を返します。
コード、入力、出力はOPAプレイグラウンドこちらでご確認いただけます。
それでは、セットと貪欲探索についてご説明いたします。
allow[reason] {
a := input.access[_]
m := a.mode
m == "special"
input.type == "POST"
reason := sprintf("mode '%s' exists and allowed to special access!", [m])
}
Code language: Perl (perl)
まず、_
の意味を理解する必要があります。上記の例では、演算子がinput.access
から.
(ドット)演算子を用いてセットを作成しています。このセット内のキーを指定することで、値を取得することが可能です。
例えば、次のような入力があるとします:
{
"type": "POST",
"access": [
{
"mode": "regular",
"type": "member"
},
{
"mode": "special",
"type": "acct-admin"
}
]
}
Code language: Perl (perl)
_
演算子は、配列の全内容、すなわち開始角括弧と終了角括弧[ ]
の間に存在するすべての要素を取得します。m := a.mode
は、mode
を取得するための変数代入です。a := input.access [_]
から得られるmode
は2つ存在しますが、m == 「special」
は、『mode』: 「special」
が集合の最初または最後のシーケンスにあるかに関わらず、真を返します。
少し分かりにくいかもしれませんので、実際の例でご説明いたします。こちらのOPAプレイグラウンドのコメントを参照しながら進めてください。先述の通り、Regoの記述方法を解説した資料は多数存在します。OPAプレイグラウンドで実践的な経験を積むことも可能です。ぜひお試しください。
これまでのセクションで学んだ情報を踏まえ、組織内でこの知識をどのように活用できるかお考えかもしれません。例えば、クラウドインフラ(Kubernetesなど)でOPAをどのように活用できるでしょうか?コードリポジトリ(Bitbucket、GitLab、GitHubなど)との連携はどのように機能するでしょうか?TerraformのようなInfrastructure-as-Code(IaC)ソフトウェアとの併用は可能でしょうか?
見てみましょう。
Kubernetes
KubernetesでOPAを有効化する方法は2通りございます:Admission Controller(具体的にはValidatingAdmissionWebhook
アドミッションコントローラーを有効化する方法)と、OPA Gatekeeperを使用する方法です。後者の設定がより容易でございます。(Gatekeeperを使用したOPAの設定方法についてはこちらをご参照ください。)KubernetesでOPAを使用する際には、以下のような設定が必要となります:
- 新規ネームスペースへの必須ラベルの追加。
- セキュリティ上の理由から、Kubernetesリソースが使用できるリポジトリの定義。
- コンテナ内のリソース制限と要求の定義。
- その他の例はOPAのウェブサイトでご確認いただけます。
以下では、Gatekeeper を使用したネームスペースへの必須「owner」ラベルの例をご紹介します(Kubernetes での Gatekeeper の設定は既に完了しているものと仮定します)。
まず、ConstraintTemplate を定義する必要があります。この ConstraintTemplate 内で Rego および強制アクションを定義します。
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
validation:
openAPIV3Schema:
type: object
properties:
labels:
type: array
items:
type: string
targets:
target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
violation[{"msg": msg, "details": {"missing_labels": missing}}]{
provided := {label | input.review.object.metadata.labels[label]}
missing := required - provided
count(missing) > 0
msg := sprintf("Label is required: %v", [missing])
}
Code language: Perl (perl)
ConstraintTemplate
を作成した後、そのテンプレートに基づいて制約を作成する必要があります。
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: ns-musth-have-gk
spec:
enforcementAction: warn ### deny(default), dryrun, warn
match:
kinds:
apiGroups: [""]
kinds: ["Namespace"]
parameters:
labels: ["gatekeeper"]
Code language: Perl (perl)
ご覧の通り、enforcementAction
には3つのタイプがございます:deny
(デフォルト)、dryrun
、およびwarn
です。ご利用になるタイプは、組織のワークフローによって異なります。warn
を選択した場合でも、新しいネームスペースの作成は可能です。ただし、そのネームスペース作成時にラベルを指定しない場合、警告が発生します。例えば:
$> kubectl create ns gatekeeper-test
Warning: [ns-must-have-gk] you must provide labels: {"gatekeeper"}
namespace/gatekeeper-test created
Code language: Perl (perl)
GitHub
GitHubは、もはや単なるコードリポジトリやコードのホスティングツールとしてのみ利用されるものではありません。コードリポジトリからCI/CDツールおよびアーティファクトリポジトリへと進化を遂げており(将来的にはさらに機能が追加される可能性があります)、
GitHubはCI/CDツールとして機能させるため、GitHub Actionsを導入しました。GitHub Actionsは、ビルド、テスト、デプロイのパイプラインを自動化できる継続的インテグレーションおよび継続的デリバリー(CI/CD)プラットフォームです。リポジトリへのプルリクエストごとにビルドとテストを実行するワークフローを作成したり、マージされたプルリクエストを本番環境にデプロイしたりすることが可能です。これにより、本番環境へのデプロイ前にリポジトリ内の全ポリシーに対してOPAを実行できるため、非常に有用です。

上記のフローチャートでは、定義されたすべてのポリシーに対するOPAテストが失敗した場合、修正が完了するまでGitHubがコードのマージを防止すべきであることが確認できます。
フローチャートに示されているGitHub Actionsの例を以下に示します:
name: Run OPA Tests
on: [push]
jobs:
Run-OPA-Tests:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Setup OPA
uses: open-policy-agent/setup-opa@v2
with:
version: latest
- name: Run OPA Tests
run: opa test tests/*.rego -v
Code language: Perl (perl)
Terraform
Terraformは、インフラストラクチャを安全かつ予測可能に作成、変更、改善することを可能にするオープンソースのInfrastructure-as-Codeソフトウェアツールです。
以下は、すべてのGoogleプロジェクトに「ラベル」と「所有者」を必須とするポリシーの例です:
package terraform
import input.tfplan as tfplan
import input.tfrun as tfrun
identifiers {
r := tfplan.resource_changes[_]
"google_project" == r.type
some i, j
r.instances[i].attributes.labels
r.instances[j].attributes.labels.owner
}
deny[reason] {
not identifiers
reason := "Type of 'google_project' has to have 'labels' and 'owner'."
}
Code language: Perl (perl)
以下に、対応するTerraformプランのスニペットを記載いたします:
{
"mock":
{
"project":
{
"tfplan":
{
"resource_changes": [
{
"mode": "data",
"type": "vault_generic_secret",
"name": "gcp-credential",
"provider": "provider.vault",
"instances": [
{
"schema_version": 0,
"attributes": {
"data": {
"value": "BOGUS"
},
"data_json": "BOGUS",
"id": "bogus-id",
...
}
}
]
},
{
"mode": "managed",
"type": "google_project",
"name": "cluster-project",
"provider": "provider.google",
"instances": [
{
"schema_version": 1,
"attributes": {
"auto_create_network": true,
"id": "gcp-opa-project",
"labels": {
"app": "opa-test",
"owner": "acme-inc"
},
"name": "gcp-opa-project",
"org_id": "bogus-org-id",
"project_id": "gcp-opa-project",
"skip_delete": true,
"timeouts": {
"create": null,
"delete": null,
"read": null,
"update": null
}
},
"private": "bogus-private"
}
]
},
{
"mode": "managed",
"type": "google_project_iam_member",
"name": "project-iam-member",
"provider": "provider.google",
"instances": [
{
"schema_version": 0,
"attributes": {
"etag": "bogus-etag",
"id": "bogus-id",
"member": "bogus-serviceaccount-email",
"project": "gcp-opa-project",
"role": "roles/owner"
},
"depends_on": [
"google_project.gcp-opa-project",
"google_service_account.terraform-gcp-opa-project"
]
}
]
},
{
"mode": "managed",
"type": "agoogle_storage_bucket_iam_binding",
"name": "project-iam-member",
"provider": "provider.google",
"instances": [
{
"schema_version": 0,
"attributes": {
"etag": "bogus-etag",
"id": "bogus-id",
"member": "bogus-member",
"project": "gcp-opa-project",
"role": "roles/owner"
},
"depends_on": [
"google_project.gcp-opa-project",
"google_service_account.terraform-gcp-opa-project"
]
}
]
}
]
}
}
}
}
Code language: Perl (perl)
Terraformのプラン出力において、以下の点が確認できます:
- 2番目のインデックスresource_changes は、google_projectというタイプです。
- ポリシーに基づき、
google_project
タイプにはlabels
およびlabels.owner
が必須となります。 - 上記の結果が真の場合、Terraformプランは通過し、Googleプロジェクトコードを本番環境にデプロイできます。
最初にポリシーの例をご覧いただきましたので、引き続きTerraformのセキュリティベストプラクティスをご確認ください。
結論
多くの組織では、インフラストラクチャを定義・保護・管理するために複数のポリシー言語やモデルを使用しています。これを管理可能にするためには、それらを扱うための統一されたツールセットとフレームワークが必要です。Open Policy Agent(OPA)は、貴組織にとって適切なツールとなる可能性があります。CI/CDやオブジェクトストレージを含む多様な統合とユースケースを備え、複数のクラウドプロバイダーやプログラミング言語と連携します。さらに、CNCFによればOPAは現在「卒業プロジェクト」となっており、今後の機能強化や新機能追加の可能性が高いと言えます。またOPAは高水準宣言型言語であるRegoによって支えられています。Regoでの記述は少し難しそうに思えるかもしれませんが、その概念を理解すれば実に楽しいものとなるでしょう。.