👽

Casbin導入時にハマったポイント

に公開

この記事は、Finatext Advent Calendar 2025 の18日目の記事です。

はじめに

Go言語などのアプリケーションで強力なアクセス制御(RBAC/ABAC)を実現するライブラリとして、Casbinがあります。
https://casbin.org/
導入や基本的な実装について、すでに素晴らしい記事がいくつも公開されています。

Casbinで始めるアクセス制御 | Future Tech Blog

Casbinによる権限管理の実装 | 電通総研 テックブログ

これらを参考にすれば「動くもの」を作るのは比較的容易です。しかし、実際に本番環境(特にコンテナベースの分散環境や大規模データ)で運用しようとすると、いくつか考慮すべき 「Day 2 Ops(運用フェーズ)」の課題 に直面します。

今回は、基本的な実装は割愛し、 「複数インスタンス構成時の整合性」と「パフォーマンスを意識したデータ設計」 という2つの観点から、実運用での考慮事項をまとめます。

1. 分散構成における「権限情報のズレ」

Casbinの大きな特徴の一つは、「ポリシー(権限ルール)をメモリ上にロードして判定を行うため高速である」 という点です。しかし、この仕組みは複数台構成(K8s上のPodが複数ある場合など)において注意が必要です。

課題:Management APIによる更新の罠

Casbinには、動的に権限を追加・削除するための Management API(AddPolicy, AddGroupingPolicy など)が用意されています。これらを実行した際の挙動は以下の通りです。

  • DB(Adapter)への書き込み: 永続化層(MySQLなど)のデータが更新される。
  • メモリの更新: APIを実行したそのインスタンスのメモリ上のキャッシュが更新される。

ここで問題になるのが、「他のインスタンスは、DBが更新されたことを(デフォルトでは)検知できない」 という点です。

(例)Instance A,Bの2台構成のケース

インスタンスAで「User1に管理者権限を付与」した直後、User1からのリクエストがロードバランサによってインスタンスBに振り分けられると、インスタンスBのメモリ上にはまだ権限情報がないため「Forbidden(権限なし)」と判定されてしまう。

対策:Watcherの導入か、運用回避か

この問題を解決するには、大きく分けて2つのアプローチがあります。

A. Watcher (Pub/Sub) の導入
Casbinには Watcher という仕組みが用意されています。
https://casbin.org/docs/watchers/
Redis WatcherやEtcd Watcherなどを組み合わせることで、「あるインスタンスで更新があったら、Pub/Sub経由で他インスタンスに通知し、ポリシーをリロードさせる」 ことができます。
リアルタイム性が要件として求められるシステムでは、この構成がほぼ必須になります。

B. 定期リロード(許容できる場合)
厳密なリアルタイム性が不要であれば、各インスタンスで定期的に LoadPolicy() を実行する、あるいは権限変更時に全インスタンスの再起動を伴うデプロイを行う、といった運用でカバーすることも可能です。

2. パフォーマンスとデータ設計(継承関係の扱い)

RBACを採用する場合、Casbinの g (grouping policy) を使って「ユーザー」と「ロール」を紐付けるのが一般的です。

p, admin, data1, read
p, admin, data1, write
p, staff, data1, read
g, alice, admin
g, bob, staff

小規模なうちは問題ありませんが、ユーザー数が数万、数十万と増えてきた場合に、この g ポリシーをすべてCasbinの casbin_rule テーブル(およびメモリ)で管理するのは得策ではありません。

課題:ポリシー定義の肥大化

全ユーザーのロール割り当て情報をCasbinに持たせると、以下の弊害が発生します。

  • 起動時間の増大: アプリ起動時(LoadPolicy)に数十万行のデータをDBからメモリにロードするため時間がかかる。

  • メモリ圧迫: 全ユーザーの継承関係をメモリに保持し続ける必要がある。

  • 判定コスト: 継承関係のグラフが巨大になり、判定ロジックが重くなる可能性がある。

対策:ユーザーとロールの紐付けを分離する

大規模な環境では、「Casbinにはロールと権限の定義(Rule)だけを持たせ、ユーザーとロールの紐付け(User-Role Map)はアプリ側のDBで管理する」 という設計が有効です。

具体的な実装イメージ(Go言語)での比較です。

従来の書き方(Casbinに全情報を集約)
Casbin内で「aliceはadminか?」を探索させる方法です。データ量が増えると重くなります。

// casbin_ruleテーブルに "g, alice, admin" が入っている前提
// Enforcerは巨大なグラフを探索する必要がある
ok, _ := e.Enforce("alice", "data1", "read")

推奨する書き方(アプリ側で解決して渡す)
ユーザーに紐付くロールの取得は、アプリケーション側の高速なKVSやDBで行い、Casbinには「解決済みのロール」を渡します。

// 1. アプリ側でユーザーのロールを取得(高速)
// 例: user_rolesテーブルなどから取得
roles := myUserRepo.GetRoles("alice") // []string{"admin"}

// 2. ユーザーIDではなく「ロール」をSubjectとして渡す
// Casbin側は "p, admin, data1, read" だけ知っていれば判定可能
for _, role := range roles {
    if ok, _ := e.Enforce(role, "data1", "read"); ok {
        // 許可
        break
    }
}

こうすることで、Casbinが管理すべきデータは「システムのロール定義(せいぜい数十〜数百行)」に収まり、ユーザー数が増えてもCasbinのパフォーマンスは劣化しません。

まとめ

Casbinは非常に柔軟ですが、ステートフルな側面(オンメモリキャッシュ)や、データ設計の自由度が高すぎるがゆえの落とし穴があります。

  • インスタンスが複数あるなら、整合性をどう保つか(Watcher or Reload)を決める

  • ユーザー数が多いなら、casbin_rule をユーザー台帳代わりにしない

この2点を設計段階で考慮しておくだけで、本番運用後のトラブルを未然に防ぐことができるはずです。

Finatext Tech Blog

Discussion