こんにちは。SmartHRでプロダクトエンジニアをしている@bmf_sanです。
SmartHRはマルチプロダクト戦略を推進していますが、「誰がどのデータにアクセスできるか」という権限管理の開発効率がボトルネックになっていました。理想的には新しいロール管理基盤を構築し、各プロダクトチームが完全に自律的に開発できる環境を目指したいところです。しかし、UIの変更制限、短期での価値提供、段階的移行によるリスク管理という3つの制約がありました。
この記事では、これらの制約を受け入れ、ポリシーベースアーキテクチャという現実的なアプローチを選択した経験を通じて、「完璧を求めない意思決定」と「制約との向き合い方」について共有します。
なお、本記事のテーマについてアーキテクチャConference 2025にて「完璧を求めない意思決定 〜アクセス制御基盤における制約との向き合い方〜」というタイトルで発表予定です。
マルチプロダクトSaaSで生じた権限管理の開発効率の低下
私たちは人事労務を中心に複数の業務アプリケーション(プロダクト)を展開するマルチプロダクトSaaSを提供しています。各プロダクトでは「誰がどのデータにアクセスできるか」という権限制御が必要不可欠です。
従来、権限管理は中央集権型のアーキテクチャで実装されていました。中央の権限基盤チームが全プロダクトのアクセス制御ロジックを実装し、コアサービス(モノリス)がロール管理・アクセス制御・従業員データ取得を一手に担う構成です。
しかし、プロダクト数の増加に伴い、この構成に課題が出てきました。各プロダクトから権限機能の要求が集中し始め、権限基盤チームの開発キャパシティがボトルネックになっていました。プロダクトチームが自律的に開発できず、価値提供が遅延するリスクが高まっていたのです。
従来のアーキテクチャは以下のような構成です。
---
title: 従来のアーキテクチャ(中央集権型)
---
graph TB
User[ユーザー]
Products[各プロダクト]
Core[コアサービス モノリス
ロール管理 + アクセス制御 + 従業員データ取得]
DB[(従業員データ)]
User -->|利用| Products
Products -->|権限確認・データ取得| Core
Core -->|データアクセス| DB
style Core fill:#ffcccc
style Products fill:#e1f5ff
style DB fill:#ffffcc
この構成には、いくつかの問題がありました。
- 責務の密結合
- ロール管理、アクセス制御、従業員データ取得が一つのシステムに集約されており、一つの変更が複数の責務に影響します。実装・テスト・レビューのコストが高く、変更に時間がかかります
- 開発負荷の集中
- アクセス制御ロジックの実装・保守が権限基盤チームに集中しており、全プロダクトの要求を一手に引き受ける状況です
- プロダクト固有要件への対応困難
- 共通機能として設計されているため、各プロダクト特有の権限要件に柔軟に対応することが難しい状況です
マルチプロダクト戦略を推進する上で、この状況は解決すべき課題でした。
理想的な解決策を阻む制約
理想的には、新しいロール管理基盤を構築し、ロール管理とアクセス制御を完全に分離したいと考えていました。統一的なロール管理UIを提供しながら、各プロダクトが独自のポリシーを持ち、中央集権型から分散型へ転換する。各プロダクトチームが自律的にポリシーを開発・運用し、権限基盤チームへの依存を完全に解消する。このアプローチは、全責務を完全分離し、各チームが自律的に開発できる、技術的に理想的なアーキテクチャだと考えていました。
しかし、私たちには複数の制約がありました。
UIの変更制限
理想的なアーキテクチャではデータモデルの根本的な変更が必要でした。設計上の工夫で結合度を抑えることはできても、UIへの影響は避けられません。そして、ロール管理のUI・ワークフローは既に多くのユーザーが利用しています。UIの大幅な刷新を伴う変更は、大きなコストとリスクを伴います。ユーザーの学習コスト・混乱を避ける必要がありました。
短期での価値提供
ボトルネック問題に対して、小さく始めてリスクを抑えながら価値を提供することが求められていました。理想的なアプローチには広範囲な改修と長期間を要するため、その間の機会コスト(プロダクト開発の遅延)が大きすぎるという判断がありました。まずは実用的な価値を提供し、マルチプロダクト戦略を加速させる必要がありました。
段階的移行によるリスク管理
リアーキテクチャにはある程度大規模な改修がつきものです。ストラングラーパターンやレガシーミミックなど移行のリスク管理の手段はいくつか考えられますが、今回は理想的なアーキテクチャへの移行がユーザー体験に大きく影響を与える可能性がありました。プロダクト志向を大事にしている私たちは、検証範囲を小さくしつつ、段階的に移行したいという判断がありました。
ポリシーベースアーキテクチャの採用
これらの制約を前に、私たちは「理想的なアプローチ」ではなく、制約を踏まえた「現実的なアプローチ」を選択しました。ポリシーベースアーキテクチャによる段階的責務分離を行い、既存システムを維持しながら、ボトルネックを緩和します。そして、将来への選択肢を残します。
この選択は、一見すると妥協のように見えるかもしれません。しかし、私たちはこれを妥協ではなく、制約という現実の中で最良の価値を生み出すための戦略的な意思決定だと考えています。
ここで、理想的なアプローチと現実的なアプローチを比較してみます。
| 観点 | 現実的なアプローチ(今回) | 理想的なアプローチ |
|---|---|---|
| 開発スピード | 既存UI・プロセスをほとんど変更せず、権限基盤に集中していた責務を切り出せる | 新しいロール管理基盤の構築とポリシーの実装が必要 |
| 移行コスト | ポリシー層を追加するだけで段階的移行が可能 | 全サービスを同時に改修する必要が生じやすい |
| プロダクト要件への対応 | 共通ポリシーで主要ケースをカバーし、不足分は個別拡張できる | プロダクトごとに最適化できるが、標準化が難しい |
| リスク | 影響範囲をポリシーファイルに閉じ込められる | UI・データモデルの改修が広範囲に波及 |
| 価値提供 | 短期間での価値提供 | 中長期の期間を要する |
どちらのアプローチにもトレードオフがあります。理想的なアプローチは将来的な自律性・拡張性で優れる一方、現実的なアプローチはリスクを抑えながら早期に価値を提供できます。
私たちが現実的なアプローチを選択したのは、早期の価値提供とリスク管理を重視したからです。理想的なアプローチによる完全な自律性よりも、短期間でボトルネックを緩和し、マルチプロダクト戦略を加速させることの価値が大きいと判断しました。
ただし、このアプローチには懸念もありました。既存の共通UIを維持したままで、各プロダクトの異なる要求を同水準で満たせるのか。共通ポリシーという制約の中で、どこまで柔軟性を確保できるのか。これらの懸念を認識しながらも、段階的な改善と学習を通じて対応していく道を選びました。
このアプローチにより、以下の3つの価値を実現できます。
責務分離によるボトルネックの緩和
アクセス制御ロジックの責務を切り出すことで、権限基盤チームがボトルネックとなっていた状況を緩和します。ポリシー層による責務分離により、アクセス制御基盤は汎用的な評価機能の提供に専念し、プロダクト固有要件への対応コストを削減します。
学習機会の確保
実運用を通じた学びも重要な価値です。ポリシー設計のベストプラクティスの蓄積、パフォーマンス特性の把握、プロダクト固有の要求パターンの理解。これらの学びが、次のステップ(新しいロール管理基盤の構築)への具体的な道筋を示します。
将来への選択肢の確保
ポリシー導入により、ボトルネックは「基盤」から「ポリシー設計・運用」へシフトします。この段階的な責務分離により、将来に向けた複数の選択肢を残せます。
ポリシー実装やロール管理のデータ構造を再編することで、ポリシー開発をプロダクトチームに委譲できるようになります。各プロダクトが汎用的なアクセス制御基盤を活用しながら、拡張的にポリシーを開発し、独自の要件に対応する道が開けます。基盤チームそのものがボトルネックになるのではなく、ポリシー設計の柔軟性によってスケーラビリティを確保できます。
制約に基づくアーキテクチャ設計
実際に、この現実的なアプローチをどのように実現したのか、具体的なアーキテクチャと技術を説明します。
既存UIを維持したポリシー層の追加
私たちのアプローチの要点は、「UI・プロセスはそのまま、ポリシー層だけ追加」することでした。
---
title: 新しいアーキテクチャ(ポリシーベース)
---
graph TB
User[ユーザー]
Products[各プロダクト]
Core[コアサービス モノリス
ロール管理 + 従業員データ取得]
AccessControl[アクセス制御基盤
ポリシー評価]
OPA[OPA
ポリシーエンジン]
PolicyFiles[ポリシーファイル
Git管理]
DB[(従業員データ)]
User -->|利用| Products
Products -->|データ取得| Core
Core -->|ポリシー評価依頼| AccessControl
AccessControl -->|評価実行| OPA
OPA -.->|参照| PolicyFiles
Core -->|データアクセス| DB
style Core fill:#ccffcc
style AccessControl fill:#ffccff
style OPA fill:#ffccff
style Products fill:#e1f5ff
style PolicyFiles fill:#ffe1cc
style DB fill:#ffffcc
新しいアーキテクチャでは、責務が明確に分離されています。アクセス制御基盤はポリシー評価のみを担当します。コアサービス(モノリス)はロール情報の管理と従業員データ取得を継続します。各プロダクトはビジネスロジックとUI制御に集中します。これにより、責務分離を実現しつつ、既存のユーザー体験を維持できます。
システム内での処理の流れは以下のようになります。
- ユーザーが各プロダクトで従業員データアクセスを要求
- プロダクトがコアサービスに従業員データ取得を依頼
- コアサービスがアクセス制御基盤にポリシー評価を依頼
- アクセス制御基盤がRegoポリシーを評価し、検索条件を生成して返却
- コアサービスが生成された検索条件を使って従業員データを取得し、プロダクトに返却
ポリシー評価と検索条件生成のステップが新たに追加された形です。この変更は、ユーザーからは見えません。
ポリシーエンジンには、OPA(Open Policy Agent)を選択しました。選択の理由は、OPAがポリシーエンジンの代表的な選択肢の一つであり、多くの実績と豊富なエコシステムがあるからです。OPAはCNCF(Cloud Native Computing Foundation)のGraduated Projectであり、高いパフォーマンスと水平スケーラビリティを備えています。他の選択肢として、独自実装(保守コスト大)やAWS/GCPのサービス(ベンダーロックイン)も検討しましたが、OPAの汎用性とコミュニティの規模が決め手となりました。
OPAでは、Regoという宣言的なポリシー記述言語を使用します。Regoでは、「誰が・何を・いつ・どのようにできるか」を宣言的に定義します。例えば、「管理者ロールを持つユーザーは全従業員データにアクセス可能」といったルールをコードで記述します。
# 例:管理者ロールを持つユーザーは全従業員にアクセス可能
allow {
input.user.roles[_] == "admin"
input.resource.type == "employee"
}
ポリシーをコード(Regoファイル)として管理することで、Policy-as-Codeを実現します。ポリシーファイルはGitHubでバージョン管理され、レビュー・テスト・CI/CDが適用されます。
共通ポリシー設計による意思決定の遅延
ポリシー管理においては、「新しいロール管理基盤への移行」の判断を将来に委ねました。既存の共通UIを維持しながら、将来的にポリシーを分散管理できる余地を残します。理想的なアプローチと現実的なアプローチにおけるポリシー管理の考え方の違いを説明します。
理想的なアプローチでは、新しいロール管理基盤を構築し、統一的なロール管理UIを提供します。その上で、各プロダクトチームが自プロダクトのポリシーを自由に設計・実装・管理し、完全な自律性を持ちます。
---
title: 理想的なアプローチ(分散型)
---
graph TB
Product1[プロダクトA]
Product2[プロダクトB]
Product3[プロダクトC]
RoleUI[新ロール管理基盤
統一的なUI]
Policy1[ポリシー A]
Policy2[ポリシー B]
Policy3[ポリシー C]
Product1 --> RoleUI
Product2 --> RoleUI
Product3 --> RoleUI
Product1 --> Policy1
Product2 --> Policy2
Product3 --> Policy3
style Product1 fill:#e1f5ff
style Product2 fill:#e1f5ff
style Product3 fill:#e1f5ff
style RoleUI fill:#ffe1cc
style Policy1 fill:#ccffcc
style Policy2 fill:#ccffcc
style Policy3 fill:#ccffcc
一方、現実的なアプローチでは、既存のロール管理UI・ワークフローを継続利用するという制約があります。これは、アクセス制御ロジックの共通化を前提としています。つまり、共通的なポリシー設計が必要になります。完全な自律性を持たない形になります。
---
title: 現実的なアプローチ(共通型)
---
graph TB
Product1[プロダクトA]
Product2[プロダクトB]
Product3[プロダクトC]
RoleUI[共通ロール管理UI]
CommonPolicy[アクセス制御基盤]
ExtPolicy1[共通ポリシー]
Product1 --> RoleUI
Product2 --> RoleUI
Product3 --> RoleUI
Product1 --> CommonPolicy
Product2 --> CommonPolicy
Product3 --> CommonPolicy
CommonPolicy --> ExtPolicy1
style Product1 fill:#e1f5ff
style Product2 fill:#e1f5ff
style Product3 fill:#e1f5ff
style RoleUI fill:#ffe1cc
style CommonPolicy fill:#ccffcc
style ExtPolicy1 fill:#d4f1d4
この違いは、UIを変更しない選択の理由でもあります。共通的なロール管理を維持することで、ポリシーの一貫性とユーザー体験の統一性を保ちながら、各プロダクトが拡張的にポリシーを開発できる余地を残しています。ただし、自律性を持たないため、共通ポリシーを保守運用する責任が基盤チームに残ります。この点の解決は今回は見送り、将来の課題としました。
制約との向き合い方
本記事では、アクセス制御基盤の設計を通じて、「完璧を求めない意思決定」と「制約との向き合い方」を実践した経験を共有しました。
完璧な解決策を追求すると、制約は「障害」になります。しかし、完璧を求めず、現実的な価値提供を優先すると、制約は「設計のパラメータ」になります。
設計とは、制約を見つけることです。しかし、何を制約とするかは、ときに難しい判断を迫られます。多くの場合、制約は外部から与えられるものと考えられがちです。予算が足りない、時間がない、既存システムを変えられない。しかし、これらをすべて制約として受け入れるべきなのでしょうか。どこまでを制約とし、どこから挑戦するのか。この線引きこそが、制約を定義する難しさです。
今回のケースでは、理想的なアプローチを追求せず、3つの制約を明確に定義しました。UIの変更制限、短期での価値提供、段階的移行によるリスク管理。これらの制約を設計の前提として受け入れることで、ポリシーベースアーキテクチャという解決策が生まれました。
さらに、すべてを今決めるのではなく、決定を遅延させました。ポリシーベースアーキテクチャの採用は、ボトルネックの緩和という喫緊の課題に対応するため、今決めるべきことです。一方で、新しいロール管理基盤への移行や完全な責務分離は、実運用を通じた学びを得てから判断するため、将来に委ねました。完璧を求めないからこそ、この柔軟性を保つことができました。
意思決定の遅延により、将来の選択肢を保持できます。
- ポリシーの共通化を継続する
- プロダクトごとに独自ポリシーへ移行する
- 新しいロール管理基盤へ移行する
これらの選択肢により、段階的にアーキテクチャを進化させていくことができます。
理想的なアプローチと現実的なアプローチ、どちらを選ぶかは状況によります。今回、私たちは制約を明確にし、意思決定を遅延させることで、現実的でありながら将来への柔軟性を保つ設計を実現しました。これは妥協ではなく、今必要な価値を提供しながら、将来の選択肢を狭めないという戦略的な判断です。
完璧を求めないこと。それは制約を敵視せず、制約の中で価値を生み出す姿勢です。制約を受け入れ、決定を遅延させる。このプロセスが、持続可能なアーキテクチャ設計につながると考えています。
We Are Hiring!
アーキテクチャチームでは、一緒にSmartHRの成長を加速させてくれるプロダクトエンジニアを募集しています。
権限管理、アクセス制御といったドメインを中心に、プロダクト横断で組織やシステムに影響を与える基盤を設計・開発・運用しています。
これらの基盤はSmartHRのビジネスを成長させる鍵となっており、泥臭く険しい道のりではありますが、ソフトウェアエンジニアとしてのやりがいと成長機会に恵まれた環境です。
少しでも興味を持っていただけましたら、ぜひお会いしてお話ししましょう!
まずはカジュアル面談で話しましょう
正式応募をお考えの方
元の記事を確認する