Microsoft Bot Frameworkにおけるチャットボットのマルチテナント設計を本気で考えてみた（Build 2018対応版）

# 課題
- Azure Bot Serviceは、BotRegistrationと、AppServiceを組み合わせで契約してくれる。アプリとボットがすぐに接続できるのはメリットだが、密になりすぎていて切り離せなく、マルチテナント化しにくい。
- 同様の機能のボットを複数作りたいとき、Bot Service単位で作るとコストが倍々でかかってしまう。

# 対策および目的
- Microsoft Bot Framework、もといAzure Bot Service( https://azure.microsoft.com/ja-jp/services/bot-service/ )のテクノロジーを活用しつつ、マルチテナント対応するためのベストプラクティスを考える。
- この場合のマルチテナントとは、1アプリケーションでマルチテナントを捌けるチャットボットのことを指す。
- GAしたBot Serviceの契約単位についても考える。

# 引用
- 考え方としては[Creating a Single Bot Service to Support Multiple Bot Applications](https://www.microsoft.com/developerblog/2017/01/10/creating-a-single-bot-service-to-support-multiple-bot-applications/)をベースとしている。[Ami Turgman](http://www.amiturgman.com/)氏の簡潔で明確な説明に感謝。

# 概要（TL;DR）
![image.png](https://qiita-image-store.s3.amazonaws.com/0/195903/25e6995e-86a7-c9d1-1b45-bda79ca1058c.png)  

- **Azure Bot Serviceは利用しない（重要）**
- Bot単位に、Bot Channels Registrationを契約する。
- WebAppsは基本１個でOKだが、１アプリで負荷が増大し捌けなくなってきたら複数に分ける。
- VSTSなどから継続的にビルド・デプロイする。
- ボットとWebAppsを疎にすることによって、ボットの増加・廃止と、WebAppsの変更（たとえばLogicAppに変更など）がスムーズに行える。

# 設計例
- 例として、社内ヘルプデスク系のチャットボットのマルチテナント・マルチ部署・マルチチャンネル設計を下記に示す。    
![image.png](https://qiita-image-store.s3.amazonaws.com/0/195903/6705d24f-f2ed-cf18-5d11-031b93367904.png)  

<br> 

- この場合、1Botの単位＝**1テナント部署(テナントに属する部署)**であることに注意する。
- 1テナント部署1bot、nチャンネルである。Bot Channels Registrationごとに複数チャンネルと接続できる。
- URLのルーティングは、1botずつに設定する。 /api/{tenantcode}/{departmentcode}/messages など。
- ボットの対話ログを、テナントごとに箱を分けるか、1つの箱に入れるかは好みによる。セキュリティ的に複数テナントが同一の箱に入るのはNGというところがいたら、分けた方がよい。

# 実装例
[Creating a Single Bot Service to Support Multiple Bot Applications](https://www.microsoft.com/developerblog/2017/01/10/creating-a-single-bot-service-to-support-multiple-bot-applications/)から引用・変更し、日本語でのコメントを追加している。

```
var express = require('express');
var builder = require('botbuilder');
var port = process.env.PORT || 3978;
var app = express();

// テナントコード、部署コードを設定する。
// また、それぞれのBot Channels Registrationで取得したMicrosoft ApplicationID、Passwordを設定する。
var customersBots = [
  { tenantCode: 'tenantA', departmentCode: 'helpdesk', appid: 'hoge', passwd: 'hogeps' }, 
  { tenantCode: 'tenantA', departmentCode: 'backoffice', appid: 'fuga', passwd: 'fugaps' }, 
  { tenantCode: 'tenantB', departmentCode: 'consumer', appid: 'piyo', passwd: 'piyops' }, 
];

// ①エンドポイントを作成する。
customersBots.forEach(cust => {
  
  // ボットのインスタンスを作成
  var connector = new builder.ChatConnector({
    appId: cust.appid,
    appPassword: cust.passwd
  });
  var bot = new builder.UniversalBot(connector);

  // ダイアログのバインド
  bindDialogsToBot(bot, cust.tenantCode);

  // エンドポイント紐づけ
  app.post(`/api/${cust.tenantCode}/${cust.departmentCode}/messages`, connector.listen());
  
});

// ②ダイアログのバインド
function bindDialogsToBot (bot, tenantCode) {
  bot.dialog('/', [
    session => {
      session.send(`Hello... I'm a bot for tenantCode: '${tenantCode}' departmentCode: '${departmentCode}'`);
    }
  ]);
}

// expressのlistenを開始
app.listen(port, () => {
  console.log(`listening on port ${port}`);
});
```
  
# 実装ポイント
- **（重要）プレーンなnode.jsにはDI機能もなければ、シングルトンの考え方も無いので、テナントごとにインスタンスは分けられる**
- 何が言いたいかっていうと、逆にインスタンスが共有されるような変な心配をする必要がないということ。 for Java出身者。
- なのでテナントや部署ごとにメッセージクラスを作成し、「①エンドポイントを作成する。」のループの中で共通の親クラスを継承した個別メッセージクラスをインスタンス化しておけば、共通のIFでメッセージ内容だけ変更する、ということができる。  
![image.png](https://qiita-image-store.s3.amazonaws.com/0/195903/8e68c8ab-42db-e416-0553-21a7303b8a42.png)  
- ボットはLUISやQnAMakerと接続するケースがあると思うので、その場合は「①エンドポイントを作成する。」のループの中で、「②ダイアログのバインド」をベースにして、各種コグニティブサービスをバインドすればオーケー。
- イメージがつかみづらいが、ボットのインスタンス：URLのルーティング：Bot Channels Registrationは、すべて１：１：１となっている。なのでループ中のボットにバインドしたものが、ボットごとの振る舞いを決定する。
- Bot Channels Registrationにて、URLの設定を忘れずに。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/195903/d1aae7d5-cd39-9a9e-af94-662645c0f731.png)


# typescriptのベストプラクティス
## ディレクトリ構成
![image.png](https://qiita-image-store.s3.amazonaws.com/0/195903/c0d13cb1-c6ab-7826-62e1-553527c34e31.png)  

- bots配下にbotの振る舞いを決定するロジックをまとめる。
- 基本的なふるまいはBasicQnABotに入れて、個社ロジックを{tenantocode}/{botcode}/xxx.tsにまとめる。条件分岐などの個別なふるまいを定義する。
- servicesにはリソースごとのヘルパークラス的なものを配置する。
- modelsにはデータリソースとの接続クラスを配置する。データリソースに依存しない共通的な親クラスがあると望ましい。  
![image.png](https://qiita-image-store.s3.amazonaws.com/0/195903/2a13f258-db28-5ad3-f388-169987fed59f.png)

## 既知の問題 
- **BotFramework関連のSDK（npmリポジトリのライブラリ）が、typescriptの型定義ファイル未対応のものが多い。**
- github上のコミュニティ自体は盛んなので、これ作ってよ、とかプルリク送ると結構迅速に対応してくれる。
- が、プルリクの取り込みは行ってくれるが、npmリポジトリの更新はかなり時間がかかる。自分である程度定義しておくか、フォークしたものを一時的に取り込むなどの対応が必要。
- ちなみに[私のQnAMakerの質問](https://github.com/Microsoft/BotBuilder-CognitiveServices/issues/84)は無視されています……。

# 総括
- 以上です。ほかにもマルチテナントのやり方でいい方法があったらコメントにお寄せください。