この記事はエムスリー Advent Calendar 2020 3日目の記事です。まだまだ面白い記事が続々投稿されるので、ぜひぜひ読んでみてください!
エムスリーエンジニアリンググループの青木(@blue_1617)です。 日々「エムスリーデジカル」を作ったり YouTube 見たりしてます。最近は「ピクミン3」をクリアしました。
さて、今回は Rails で認証マイクロサービスを作った話をします。 ちなみにサービスの名前は Gatekeeper です。かっこいいでしょう??
モチベーション
事業が拡大するにあたり RDS の負荷や Blast Radius の拡大が懸念されてきたため、シャーディングを実施することにしました。
今回のシャーディングは、もともとあった RDS とアプリ群をまるごと各シャードに配置するイメージです。すると、個々のシャード内には認証機能を備えた Rails アプリが構えているので、シャードに閉じたユーザーに関しては気にすることはあまりないです。
一方、問題になるのは全シャードを跨ぐことができるシステム管理者などです。
具体的には
- システム管理者はシャードごとに Password などを管理しないとならない
- エムスリーデジカルでは各シャードの振り分けにクライアント証明書を使っているので、シャードを行き来するために証明書を N 個管理する必要がある
- 認証を各シャード内の Rails アプリが実施すると各シャードの DB でデータが個別にできてしまい、全シャード間で "整合性" を保って管理する必要がある
などなどの問題があります。
これらを解決するために、各シャードの外側にシステム管理者用の認証マイクロサービスを作成する必要がでてきました。
システム概略
上図にシステムの概略図を示します。
システム管理者がログインして、あるシャードにアクセスする流れは以下です。
- システム管理者は一般ユーザーとは異なるドメインにアクセスする
- 社内 IP アドレス制限を突破する(VPN 経由)
- ID, Password 認証を突破する
- GUI でアクセスしたいシャードを選択する
- JWT が払い出される
- JWT の内容を元に認証 & シャードへ振り分けを実施
- JWT の内容を元にシャード内の DB で
find_or_create
相当の処理 - シャード内でシステム管理者として振る舞える
この仕組みによって、システム管理者は自身の ID, Password を一度入力するだけでどのシャードにもアクセスできます。
また、システム管理者のデータは Gatekeeper 内の DB が正となり、シャードの DB たちは Gatekeeper の DB(正確にはそこから生成された JWT)の内容を信用するだけでよくなりました。
もうちょっとだけ説明加えます。
Rails 6系の認証サーバー
シャード内の Rails アプリ(5系)に先立って、Gatekeeper は Rails 6系で作りました。
Rails 6系 では webpacker が導入されたり、autoloading の仕組みとして Zeitwerk が採用されたり変更点がそこそこありました。 シャード内のアプリ本体ではなく、マイクロサービスで先に技術検証などを済ませてしまえたので、本体の 5系 => 6系 もやりやすくなりました。
ちなみにエムスリーデジカルの Rails 5系への移行はこの話が面白いです。 www.m3tech.blog
試行錯誤の時の誰か氏
俺はただ Rails を触りたいだけなのになぜ出てきてしまうのか webpack
— blue (@blue_1617) 2020年4月7日
JWT を使った認証機構
JWT についての説明は ここ を読んでいただくとして、今回はどういった使い方をしたか紹介します。
まず ruby-jwt を使って下記のように JWT を生成します。
ALGORITHM = 'ES512' # JWT 生成 def generate_jwt(payload) target_payload = payload.merge({ iss: env_jwt_iss, exp: env_jwt_expire }) JWT.encode(target_payload, ecdsa_key, ALGORITHM) end
payload
の中には下記のように、システム管理者の情報が詰まっています。
これを cookies に埋め込むことでシャード内のアプリが読み取れるようになります。
ちなみに、JWT はあくまで Gatekeeper の認証結果を各シャードのアプリに渡すためだけに使っており、セッション保持やログアウトの処理はすべて各シャード内の Rails アプリ側が実施します。そのため、ここでの expires
は短く(1時間とか)設定されてます。認証処理は Gatekeeper で、セッション管理は各シャードの Rails という役割分担ですね。
# JWT に含める認証用の cookie def update_auth_cookie_of_digikar_user(user:, shard_id:) payload = { digikar_user: { id: user.id, name: user.name, email: user.email }, shard_id: shard_id } cookies[cookie_name] = { value: JwtService.generate_jwt(payload), expires: cookies_expire_at, httponly: true, secure: Rails.env.production?, domain: domain } end
実際にシャード内の Rails アプリが JWT をデコードする処理。公開鍵は KMS 管理にしてます。
ALGORITHM = 'ES512' # cookies[cookie_name] として cookies から jwt が取得できる def decode_jwt(jwt) JWT.decode(jwt, ecdsa_public, true, iss: iss, verify_iss: true, algorithm: ALGORITHM) end
デコードした内容をもとにシャード内の DB でシステム管理者を find_or_create
します。例外を吐き得るので、実際のコードでは rescue
処理などもしてます。
def authenticate_by_cookie(jwt:) # デコードした JWT の内容を検証 decoded_jwt = JwtService.decode_jwt(jwt) validate_jwt_info!(decoded_jwt) # JWT からデータを parse してシステム管理者を DB から探す admin_user_info = decoded_jwt&.first&.dig(JWT_USER_INFO_KEY) authenticated_user = find_with_check_admin!(id: admin_user_info['id'], email: admin_user_info['email']) return authenticated_user if authenticated_user.present? # DB に JWT に含まれたシステム管理者が存在しなければ作る create_by_jwt!(admin_user_info) end
以上の処理を経て、晴れてシャードに Gatekeeper 経由でログインできました!
まとめ
システム管理者の認証機構を JWT を使った Rails 6系のマイクロサービスに切り出しました! これによってシステム管理者のログイン導線を簡略化でき、システムの複雑性も解消できました! やったね!
We are Hiring!!!
エムスリーはギークでオタクでグレートなエンジニアを絶賛募集しています!
ちなみに、自分が所属する「エムスリーデジカル」チームは12/14(月) 19:00~ に採用説明会を行います! その後は親睦会もあるのでエムスリーのことが気になるそこのあなたもぜひいらしてください!
カジュアル面談もカジュアルに開いております!