
- はじめに
- 認可基盤を移行することになった背景
- 認可エンジンの選定と、会議一覧の認可フィルタ設計
- PoCによる設計の具体化
- データの移行、新旧並行稼働、新認可への切り替え
- うまくいった点と反省点
- おわりに
はじめに
こんにちは、株式会社ACES でソフトウェアエンジニアをしている中野です!
最近シャクティマットを購入し、寝る前に悶えながら楽しんでます。
私が所属するチームでは、オンライン商談や会議を高精度に記録・文字起こし・要約・解析する、AI議事録・セールスイネーブルメントツール「ACES Meet」を開発・運用しています。 私はACES Meetの認可基盤を刷新するプロジェクトで、バックエンドとインフラの設計/開発を担当しました。新しい認可基盤は8月中旬にリリースされ、無事に移行を完了しています。
本記事では、このプロジェクトにおける認可エンジンの選定から、新旧認可基盤の並行稼働、安全に新認可基盤へ切り替えるまでの取り組みを紹介します。
認可基盤を移行することになった背景
ACES Meetでは従来、アプリケーション内に直接記述した権限チェックによって認可を行っていました。従来は、テナント管理に関するロールが「管理者」と「メンバー」の2種類に限られていました。また、会議にアクセスできるユーザーの操作権限も一律で、閲覧のみ・編集のみといった区別ができませんでした。
この仕組みでは「誰が・どのデータにアクセスでき、どの操作を行えるか」を細かく制御できません。 そのため厳格な権限管理が求められるエンタープライズ企業では、内部統制上の要件を満たせず、ACES Meetを導入できないケースがありました。
より多くの企業にACES Meetを利用していただくためには、アクセス権限をより厳密に管理できるよう、認可基盤を刷新する必要がありました。
認可エンジンの選定と、会議一覧の認可フィルタ設計
Cerbosを選んだ理由
当初は、認可エンジンとしてOpenFGAを選定していました。主な理由は、複数サービスの認可データを一元管理できること、特定のクラウドへのロックインを避けられること、そしてReBAC(ユーザーと対象との関係性に基づいてアクセスを制御する仕組み)に対応できることでした。
しかし、認可基盤の利用を想定していた別サービスの提供が終了し、利用対象がACES Meetのみになったことで、認可データを一元管理する必要性が薄れました。その結果、OpenFGAを選定した当初の前提が変わったため、認可エンジンを改めて選定することにしました。
再選定では、OpenFGA、AWS Verified Permissions(AVP)、Cerbosを最終候補とし、以下の評価軸で比較・検討しました。
| 評価軸 | OpenFGA | AVP | Cerbos |
|---|---|---|---|
| 権限データの置き場所 | エンジン側に関係タプルを持つ → 二重管理 |
アプリ側 | アプリ側 |
| 一覧のフィルタリング | ListObjects (グラフ探索) |
BatchIsAuthorized 最大30件 条件生成なし |
PlanResources APIが条件を返す |
| 大規模データへの適合性 | 関係データ量が運用負荷に直結 | 推移的親 100件 (引き上げ不可) |
認可データを保持しないため 関係データ量による制約を受けにくい |
| インフラ適合 | 中央集権ストアの追加運用 | AWS依存が強い | 既存ECSにサイドカー追加のみ |
最終的に、認可エンジンとしてCerbosを採用しました。決め手は、認可データの二重管理を避けられることと、一覧取得に必要な認可条件を生成できることです。
OpenFGAのように認可専用のデータストアを持つ構成では、アプリケーションDBとの二重書き込みや、データの整合性を担保するための仕組みが必要になります。一方、Cerbosはステートレスな認可エンジンであり、アプリケーション側のデータをリクエストに含めて認可判定を行えます。
AVPも認可データをアプリケーション側に保持できる点では同様でしたが、一覧取得ではBatchIsAuthorizedの最大30件という上限が制約となりました。以上から、今回の要件にはCerbosが最も適していると判断しました。
会議一覧をどう認可フィルタするか
Cerbosで認可を実装するうえで、課題の一つとなったのが会議一覧です。 ACES Meetでは、会議一覧の検索・絞り込みにElasticsearchを利用しています。 そのため、閲覧可能な会議だけが検索結果に含まれるよう、認可条件をElasticsearchの検索クエリへ組み込む必要がありました。
ここで鍵となるのが、CerbosのPlanResources APIです。リソースごとにアクセス可否を問い合わせるのではなく、指定したユーザーがアクセス可能なリソースの条件を取得できます。
例えば、あるユーザーについて問い合わせると、単純な許可・拒否ではなく、 「公開範囲がテナント全体である」または「参加者に本人が含まれている」といった 条件式が返されます。レスポンスは次の3種類です。
| レスポンス | 意味 | 処理 |
|---|---|---|
| KIND_ALWAYS_ALLOWED | 無条件に許可 | フィルタなし |
| KIND_ALWAYS_DENIED | 無条件に拒否 | 空結果 |
| KIND_CONDITIONAL | 条件付き | ASTをクエリに変換 |
無条件の許可・拒否に該当しない場合は、KIND_CONDITIONALとして、認可条件が抽象構文木(AST)の形式で返されます。会議一覧へ適用するには、このASTをElasticsearchのクエリへ変換する必要があります。そこで、変換方法として次の3案を検討しました。
案1:閲覧可能な会議IDをDBから取得し、Elasticsearchへ渡す(見送り)
Cerbosでは、認可条件をクエリへ変換する公式アダプターがいくつか提供されています。この案では公式のSQLアダプターを利用できる点がメリットでした。
一方、閲覧範囲が広いユーザーではIDリストが巨大になり、Elasticsearchのtermsクエリの上限や性能面で問題が生じる可能性がありました。また、DBで先に絞り込んだ大量のIDをElasticsearchへ渡す構成になるため、Elasticsearchによる検索・絞り込みの利点も活かしにくくなります。以上の理由から、この案は見送りました。
案2:Elasticsearchで検索した後、Cerbosで閲覧できない会議を除外する(見送り)
実装上は単純に見えますが、この方式ではページネーションと総件数の整合性を保てません。 例えば、1ページ50件で3ページ目を取得するためにElasticsearchから150件を取得しても、認可判定後に80件まで減れば、3ページ目を返せなくなります。総件数についても、Elasticsearch上では10万件でも、実際に閲覧できるのは3,000件といった不一致が生じます。以上の理由から、この案も見送りました。
案3:認可条件そのものをElasticsearchクエリへ変換する(採用)
Cerbosが返した認可条件をElasticsearchクエリへ変換し、検索条件の一部として組み込む方式です。認可条件を含めて検索するため、ヒット件数とページネーションをElasticsearch側で正しく計算できます。案1のようなIDリストの肥大化や、案2のようなページネーションの不整合を構造的に回避できるため、この案を採用しました。
ただし、Cerbosが公式に提供しているアダプターはSQLAlchemy、Prisma、Mongooseなどを対象としており、Elasticsearch向けのものはありませんでした。既存アダプターの実装はいずれも数百行程度と比較的シンプルだったため、それらを参考にElasticsearchアダプターを自作することにしました。
全体の流れは次のようになります。

PoCによる設計の具体化
Cerbosの採用を決め、認可フローの大枠を整理した段階で、実際の構成や実装方法を具体化するためのPoCを作成しました。 PoCでは、Cerbosによる認可判定を既存APIへ組み込む流れや、PlanResources APIが返す認可条件をElasticsearchのクエリへ変換する方法を検証しました。さらに、実装を通じて認可に必要なデータを洗い出し、データモデルや各コンポーネントの責務についてチームで議論しました。
この検証により、机上の設計だけでは見えにくかった影響範囲や実装上の難所を、 早い段階で具体化できました。PoCで得られた結果をもとに設計と計画を見直し、 本実装へ進みました。
データの移行、新旧並行稼働、新認可への切り替え

Cerbosを採用し、認可フローの設計と実装方針を固めた後、新しい認可基盤への切り替えを進めました。移行に伴うリスクを段階的に減らすため、プロセスを次の4つのフェーズに分けました。
パラレルランまでに、新旧モデル間のデータ整合性と、新旧の認可判定が一致することを確認します。 これにより、実際にユーザー影響が生じる新認可へ切り替えでは、切り替えそのものに集中できるようにしました。
| フェーズ | やること | この時点の判定 | ユーザー影響 |
|---|---|---|---|
| デュアルライト (新旧二重書き込み) |
新規・更新を旧・新の両モデルへ書く | 旧認可 | なし |
| データ移行 | 既存データを新モデルへ揃える(DB → Elasticsearch) | 旧認可 | なし |
| パラレルラン (新旧認可の並行検証) |
新認可を影で評価し、一致率を計測 | 旧認可 | なし |
| 新認可へ切り替え | 新認可へ切り替え、API+UI実装、デュアルライト廃止 | 新認可 | あり |
デュアルライト(新旧二重書き込み)
最初に、認可に関わるデータを旧モデルと新モデルの両方へ書き込む、デュアルライトを導入しました。会議の作成・共有、公開範囲の変更、メンバーの招待・削除、管理系処理など、認可結果に影響する書き込み経路を洗い出しました。そのうえで旧モデルを作成・更新する際に、後方互換性を保つ形で新モデルも作成・更新するようにしました。
デュアルライトをデータ移行より先に導入したのは、稼働中のサービスでは、移行処理の実行中にもデータが更新され続けるためです。既存データを先に移行しても、その間に発生した変更が新モデルへ反映されなければ、移行が完了した時点ですでに新旧モデル間に差分が生じます。 そこで、まずデュアルライトによって「これから発生する変更」を両方のモデルへ反映できるようにし、その後のデータ移行で「それ以前に作られたデータ」を新モデルへ反映しました。
新旧モデルへの書き込みを同一トランザクションに含めれば、データの不整合は防ぎやすくなります。しかし今回は、あえてトランザクションを分けました。 新認可へ切り替えるまでは旧モデルを正とし、新モデルを実際の認可判定には利用しないためです。新モデルへの書き込み失敗によって既存機能まで利用できなくなることを避けました。 万が一、不整合が発生した場合のためにデータの差分をRedashで監視し、差分が生じた場合はその都度対応する方針を取りました。
データ移行
次に、既存データを新モデルへ移行しました。一括変換には、繰り返し実行しても結果が変わらない冪等なワンショットスクリプトを用意し、ドライラン、本番実行、検証の順に進めました。 まずDBのデータを移行し、その結果をもとにElasticsearchのデータも移行しました。
1回目:深夜帯に実施したものの、途中で中止
DBのマイグレーションは、本番環境のコンテナでメモリが不足し、途中で失敗しました。sandboxとstagingでは正常に完了していましたが、本番環境とはデータ量に大きな差があり、本番規模で初めて生じる課題を事前に洗い出せていませんでした。 原因は、移行スクリプトが一度に多くのデータをメモリへ読み込んでいたことです。必要なメモリ量を見積もったうえで、コンテナのメモリを一時的に4GBから16GBへ増やして再実行し、DBのデータ移行を完了しました。

一方、Elasticsearchのマイグレーションは当初順調に進んでいたものの、想定以上に時間がかかり、深夜帯には完了しない見通しとなりました。さらに、処理中にデータノードのストレージ空き容量が急速に減少したため、継続するリスクが高いと判断し、処理を中止しました。
sandboxとstagingでの検証時にエラーが頻発したことから、バッチサイズを必要以上に小さくしていたことも、処理時間が長引いた一因でした。検証環境と本番環境ではElasticsearchのスペックが大きく異なるため、その差を考慮してバッチサイズを設計すべきでした。
ただし、バッチサイズを大きくしていればストレージ消費の問題がより早く顕在化していたと考えられるため、いずれにしても1回目の処理は中止する必要があったと考えています。
2回目:Elasticsearchのデータ移行を日中帯に実施し、完了
1回目の結果からストレージ容量の不足が判明したため、事前にElasticsearchのストレージを増設しました。
また、1回目はバッチサイズを大きくして深夜帯に短時間で完了させる想定で実施したのですが、方針を変更しました。 バッチサイズをサービスに影響が出ないレベルに抑え、日中帯に時間をかけて処理することにしました。通常時より負荷は高まりましたが、サービスへの影響はなく、最後まで実行できました。
2回目はメンテナンスモードを使用しなかったため、処理中の更新によってデータに差分が生じる可能性がありました。そこで、移行後に新旧データの整合性を検証し、差分が見つかった場合にアラートを出す仕組みを用意しました。
パラレルラン(新旧認可の並行検証)
データ移行後は、本番トラフィックを利用して新旧の認可判定を比較するパラレルランを実施しました。認可を行うすべてのAPIを修正し、ユーザーへの応答には従来どおり旧認可の判定を使用しながら、同じ入力を新認可でも評価して、その結果をログへ記録しました。これにより、ユーザーへ影響を与えることなく、新旧の判定が一致しているかを検証できるようにしました。 さらに、判定の一致率を継続的に確認できるよう、Redash上に監視環境を構築しました。

この段階でパラレルランを実施したことで、デュアルライトやデータ移行に起因する不整合を、新認可へ切り替える前に検知・修正できました。その結果、ユーザー影響を抑えながら、安全に切り替えを進めることができました。
新認可への切り替えと、ロールバックを前提としない判断
パラレルランによって新旧の認可判定が一致することを確認した後、実際の判定を旧認可から新認可へ切り替えました。このとき、大きな論点となったのがロールバック方針です。
当初は「新認可への切り替え → API・UIのリリース → デュアルライトの廃止」という順序で進め、切り替え後もしばらくデュアルライトを維持する想定でした。リリース後、致命的な問題が発生した場合に、旧認可へ戻せる状態を残すためです。
しかし、実際に検討すると、次の課題がありました。
- リリース後もデュアルライトを維持するためには、書き込み方向を「旧 → 新」から「新 → 旧」へ切り替える必要があり、実装コストが大きい
- 新モデルで導入した概念には旧モデルに対応する表現がなく、完全に巻き戻すのは現実的ではない
これらを踏まえ、今回のリリースはロールバックを前提とせず、問題が発生した場合は新認可側を修正する方針を採用しました。この方針をもとに、新認可への切り替えと同時に、ロールバックのために残していたデュアルライトと旧モデルへの参照も廃止しました。
うまくいった点と反省点
うまくいった点
今回の移行で特に効果的だったのは、事前調査とPoCで得た知見を、早い段階からプロジェクト計画へ反映できたことです。技術的な不確実性や影響範囲をチームで共有したことで、リスクの高い工程を把握し、状況に応じて計画を見直しながら進められました。
また、移行をデュアルライト、データ移行、パラレルラン、新認可への切り替えという段階に分けたことも、安全性を高めるうえで有効でした。特にパラレルランでは、本番トラフィックを使って新旧の認可判定を比較することで、デュアルライトやデータ移行に起因する不整合を、ユーザーへ影響が及ぶ前に検知・修正できました。
反省点
本番と同等のデータ量やインフラ条件を前提とした検証が不足していました。 sandboxやstagingでは正常に動作していても、本番ではDBのデータ移行時にメモリが不足し、Elasticsearchのデータ移行では処理時間やストレージ消費が想定を上回りました。 いずれも、本番規模で初めて顕在化した問題です。 事前に対象件数からメモリ使用量、処理時間、ストレージ使用量を見積もり、本番相当の負荷試験や小さな単位での試行を行っていれば、より精度の高い実行計画を立てられたはずです。
また、デュアルライトは想定以上に影響範囲が広く、書き込み経路の洗い出しと見積もりが不十分でした。実装後の監視によって抜け漏れを検知・修正できたものの、会議の作成・更新だけでなく、共有、グループ、メンバー管理、管理系処理など、認可データへ影響する経路を事前に体系立てて整理する必要がありました。今後、横断的なデータ変更を伴う移行を行う際は、機能やデータを起点とした調査だけでなく、API、バッチ処理、管理機能など複数の観点から書き込み経路を洗い出し、影響範囲を段階的に検証する必要があります。
加えて、新しい認可モデルへの移行に伴って一部の既存機能を対象外としましたが、 その機能が想定以上に利用されていたことをリリース後に把握しました。 技術的に安全な切り替えだけでなく、利用実態の把握と仕様変更によるユーザー影響の確認も移行計画に含めるべきでした。アクセスログや問い合わせ履歴の確認、対象ユーザーへのヒアリングなどを通じて、変更・廃止する機能の利用状況を事前に確認し、必要に応じて代替手段や段階的な移行期間を用意する必要がありました。
おわりに
今回は、認可エンジンの選定から、Elasticsearchの会議一覧へ認可条件を組み込む方法、デュアルライト、データ移行、パラレルランを経て新しい認可基盤へ切り替えるまでの取り組みを紹介しました。PoCとパラレルランによって不確実性を早期に減らせた一方で、本番規模を想定した検証と、仕様変更によるユーザー影響の調査には改善の余地がありました。今回得た知見を今後の移行計画にも取り入れ、より安全で予測可能な形でプロジェクトを進めていきたいと考えています。
ACESでは現在、複数のエンジニアポジションで採用を行っています。本シリーズを読んでACESの開発に興味を持っていただけた方は、ぜひカジュアル面談でお話ししましょう!










