🔑

私が考えるマイクロサービスアーキテクチャ

2022/10/26に公開

はじめに

以前に、マイクロサービスアーキテクチャにゼロから挑んだ開発経験から、私が現時点で考えるマイクロサービスアーキテクチャを書いてみる。前回はAWSで構築したがAWSに限定せず汎用的に表現してみたいと思う。

前提

例として、社員の勤怠と有給の管理ができるようなwebのSaaSプロダクトを考える。
ここでいうプロダクトとは商品として販売できる最小の単位とする。

境界づけ

まずは、プロダクトを5つの機能に分類する。

  • 認証・・・認証を行うIdP。ユーザー固有のIDを管理するユーザーディレクティブを持つ。
  • ユーザー・・・認証されたユーザーと権限の紐付きを持つ。
  • 権限・・・ロールとポリシーによる権限を設定する。「ユーザー」「権限」「勤怠」「有給」というサービスそれぞれに個別の設定ができる。
  • 勤怠・・・勤務の開始と終了を管理できる。
  • 有給・・・有給の付与、消化、残日数の管理ができる。

パターン1:バックエンドがマイクロサービス

  1. ブラウザからURLでWebクライアントを読み込み。(html,css,js)
  2. ID/PWなどで認証をしてIDトークンを取得。
  3. 取得したIDトークンを使ってバックエンドと対話。

ポイント1

プロジェクトがスモールスタートした場合、最初はこのパターン1になるかもしれない。
このアーキテクチャの特徴はバックエンドは小さく別れているがフロントは一つだということである。
プロダクトが一つだからといって、バックエンドもモノリシックなアーキテクチャにする必要はないと考えている。

ポイント2

もう一つの特徴として、GraphQLのBFF(backend for frontend)サーバーを中間サーバーとして設置していることがあげられる。
もしあるユーザーの権限を調べたくなった場合、BFFがなければクライアントは最初にユーザーサービスに問い合わせてそのユーザーの権限IDがわかったら、次に権限サービスにその詳細を問い合わせなければならない。
GraphQLのリゾルバ機能を使えば、クライアントは一度の問い合わせで、ユーザー詳細と権限詳細の両方がマージされたレスポンスを受け取ることができる。
以下GraphQLのクエリ例である。(HTTPリクエストPOSTメソッドのペイロードをパースしたもの)

クエリ例
query getUser {
    getUser(id:"user1"){
        id
        name
        role_id
        role {
            id
            name
            policy
        }
    }
}

GraphQLサーバー側の設定では role フィールドに 権限サービス へのリゾルバを設定しておくことにより、 role フィールドに対しレスポンスを返す責務を持つのは 権限サービス ということになる。GraphQLは最初に ユーザーサービス から1次元目の id name role_id を受け取ったら、次にそのレスポンスを 権限サービス に渡し、 role の情報を要求する。権限サービスは role_id が渡されたことによって、返すべきデータを識別でき、role フィールドの中身である2次元目の id name policy を返却する。
GraphQLはこうして全てのバックエンドサービスへ順番にリクエストしていき、レスポンスを全て集め終わったら、クライアントが要求している構造どおりのレスポンスを返す。以下のような構造のデータを受け取ることができる。

レスポンス例
{
  id: 'user1',
  name: 'yamada hanako',
  role_id: 'role1',
  role: {
    id: 'role1',
    name: 'admin',
    policy: {
      services: {
        user: ['group:getUser', 'createUser', 'listUsers'],
        role: ['getOwnRole'],
	attendance: [...],
	paid_vacation: [...]
      }
    }
  }
}

認証について


小さく独立したバックエンドサービスがリクエストを受け取ったとき、それが認証済みのリクエストかどうか知る必要がある。そのためには認証サービスに発行してもらったIDトークンを Authorization ヘッダーに含めてリクエストする。リクエストを受け取ったバックエンドサービスは、そのトークンを検証することで認証済みのリクエストかどうか知ることができる。
https://aws.amazon.com/jp/premiumsupport/knowledge-center/decode-verify-cognito-json-token/

権限について

認証が確認できたら、今度はそのユーザーがアクションを実行できる権限を持っているか知る必要がある。
AWSのCognitoや、GCPのIdentityPlatformといった認証サービスは、IDトークン発行時に追加情報を付与することができる。認証後かつIDトークン生成前に関数をトリガーし、ビジネスロジックを差し込むフック機能である。
これを利用し、IDトークンに tenant_idrole_id を追加する。ユーザーが属する組織と権限のIDである。
IDトークンは以下のようになるだろう。

IDトークンのペイロード部
{
  "sub": "aaaaaaaa-bbbb-cccc-dddd-example",
  "aud": "xxxxxxxxxxxxexample",
  "exp": 1500013000,
  "email": "anaya@example.com",
  "tenant_id": "tenant1",
  "role_id": "role1"
}

バックエンドサービスは tenant_idrole_id を使って権限サービスに問い合わせ権限を確認する。権限サービスは以下のようなレスポンスを返すだろう。

レスポンス例
{
  id: 'role1',
  name: 'admin',
  policy: {
    services: {
      user: ['group:getUser', 'createUser', 'listUsers'],
      role: ['getOwnRole'],
      attendance: [...],
      paid_vacation: [...]
    }
  }
}

サービスごとに独自のアクションを設定できるようにpolicyを設計する。権限サービスに対しては getOwnRole アクションのみが許可されているので、自分自身の権限を取得することができる。

マイクロサービス間の連携

リクエストがあるたびにいつも権限サービスに問い合わせなければならない構成は疎結合ではないと考える。
権限サービスがメンテナンス中であるとき、他のサービスが使えないことになってしまう。
この問題を解消しようとする場合、他のサービスは権限データのキャッシュを各自で持つ必要がある。
他のサービスは自身が持つキャッシュデータを常に最新に保ちたいため、権限データの更新を随時知りたいと思っている。
権限サービスは他のサービスにデータを連携するために、データの更新があるたびにイベントを発生させて更新を知らせる。イベントは以下の例のようなものを想定する。

イベントの例
{
  "source": "role",
  "detailType": "update",
  "time": "2017-12-22T18:43:48Z",
  "detail": {
    "id": "role1",
    "name": "admin",
    "policy": {
      "services": {
        "user": ["group:getUser", "createUser", "listUsers"],
        "role": ["getOwnRole"],
        "attendance": ["*"],
        "paid_vacation": ["*"]
      }
    }
  }
}


この場合、権限サービスはイベントのプロデューサー(作成者)であり、その他のサービスはイベントのコンシューマー(消費者)となる。コンシューマーはイベントを受け取ったら自身のキャッシュに保存する。
同期でなくイベントで連携する理由はお互いが健康でないと成立しない通信をなるべくしないようにすることで疎結合を保つためである。また権限サービスの性能が他のサービスに影響されないためである。
例としては、ユーザーサービスのレスポンスが遅延したために、権限サービスも引きづられて遅くなるといった事などがあげられる。

フロントのマイクロサービス化

これまでフロントを一つで考えてきたが、今後「 認証 + ユーザー + 権限 」サービスを他のプロダクトでも使いたくなることがあるかもしれない。複数のプロダクトで共通で使うサービスはフロントを分けた方がいい。

パターン2:フロントを分ける

フロントが1つだった際のURLが、 https://example.com/ だったとすると、
3つに分割した際は以下のようにトップレベルドメインは共通のほうがよい。

  • https://account.example.com/
  • https://attendance.example.com/
  • https://paid_vacation.example.com/

このようなURL構成の場合、認証サービスから発行されたIDトークンはcookieの .example.com ドメインに保存することができ、そのIDトークンはこの3つのフロントが全てアクセスすることができる。

BFFはフロントのためにある

BFFは文字通りフロントのためのバックエンドなので、ひとつのフロントに対してひとつのBFFがよいと考える。
要件の違う複数のフロントがひとつのBFFにアクセスしていたりすると、BFFがフロントの細かい要求に対応できなくなりBFFの仕様が複雑化するので、あくまでもフロントの細かい仕様変更に追随するようにBFFをチューニングしていくのがよいと感じた。また逆に、フロントが複数のBFFのエンドポイントを、使い分けるのも違う。
フロントが複数のエンドポイントを叩かなくてよいためにBFFが存在しているので、フロントは常にひとつのBFFだけと通信するようにするといいと感じた。

おわりに

全てのマイクロサービスにIDトークンが中継されていくことから、マイクロサービス化の肝は認証にあると思う。
「 認証 + ユーザー + 権限 」ここだけしっかり独立して作られていれば、それを使う他のサービスは厳密なマイクロサービス化や疎結合にこだわる必要もないかもしれない。サービスの特性によってはモノリシックが最適解になる場合もあると思うが、その場合であっても認証周りだけは疎結合に設計したほうがよいと思う。

レスキューナウテックブログ

Discussion