Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
NestJSで作るマルチテナントSaaS / Multi-tenant NestJS-base...
Search
hiroga
March 18, 2022
Technology
0
970
NestJSで作るマルチテナントSaaS / Multi-tenant NestJS-based SaaS
NestJS meetup Online #1
NestJSで作るマルチテナントSaaS
hiroga
March 18, 2022
Tweet
Share
More Decks by hiroga
See All by hiroga
LlamaGen: LlamaのNext-Token予測を使った画像生成 / Autoregressive Model Beats Diffusion: Llama for Scalable Image Generation
hiroga
0
250
人事評価GPTsで評価の本質に向き合おう! / HR GPTs: Essential evaluations focus!
hiroga
1
370
生成AI元年を個人的に振り返る / Reflecting on First Year of the Generative-AI
hiroga
0
300
AWS Startup Day 2023 今日ここで! コスト削減ハンズオン / Cost-Saving Hands-On today!
hiroga
0
15
ChatGPT社内活用資料 / Internal use of ChatGPT
hiroga
0
85
マルチテナントSaaSのカスタム要件に、 Auth0テナントを分割せず向き合う! / Multi tenant SaaS with Auth0
hiroga
1
2.7k
雑な攻撃からELBを守る一工夫 +おまけ / Know-how to protect servers from miscellaneous attacks
hiroga
0
2.4k
[AWS CDK] 1,000+のCloudWatch Alarmsを自動生成する技術 / [AWS CDK] Technics to Generate 1,000+ CloudWatch Alarms
hiroga
2
1.6k
Coralの投資先にあらゆる保険金請求をWeb化しようとしている会社があるらしいですよ
hiroga
1
110
Other Decks in Technology
See All in Technology
DevOps視点でAWS re:invent2024の新サービス・アプデを振り返ってみた
oshanqq
0
180
Amazon SageMaker Unified Studio(Preview)、Lakehouse と Amazon S3 Tables
ishikawa_satoru
0
150
Turing × atmaCup #18 - 1st Place Solution
hakubishin3
0
480
AWS re:Invent 2024で発表された コードを書く開発者向け機能について
maruto
0
190
組織に自動テストを書く文化を根付かせる戦略(2024冬版) / Building Automated Test Culture 2024 Winter Edition
twada
PRO
13
3.7k
マイクロサービスにおける容易なトランザクション管理に向けて
scalar
0
120
AI時代のデータセンターネットワーク
lycorptech_jp
PRO
1
280
PHPからGoへのマイグレーション for DMMアフィリエイト
yabakokobayashi
1
170
GitHub Copilot のテクニック集/GitHub Copilot Techniques
rayuron
29
12k
ガバメントクラウドのセキュリティ対策事例について
fujisawaryohei
0
530
株式会社ログラス − エンジニア向け会社説明資料 / Loglass Comapany Deck for Engineer
loglass2019
3
32k
オプトインカメラ:UWB測位を応用したオプトイン型のカメラ計測
matthewlujp
0
170
Featured
See All Featured
ReactJS: Keep Simple. Everything can be a component!
pedronauck
665
120k
Navigating Team Friction
lara
183
15k
Making the Leap to Tech Lead
cromwellryan
133
9k
JavaScript: Past, Present, and Future - NDC Porto 2020
reverentgeek
47
5.1k
Practical Orchestrator
shlominoach
186
10k
Easily Structure & Communicate Ideas using Wireframe
afnizarnur
191
16k
Performance Is Good for Brains [We Love Speed 2024]
tammyeverts
6
520
Speed Design
sergeychernyshev
25
670
Designing Experiences People Love
moore
138
23k
Java REST API Framework Comparison - PWX 2021
mraible
PRO
28
8.3k
How to train your dragon (web standard)
notwaldorf
88
5.7k
Reflections from 52 weeks, 52 projects
jeffersonlam
347
20k
Transcript
\ 積極採⽤中 / 2022年3⽉18⽇ ⼩笠原寛明(@xhiroga) NestJSで作るマルチテナントSaaS NestJS meetup Online #1
⽬次 \ お話したい / 1 1. はじめに 2. NestJS ×
マルチテナント × 認証 3. NestJS × マルチテナント × MongoDB 4. NestJS × マルチテナント × ロギング 5. おわりに
\ あなたと⼀緒に働きたい! / はじめに
\ 積極採⽤中 / ⾃⼰紹介 3
\ 全職種採⽤中 / 4
\ 積極採⽤中 / 会社紹介 ⽇本初の商品を連発している保険会社です。 5
\ 積極採⽤中 / 会社紹介 そのノウハウを元に保険SaaSを提供しています。 6 顧客 保険会社* *事業会社や保険代理店 のご利用も可能
プラン選択 本人認証 告知・重要事項説明 会員資格確認 商品ページ(LP) & 申込フォーム 契約参照 異動・解約 決済 契約更新 お客様 ポータル 査定・承認 問合せ 提出書類の参照 (電子データ) 支払記録 保険金 請求フォーム
\ 積極採⽤中 / 会社紹介 保険業務をSaaSでなめらかにし、みなさんがよい保険にアクセスしやすいようにしています。 7
\ あなたと⼀緒に働きたい! / サンプルコード
\ 全職種採⽤中 / 9
\ あなたと⼀緒に働きたい! / NestJS × マルチテナント × 認証
\ 積極採⽤中 / TL;DR • 認可トークンを⽤いたテナントIDの取得を⼀箇所で⾏うため、AuthGuardを⽤いる • テナントIDをLoggerに注⼊するために、useClass構⽂を⽤いる 11
\ 積極採⽤中 / AuthGuardを⽤いる • ヘッダーやパス、サブドメインからテナントIDを取得する場合、必ずしもAuthGuardは必要ではない • OAuthを⽤いて認可を⾏い、認可トークンからテナントIDを取得する場合、AuthGuardは必要 • 今回のデモでは簡略化のためヘッダーからテナントIDを取得する
12 @Injectable() export class AuthGuard implements CanActivate { constructor(private readonly logger: PinoLogger) { } canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> { const request: Request = context.switchToHttp().getRequest(); … const tenantId = request.headers['x-tenant-id’]; if (tenantId !== undefined) { request['tenantId'] = tenantId; this.logger.debug(`canActivate(): tenantId=${tenantId}`); this.logger.assign({ tenantId }) return true } } }
\ 積極採⽤中 / useClassを⽤いる • NestJSでは、GuardのようなMiddleWareもProviderである • ただし、`app.useGlobalGuards()` で追加した場合、DIのタイミングを逃してしまう •
AppModuleのようなトップレベルのモジュールに対し、特定のInjectionTokenを⽤いてInjectすることで、DIのタイミングを 逃さずにGlobalGuard同様に運⽤できる 13
\ 積極採⽤中 / useClassを⽤いる DIのタイミングを逃す例 14 app.useGlobalGuards(new AuthGuard()); @Module({ imports:
[…], controllers: […], providers: [ AppService, { provide: APP_GUARD, useClass: AuthGuard } ], }) export class AppModule { } AppModuleに対して明⽰的にInjectしている例
\ 積極採⽤中 / デモ 15
\ 積極採⽤中 / まとめ • AuthGuardで認証を⾏い、テナントIDをどこからでも取得可能にした • useClass構⽂でトップレベルのモジュールにAuthGuardを注⼊することで、LoggerをDIできた 16
\ あなたと⼀緒に働きたい! / NestJS × マルチテナント × MongoDB
\ 積極採⽤中 / TL;DR • MongoDBのDatabaseでテナントを分割した • ORMにMongooseを選定した • MongooseのコネクションはDatabaseと1:1
• リクエストスコープでMongooseをInjectするとメモリ不⾜になる • Serviceのメソッド実⾏時、適切なコネクションでModelを⽣成する 18
\ 積極採⽤中 / MongoDBのDatabaseでテナントを分割した 前提 • AWS DocumentDBを⽤いる • テナントごとにデータを分離する必要がある
19 単位 Pros Cons Cluster セキュリティが最も⾼い インフラ・管理コストのいずれも⾼い Database インフラ費⽤がテナント数に⽐例しない 。RBACを活⽤しやすい DatabaseをまたいだJOINはできあいので、 マスターデータとテナント固有データを合 わせて使うには⼯夫が必要 Collection インフラ費⽤がテナント数に⽐例しない 。コネクションを使い回せるので、パフ ォーマンスが⾼い 特定のテナントのみアクセス可能なRoleを アクセスするのが⾯倒 Row インフラ費⽤がテナント数に⽐例しない 。実装は簡単 MongoDBはRLSをサポートしていない
\ 積極採⽤中 / ORMにMongooseを選定した MongoDB事例の多さから、⼿堅くMongooseを選定しました。 20 Package NestJS Support Pros
Cons mongoose 公式Module 実績あり。NestJS公式ドキュメン トに記載あり。トランザクション 使える。 複数Connectionをサポートしてい ない。 Typegoose 公式サポートなし クラスやデコレーターを使って Modelを素早く構築できる 設定難易度が⾼い TypeORM 公式Moduleあり 実績あり(ただしRDBの割合⾼) MongoDB4系をサポートしていな いため、トランザクションが使え ない MikroORM MikroORM公式の NestJS Module 事例が少ない Prisma 公式ドキュメント 解説のみ MongoDBサポートがPreview MongoDB SDK 柔軟性は⾼い ORM相当の処理を⾃分で書くなら
\ 積極採⽤中 / リクエストスコープでMongooseをInjectするとメモリ不⾜になる • 複数のDatabaseにテナントごとに接続するには、複数のConnectionをリクエストに応じて使い分ける必要がある • 公式のMongooseModuleは複数Connectionの保持に対応していない • 最も簡単なやり⽅はMongooseModuleをリクエストスコープで⽣成することだが…
21 https://stackoverflow.com/questions/55571382/how-to-change-a-database-connection-dynamically-with-request-scope-providers-in その後、必要なものをリクエストにアタッチし、リクエストごとにデータベースを変更できます。 したがって、Scope.REQUESTを使⽤します。注⼊スコープの詳細については、ドキュメントを参照してください。 以前に作成した同じ接続を使⽤するにはどうすればよいですか。
\ 積極採⽤中 / デモ 22
\ 積極採⽤中 / Serviceのメソッド実⾏時、適切なコネクションでModelを⽣成する やりかたは2通り考えられる。 1. DIを使わない。ConnectionのPoolを⾃前で持ち、サービスの呼び出し時にModelを⽣成する。 2. DIを使う。Modelをリクエストスコープで宣⾔し、ConnectionのPoolをするProviderをInjectする。 23
\ 積極採⽤中 / DIを使わないサンプル 24 @Injectable() export const CatsService {
constructor() { private readonly connectionProvider: ConnectionProvider; } async getCats() { const connection = await this.connectionProvider.getConnection(); const cats = await connection.model('cats').find(); return cats; } } @Injectable() export class ConnectionProvider{ // 省略 getConnection() { const tenant = this.request.params.tenantId; } }
\ 積極採⽤中 / DIを使うサンプル 25 export const DogSchema = SchemaFactory.createForClass(Dog);
export const DogModelInjectionToken = "DogModel" export const dogModelFactory = { provide: DogModelInjectionToken, useFactory: (mongoConnectionMapProvider: MongoConnectionMapProvider, request: Request & { tenantId: string }) => { const tenantId = request.headers['x-tenant-id'] as string console.debug(`dogModelFactory.useFactory(): tenantId=${tenantId}`) return mongoConnectionMapProvider.getConnection(tenantId).model("Dog", DogSchema) }, inject: [MongoConnectionMapProvider, REQUEST] }
\ 積極採⽤中 / デモ 26
\ 積極採⽤中 / まとめ 単にMongooseModuleをリクエストスコープで利⽤するとコネクション数に問題が発⽣する。 MongoDBのコネクションを⾃前で管理し、Model⽣成時に適切に注⼊することで要件とパフォーマンスを両⽴できる。 27
\ あなたと⼀緒に働きたい! / NestJS × マルチテナント × ロギング
\ 積極採⽤中 / TL;DR • ログにリクエストIDとテナントIDを含める • 全てのErrorをCatchするExceptionsFilterを実装し、エラーを確実にログする 29
\ 積極採⽤中 / ログにリクエストIDとテナントIDを含める。 • ログへのリクエスト情報付与のために、nestjs-pinoを⽤いる • AuthGuardでリクエストIDを取得する際に、loggerにテナントIDをassignすることで、そのリクエストに対するログにテ ナントIDを付与できる(厳密なスコープは未検証) 30
canActivate(context: ExecutionContext,): boolean | Promise<boolean> | Observable<boolean> { const request: Request = context.switchToHttp().getRequest(); const tenantId = request.headers['x-tenant-id’]; if (tenantId !== undefined) { request['tenantId'] = tenantId; this.logger.debug(`canActivate(): tenantId=${tenantId}`); this.logger.assign({ tenantId }) return true } }
\ 積極採⽤中 / デモ 31
\ 積極採⽤中 / 全てのErrorをCatchするExceptionsFilterを実装し、エラーを確実にログする • NestJSは、デフォルトでは全てのエラーをロギングするわけではない • ドキュメントの通り、全てのエラーをキャッチするExceptionsFilterを実装する • useClass構⽂を⽤いてExceptionsFilterを注⼊することで、AuthGuardで設定したPinoLoggerを利⽤できる。
32 @Catch() export class AllExceptionsFilter implements ExceptionFilter { constructor( private readonly httpAdapterHost: HttpAdapterHost, private readonly logger: PinoLogger, ) { } …
\ 積極採⽤中 / デモ 33
\ 積極採⽤中 / まとめ • nestjs-pinoでログを整形し、かつリクエストIDとテナントIDを含める • ⾃前で実装したExceptionsFilterをuseClass構⽂を⽤いて注⼊することで、全てのエラーをテナントID付きでログに出⼒で きる 34
\ あなたと⼀緒に働きたい! / おわりに
\ 積極採⽤中 / チームについて • 今回ご紹介したのは、justInCaseTechnologiesでの取り組みの⼀部です • 私だけでなく、チームメンバーと⼀緒に取り組んだ成果でもあります • もっと知りたいという⽅、ぜひお話したいです!
36
\ 積極採⽤中 / チームの技術スタック バックエンド • NestJS, Fastify • pnpm
• TypeScript フロントエンド • React, Redux, Next.js • MUI, Emotion, Storybook • Jest, SWC • TypeScript インフラ • AWS, ECS, DocumentDB, Backup, ALB, WAF, GuardDuty, CodePipeline, Control Tower, AWS SSO, CDK v2, TypeScript • Vercel • GitHub Actions 37
\ 積極採⽤中 / チームの⽂化 • 物理的なホワイトボードの良さも認めるが、全員リモートワークが⼤好き • フルスタックかつ専⾨領域がある、T字型スキル志向の⽅が多い • いらないMTGを消したときに達成感を覚える
• Slackのハドルで突発的にペアプロが始まる • 議論の前に、NotionでPros/Consを整理しておく • 同じくらいのスキルの⼈がいたら、よりチームにないバックグラウンドの⼈にオファーする • いい仕事をした時はお互いに褒め合う • スクラム開発を尊ぶ 38
\ 全職種採⽤中 / 39
\ あなたと⼀緒に働きたい! / Thank you for listening!!!