皆様こんにちは、メドピアのサーバーサイドエンジニアの内藤(@naitoh)です。
社内の OpenID Connect Authorization Server を Golang 実装から Rails 実装にリプレイスしたので、技術ブログで紹介させていただきます。
前提
OpenID Connect は OAuth 2.0 認可プロセスを拡張し、認証目的で利用できるようにしたものになります。
Authorization Server は OpenID Provider (以下OP) とも呼ばれ、OpenID Connect を利用する OAuth 2.0 Client は Relying Party (以下 RP) とも呼ばれます。
OpenID Connect フロー概要は OpenID Connect Core 1.0 にある通り下記になります。
+--------+ +--------+ | | | | | |---------(1) AuthN Request-------->| | | | | | | | +--------+ | | | | | | | | | | | End- |<--(2) AuthN & AuthZ-->| | | | | User | | | | RP | | | | OP | | | +--------+ | | | | | | | |<--------(3) AuthN Response--------| | | | | | | |---------(4) UserInfo Request----->| | | | | | | |<--------(5) UserInfo Response-----| | | | | | +--------+ +--------+
背景
弊社では 過去 OP を PHP (認証エンドポイント) と Golang (トークンエンドポイント) のハイブリッド実装で提供を行なっていました。 ただ、ハイブリッド実装に伴う複雑性を解消するために 2022年に認証エンドポイントを PHP から Golang 実装にリプレイスを行い Golang 実装への統一を行いました。
これに伴い、複数言語間をまたぐ複雑性は解消されたのですが、弊社のバックエンドエンジニア採用は Rails エンジニアが主であり社内で Golang をメンテナンスできるメンバーが限られていたため、実質的に内藤一人が保守する体制になってしまいました。
サービス継続性の観点から、持続的なメンテナンスを可能にするため Rails への再リプレイスを実施したので、今回その話をさせて頂きます。
リプレイスにあたって検討した点
稼働中のサービスになるため、互換性を維持しつづ、今後のメンテナンス体制を確保するのが最大の目的です。
- メンテナンス体制
- 社内の Rails エンジニアリソースを活用して保守を行なっていくため、 Rails を用いた実装を行う。
- メンテナンスを容易にするため、なるべく自前実装は避け、既存の gem ライブラリを活用する。(ただしドキュメントが少ないと引き継ぎが厳しいので、ドキュメントがあると良い。)
- OP だけでは、動作確認が困難なため、検証用の RP も用意する。
- 既存のモノリスに OP を統合する事で、別リポジトリでの管理を避け一体的なメンテナンスが行えるようにする。(ただし、可能ならモジュラーモノリス的な疎結合にしたい。)
- 互換性
- OpenID Connect として現在使用している機能が実現可能。(OpenID Connect で OPTIONAL 扱いの機能はライブラリ側で未サポートの場合があるため。)
- スムーズに移行するため、RP とのインターフェース(リクエストパラメータ)は変更しない。 (社外の RP が存在するため弊社都合での変更はハードルが高い。)
- RP によっては同意済ステータスを引き継ぐ必要があり、旧データベースの情報を参照する必要がある。
なお、本サービスは主に医師会員向けのサービスであり、日本国内の医師数は40万人程度なので、新たな性能面の要求はありませんでした。(=現状維持できれば良い。)
移行にあたっての gem の選定基準
主に下記が検討候補として考えれました。
- OAuth/OIDC Component as a Service のような BaaS を使う。
- openid_connect gem を用いて実装する。
- Rails エンジンの仕組みの doorkeeper gem (OAuth 2 provider) + doorkeeper-openid_connect gem (OpenID Connect 拡張) を用いて実装する。
このうち、1 は予算の観点から特に検討しませんでした。
2 の openid_connect gem は、社内では OP としての 利用例は無いが RP としては長年利用しており、最初に候補に上がったのですが、OP のドキュメントが will write someday.. と無かったため、導入に躊躇いがありました。
3 の doorkeeper 本体は、非常によく使われているため特に問題なさそうでしたが、 doorkeeper-openid_connect は、Looking for maintainers! とメンテナーを募集中であり、継続性に不安がありました。
ただ、doorkeeper 本体のメンテナーでもある Nikita Bulai 氏が doorkeeper-openid_connect gem のサポート(バグ修正&改善のみ、新機能の追加は対象外)を行う事になり、継続的にリリースがされているので、この点では問題は無さそうと判断しました。
また、ドキュメントの観点では、下記が非常に充実しており、リプレイスにあたり必要な機能(promptパラメータ等)がサポートされているか、カスタマイズの観点で何をすれば良いか記載が豊富なのも重要な点でした。
また、Rails エンジンで実装されており、既存のRails モノリス統合しても密結合を避けられるためモジュラーモノリスの観点でも良さそうでした。
OpenID Connect 検証環境の構築
上記を踏まえ、一旦 3 の doorkeeper gem + doorkeeper-openid_connect gem で検証環境を構築し、評価を進めていくことになりました。
下記の実装例を参考に、手順通り実装すれば OP を構築できた & 実際に動作させながら必要な機能が期待通り動作するか、カスタマイズが意図通り行えるかを検証できたのが大きかったです。
※ 上記は RP の実装に omniauth-oauth2 gem を使われていますが、omniauth_openid_connect gem を使う事で、さらに実装量を減らしレビューコストを削減することができました。
評価の結果、 doorkeeper gem + doorkeeper-openid_connect gem は新規に OP を提供する場合は導入しやすい事がわかったのですが、カスタマイズの観点(特に既存DBにデータがある場合)からは課題があることが見えてきました。
データの移行方針の決定
この課題とは、doorkeeper-openid_connect のテーブル構造が既存DBのテーブル構造と異なるため、既存のリクエストフローとデータ保存のタイミングが異なるため、doorkeeper-openid_connect をカスタマイズしても既存DBをそのまま使い続ける事が困難な事がわかりました。
コードベース切り替え時に、既存DBをそのまま使い続ける事ができればシームレスなサービスリリースが可能になります。
具体的にはメンテナンス画面表示を用いたサービス停止が不要になり、仮に本番リリース時に問題があった場合でもコードを切り戻せばリリース前の状態にすぐに復帰する事が可能です。
ただ、これを実現するために既存DBのテーブルに無理やり doorkeeper-openid_connect のデータを書き込むと処理が複雑になり技術負債となってしまうため、残念ながらこの方針は断念し、既存DBテーブルのレコードのデータカラムを再構成し、新しく doorkeeper-openid_connect 用に用意したテーブルにデータマイグレーションする方針にしました。
これにより、リリース時は一旦サービスを停止し、メンテナンス画面を表示する対応となったのですが、メンテナンス表示対象を OpenID Connect を使用するサービスに限定する事ができたので、ビジネス影響を最小化する事で対処しました。
採用結果
最終的にOPの実装として、下記の点から 3 の doorkeeper gem + doorkeeper-openid_connect gemを採用することに決定しました。
- 互換性
- リプレイスにあたりOpenID Connect として必要な機能が実装されている。
- RP とのインターフェースを(ルーティングパスのカスタマイズ定義は必要だが)維持できる。
- 同意済ステータスを引き継ぐために、データマイグレーションは必要だが、マイグレーションをしてしまえば、複雑なカスタマイズは回避できる。
- メンテナンス体制
- 導入するにあたり、OpenID Connect 周りの処理はほぼ設定ファイルの記載で対応可能 (認可画面はデフォルトの画面が用意されているが、既存の Golang 実装の画面を erb で実装しました。)なため、メンテナンス対象のコードベースを最小化することで、OpenID Connect に詳しく無い開発者でもメンテナンスを容易にする。
- 動作検証用に管理画面に omniauth_openid_connect gem を用いた簡易RPを用意することで、 単一リポジトリでのOPの動作確認を可能にし、開発環境構築周りのトラブルを回避する。
リリース作業
無事、開発が完了したので、下記の流れでリリース作業を実施しました。
- ユーザーに対しメンテナンス時間を事前告知し、不要なアクセスを回避します。
- CloudFront 側で、簡易的なメンテナンス画面を表示し、不要なアクセスが発生しないようにしました。
- maintenance_tasks gem を用いてデータマイグレーションを行いました。本番環境での手作業によるリスクを回避し、進捗状況もわかる非常に重宝している gem です。詳しくは下記をご覧ください。 tech.medpeer.co.jp
- Rails 単体であれば flipper 等を用いたフィーチャーフラグでの切り替えを行うのですが、今回は別言語間でリプレイスなので、この手法は使えず、CloudFront 側でルーティング対象を切り替え可能にする事で対応しました。
- 本番でテスト用アカウントを用いて動作検証を行い、問題無い事を確認できたので、メンテナンス表示を削除し、無事リリースできました。 🎉
結果
OpenID Connect Authorization Server を Rails に統合する事で、複数リポジトリ、複数言語間にまたがったシステムの複雑性を解消し、一つの Rails モノリスに無事統合する事ができました。
Rails エンジンを用いる事で疎結合になっているので、今後の複雑性を回避できそうなのもポイントです。
その結果、OpenID Connect 関連のその後の機能改修で内藤が担当する事なく(レビューアーとしては参加)、他のメンバーが改修を行う事ができており、チームでのメンテナンス体制の確立という目的を無事達成することができました。
振り返り
前回の移行作業では、互換性を維持しつつ技術負債の解消を第一に取り組んだのですが、入社後あまり時間が経っていない時期に移行計画を立てたため、社内のエンジニア採用計画を踏まえたメンテナンス体制の重要性まで考慮できていなかったのが一番の敗因でした。
自分だけがメンテナンスできても、それは自分自身の首を絞めるだけなので、チームとしてメンテナンスできる体制を確保できるかが重要ですね。
是非読者になってください!
メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!
■募集ポジションはこちら medpeer.co.jp
■エンジニア紹介ページはこちら engineer.medpeer.co.jp
■メドピア公式YouTube www.youtube.com
■メドピア公式note
style.medpeer.co.jp