👋

Amplify+NextjsでマルチテナントSaaSをつくる

2023/08/16に公開

前提

  • AmplifyとNextjsについては詳細を割愛し記事を書いています
  • Amplifyを使用しているため、記事内は全てAWSを使用する前提です
  • インフラやセキュリティに関しての知識は本やネットで学習したものが多く、実戦経験が多くありません(toB SaaSの作成は2度目です)
  • 弊社のサービスは従業員の方用Nextjsプロジェクトと、管理者の方用Nextjsプロジェクトの2つのNextjsプロジェクトを開発しています。monorepoを採用しnxを使用しています。

マルチテナントとは?

SaaSを顧客に提供する際、主にセキュリティ上の観点から顧客ごとにSaaSの構成リソースを分離する提供方法をテナントの分離と呼びます。
(AWS SaaS Factory. 「テナントの分離」. https://aws.amazon.com/jp/partners/programs/saas-factory/tenant-isolation/. 参照2022/08/20)

テナントの分離にはサイロモデル・プールモデル・サイロ+プールモデルの3種類の選択肢があり、SaaS提供事業者はどれかを選択しています。
サイロモデルを選択すると、すべての資源(コンピュータリソースやデータベース)が顧客(テナント)ごとに提供されるが、プールモデルを選択すると複数の顧客が、同一資源を使用することになります。この状態をマルチテナントと呼びます。
以上は自分の理解ですので文献等に従った説明ではありません。

マルチテナントで必要なこと

詳細については割愛するが、弊社ではSaaS提供をサイロ+プールモデルを選択しています。したがってプールモデル内のテナントの資源(コンピュータリソースやデータベース)をアプリケーション内で分離する必要がでてきました。

前提

  1. セキュリティ要件が大企業になると各社ごとに異なる(IPアドレス制限やドメイン制限、SSO等)
  2. 保守・運用においてもセキュリティが求められる(本番環境にアクセスできる人員は弊社内でも限られるべき)
  3. 開発人数が少ない。基本的に1人、多くて3人程度
  4. 今後の組織発展に向けて、出来るだけコードベースで管理したい(属人化・ヒューマンエラーを少なくしたい)
  5. AmplifyはNextjs 12に対応していない(middlewareを使用できない)
    https://github.com/aws-amplify/amplify-hosting/issues/2343

目標

  1. サイロ+プールモデルでデプロイ・運用可能な状態が出来ている
  2. 顧客は サイロモデルの場合TENANT_ID.mysaasapp.com というURLに、プールモデルの場合mysaasapp.com というURLにアクセスすることで利用可能になっている
  3. Account-per-tenant方式が採用されている

詳細

  1. サイロ+プールモデルでデプロイ・運用可能な状態が出来ている
    これは前述の通りです。最上位の料金プランを契約する顧客にはサイロモデルで、それ以外の顧客をプールモデルでホスティングできるようにします。

  2. 顧客は サイロモデルの場合TENANT_ID.mysaasapp.com というURLに、プールモデルの場合mysaasapp.com というURLにアクセスすることで利用可能になっている
    マルチテナントを実装する手段として、大きく2つ考えました。1つはログイン画面にて、テナントID・ID(メールアドレス等)・PWを入力させログイン後各テナントに振り分ける。2つ目はテナントごとに異なるURLを発行し、ID・PWを入力させる。今回は1つ目を採用しようと思います。
    サイロモデルで提供する顧客には個別でCognito User Poolを持ちたい、ゆえにテナントごとに異なるURLを発行し、ID・PWを入力してもらう。プールモデルで提供する顧客は同一のCognito User Poolで構わない、ゆえにIDPWを入力させログイン後各テナントに振り分けるという形をとります。

  3. Account-per-tenant方式が採用されている
    選択肢としてVPC-per-tenantとAccount-per-tenantがありました。「本番環境にアクセスできる人員は弊社内でも限られるべき」であったためAccount-per-tenant方式を採用しました。
    (Amazon Web Services ブログ. 「AWS における Account-per-Tenant 型 SaaS 環境のライフサイクル管理」. https://aws.amazon.com/jp/blogs/news/managing-the-account-lifecycle-in-account-per-tenant-saas-environments-on-aws/. 参照2022/08/20)

インフラ設計図

【余談】テナントIDの形式・運用について

テナントIDを生成する上で、2つ疑問がありました

  1. テナントIDの形式はどうする?(何桁にする?英数字含む?)
  2. テナントIDはどう運用してる?(URLで持ったりする?)
    超ざっくりですが、調べた結果です。近いサービスのlafoolさんも参考にしました。
サービス名 IDの形式 運用
AWS 数字12桁(ex. 462553300326) URLでは持たず表示上のみ
HubSpot 数字8桁(ex. 22560907) app.hubspot.com/user-guide/22560907 みたいに
freee 数字10桁(ex. 675-640-2570) URLでは持たず表示上のみ
moneyforward 数字8桁(ex. 9236-7589) URLでは持たず表示上のみ
lafool 数字6桁(ex. 907614) URLでは持たず表示上のみ
smarthr 英数字24桁(ex. bb2adb7fa6f5a0e6cafa61f2) subdomainとして表示され事業所設定等にも表示されず

以上から

  1. テナントIDの形式はどうする?(何桁にする?英数字含む?)
    →数字8桁で
  2. テナントIDはどう運用してる?(URLで持ったりする?)
    →URLでは持たず設定にて表示する
    にします。

実装方法

1.サイロ+プールモデルでデプロイ・運用可能な状態が出来ている, 3. Account-per-tenant方式が採用されている

まず
ControlTowerを利用して作成します。

ControlTowerの導入

sandbox環境でテスト

サイロモデルでの提供開始

2. 顧客は サイロモデルの場合TENANT_ID.mysaasapp.com というURLに、プールモデルの場合mysaasapp.com というURLにアクセスすることで利用可能になっている

大まかなステップをまず。
2-1. Cognito User Pool内のユーザーがTENANT_IDを持てるようにする
2-2. JwtToken内にTENANT_IDを返すようにする
2-3. ログインしたユーザーのTENANT_IDとGroup(管理者用グループ、従業員用グループ等)により、アクセス制御をする
2-4. ドメインをAmplifyから設定する

2-1. Cognito User Pool内のユーザーがTENANT_IDを持てるようにする

$ amplify override authを実行

$ amplify override auth
✔ Which resource would you like to add overrides for? · <APP_NAME>
✅ Successfully generated "override.ts" folder at .....
✔ Do you want to edit override.ts file now? (Y/n) · no

override.tsにtenant_idに関する変更を記述。
※注意※
customAttributeは削除・設定の変更できないので注意!mutableをfalse→trueにしたりできません。

amplify/backend/auth/<APP_NAME>/override.ts
import { AmplifyAuthCognitoStackTemplate } from '@aws-amplify/cli-extensibility-helper';

export function override(resources: AmplifyAuthCognitoStackTemplate) {
  // tenant_idをcognito user poolで持つためのattribute設定
  //   https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-userpool-schemaattribute.html
  const myCustomAttribute = {
    attributeDataType: 'String',
    developerOnlyAttribute: false,
    mutable: false,
    name: 'tenant_id',
    // required: trueにするとpush時errorが出るので注意
    required: false,
  };

  if (resources.userPool) {
    // myCustomAttributeの設定追加
    if (resources.userPool.schema) {
      resources.userPool.schema = [
        ...(resources.userPool.schema as any[]),
        myCustomAttribute,
      ];
    }
  }
}

2-2. JwtToken内にTENANT_IDを返すようにする

$ amplify update authから途中でOverride ID Token Claimsを選ぶ部分が該当手順です。その他はそのままです。

$ amplify update auth
Using service: Cognito, provided by: awscloudformation
 What do you want to do? Walkthrough all the auth configurations
 Select the authentication/authorization services that you want to use: User Sign-Up, Sign-In, connected with AW
S IAM controls (Enables per-user Storage features for images or other content, Analytics, and more)
 Allow unauthenticated logins? (Provides scoped down permissions that you can control via AWS IAM) No
 Do you want to enable 3rd party authentication providers in your identity pool? No
 Do you want to add User Pool Groups? No
 Do you want to add an admin queries API? No
 Multifactor authentication (MFA) user login options: OFF
 Email based user registration/forgot password: Enabled (Requires per-user email entry at registration)
 Specify an email verification subject: .......(省略)
 Specify an email verification message: .......(省略)
 Specify an email verification message: .......(省略)
 Do you want to override the default password policy for this User Pool? Yes
 Enter the minimum password length for this User Pool: 8
 Select the password character requirements for your userpool: Requires Lowercase, Requires Numbers
 Specify the app's refresh token expiration period (in days): 30
 Do you want to specify the user attributes this app can read and write? Yes
 Specify read attributes: Email
 Specify write attributes: 
 Do you want to enable any of the following capabilities? Override ID Token Claims
 Do you want to use an OAuth flow? No
? Do you want to configure Lambda Triggers for Cognito? Yes
? Which triggers do you want to enable for Cognito Custom Message, Pre Token Generation
? What functionality do you want to use for Custom Message Create your own module
? What functionality do you want to use for Pre Token Generation Override ID Token Claims
Successfully updated the Cognito trigger locally

生成されたfunctionを編集して、tenant_idを返すようにします

amplify/backend/function/<FUNCTION_NAME>/src/alter-claims.js
exports.handler = async (event) => {
  // tenant_idをJwtTokenに含める
  event.response = {
    claimsOverrideDetails: {
      claimsToAddOrOverride: {
        tenant_id: event.request.userAttributes.tenant_id,
      },
    },
  };
  return event;
};
$ amplify push -y

設定が反映されました。新しいユーザを作成してtokenを確認してみます。

2-3. ログインしたユーザーのTENANT_IDとGroup(管理者用グループ、従業員用グループ等)により、アクセス制御をする

schema.graphqlとappsyncのvtlファイルを設定していきます。
こちらにある通りvtlファイル内の各ファイルを編集し保存することで、自動生成されるvtlファイルではなく作成したvtlファイルで上書きすることができます。
https://docs.amplify.aws/cli/graphql/custom-business-logic/#extend-amplify-generated-resolvers

※プールモデルへの追加が実際起きなかったため割愛

2-4. ドメインをAmplifyから設定する

※プールモデルへの追加が実際起きなかったため割愛

以上で終了です。目標を達成できました。

目標

  1. サイロ+プールモデルでデプロイ・運用可能な状態を作る
  2. 顧客は TENANT_ID.mysaasapp.com のようなURLにアクセスすることで利用可能になること
  3. Account-per-tenant方式を採用する

Discussion