Slack Enterprise Gridで動くSlack App開発の悩みどころ

by p1ass | November 12, 2020
tips | #slack #typescript #puppeteer #google-workspace

こんにちは、 IT 戦略部システム開発グループの岸です。

DeNA では、Google Workspace (旧 G Suite) で利用できるサービスの 1 つである Google グループを多く活用しています。Google グループはメーリングリスト用途に使われる場合が多いですが、社内では独自に JIRA や Confluence といったツールと連携し、グループのメンバーに適切な権限を自動で付与する仕組みを作成しています。 この機能により、新規メンバーが増えた場合でも 1 つのグループにユーザを追加するだけで、自動で複数のツールへ招待されるように設定できます。また、逆にグループから削除することで権限の剥奪ができるので、適切な権限管理の一端も担っています。

先日この考え方を拡張し、コミュニケーションツールとして利用している Slack Enterprise Grid に、Slack のチャンネルやユーザグループのメンバーを Google グループと連携させる Slack App を作成・導入しました。この機能により、

  • 新規メンバー参画時の Slack チャンネル招待
  • メンバー離脱時の Slack の情報共有範囲の棚卸し

といった忘れがちな業務の効率化ができました。

この記事では、そのシステムを開発する上で Slack Enterprise Grid における Slack App 開発に関連して悩んだところを 3 トピックほど紹介します。

  • 全ワークスペースに Slack App をインストールする方法
  • 共有チャンネルにおける外部ユーザの取り扱い
  • マルチワークスペースチャンネルの取り扱い

あまり情報が出回っていない Slack Enterprise Grid における Admin 向け Slack API の利用例などを紹介することで、「今後 Slack Enterprise Grid を活用していきたい」と考えている方の参考になれば幸いです。

システムの動作例

はじめにシステムのイメージを掴みやすいように、この機能の使い方を説明します。 まず、以下のスクリーンショットのように [ワークスペースのサブドメイン].ch.prj-abc-dev_my という Google グループを作成します。(Slack コマンドで簡単に作成できます)

とある Google Group のメンバー一覧画面

とある Google Group のメンバー一覧画面

そして、そのグループの中に、

  • prj-abc-dev: プロジェクト参画メンバーを1つにまとめているグループ
  • slack_link@dena.com: システムトリガー用のシステムアカウント

の2つを追加します。

このように設定すると、指定した Slack ワークスペースに #prj-abc-dev というチャンネルが作成され、そのメンバーが prj-abc-dev と同じになるようにシステムが自動で同期を取ってくれます。

動作例

動作例

Slack Enterprise Grid の特徴

このシステムは単一の Slack ワークスペースで動作するのではなく、Slack Enterprise Grid で動作します。しかし、Slack Enterprise Grid を使われている方はそこまで多くないと思われるので、Slack Enterprise Grid の特徴をいくつかピックアップして説明します。

Slack Enterprise Grid とは? | Slack

ワークスペースの上位概念であるオーガナイゼーション

Slack Enterprise Grid には、複数のワークスペースの管理するための概念として 「オーガナイゼーション(OrG)」 が存在します。 OrG は N 個のワークスペースで構成されています。ユーザは (招待された or 自由参加) のワークスペースに共通のアカウントでログインでき、ワークスペースをまたいだ DM やメッセージの検索が可能になります。

OrG の管理者はメンバーの管理やシングルサインオンの設定、請求の設定といった OrG 内の全てのワークスペースで必要となる設定の管理ができます。しかし、ワークスペース個別の設定は各ワークスペースの管理者に移譲されています。これにより、各ワークスペースが独立して運用しながらも、必要な部分だけ OrG 管理者による一括管理が実現されています。

Admin 向け Slack API

Slack App を作成する際に使われる Slack API ですが、Enterprise Grid でのみ利用可能な Admin 向けの Slack API が存在します。

Admin 向け Slack API では、通常の API では不可能なワークスペースの作成や管理、メンバーをワークスペースに参加させるといった機能が提供されています。 今回はこの API を使うことで自動同期の仕組みを実現させています。

連携システムのアーキテクチャ

このシステムは、

  • 自動同期を行うバッチ
  • Google グループの作成を補助する Slack App

の2つのシステムによって成り立っています。

自動同期を行うバッチ

このバッチでは、Google グループのメンバーと Slack チャンネルのメンバーの差分を計算し、その差分を埋めるように Slack API を叩いてメンバーの同期を行います。 このバッチは 10 分間隔で実行されており、最大でも 10 分でメンバーの同期が完了するようになっています。

Google グループの作成を補助する Slack App

最初に連携を開始するためには Google グループを作成する必要があると説明しました。しかし、命名規則にそった Google グループを手動で作るのは面倒くさい上に、タイポ等のヒューマンエラーを起こしやすいです。特に、複数個のチャンネルの同期設定を一度に行おうとすると負担が大きいです。

そこで、Google グループの作成を補助する Slack App を別途開発しました。この Slack App は連携したいチャンネルで /connect-to-google-group とコマンドを打つと、命名規則に沿った Google グループを作成してくれます。これにより、ユーザはレスポンスとして返ってきた URL をクリックし、同期するメンバーをまとめた Google グループを追加するだけで設定ができるようになりました。

Slack App (サンゴ)の設定完了レスポンス

Slack App (サンゴ)の設定完了レスポンス

同期機能開発において悩んだところ

さて、この同期機能はシュッと実現できそうに思えませんか?しかし、普通のワークスペースではなく、Slack Enterprise Grid であることに起因した悩みどころがいくつかありました。 ここではそのうちの 3 つをピックアップして紹介します。

全ワークスペースに Slack App をインストールする方法

Slack App をワークスペースをインストールするためには、OAuth2 のフローでインストールを行い、API トークンをデータストアに保存する必要があります。

OAuth2 の認可画面

OAuth2 の認可画面

通常、この手順はユーザが手動で行う必要があります。また、今回はワークスペースの管理者レベル以上のユーザトークンが必要になることから、一般のユーザがインストールを実行することが出来ません。 更に、DeNA の Slack Enterprise Grid 内には 500 を超えるワークスペースが存在するため、それぞれのワークスペースの管理者に頼んでインストールを実行してもらうのは現実的ではありません。

そこで、puppeteerを用いてブラウザを操作することによって、全てのワークスペースへインストールすることにしました。

具体的には、まず Admin 向け API で全てのワークスペースを取得します。その後、それぞれのワークスペースへ順にサインインし、インストールボタンを押します。 以下のコードは puppeteer を用いて1つのワークスペースに Slack App をインストールする部分を抜き出したコードです。

import  { Page } from 'puppeteer';

async installApp(page: Page, subdomain: string): Promise<void> {
    // ワークスペースのサインインページに移動する
    await page.waitForSelector('body > nav > div:nth-child(2) > div > div > div');
    await page.click(openWorkspaceListSelector, {delay: 100});
    const LoginOtherWorkspaceButtonSelector =
      'body > div.ReactModalPortal > div > div > div > div > div > div > div:last-child';
    await page.waitForSelector(LoginOtherWorkspaceButtonSelector);
    await Promise.all([
      page.waitForNavigation(),
      page.click(LoginOtherWorkspaceButtonSelector, { delay: 100 }),
    ]);

    // ワークスペースにサインイン
    await page.waitForSelector('#domain');
    await page.type('#domain', subdomain, { delay: 100 });
    const loginContinueButtonSelector =
      '#page_contents > div > div > div > div.p-signin_form__form_container > form > button';
    await Promise.all([
      page.waitForNavigation(),
      page.click(loginContinueButtonSelector, { delay: 100 }),
    ]);

    // インストール
    const allowButtonSelector =
'#oauth_install_form > div > div.p-oauth_page__buttons > button';
    await page.waitForSelector(allowButtonSelector);
    await page.click(allowButtonSelector, { delay: 100 });
    await page.waitFor(2000);
  }

ポイントとなるのは、一度サインインページに遷移しているところです。

他のワークスペースにサインインするボタン

他のワークスペースにサインインするボタン

Slack App のインストール URL にはワークスペースの情報が含まれていません。そのため、Slack 側がサインイン状況から勝手に Slack App をインストールするワークスペースを選択します。 しかし、今回はワークスペースを指定してインストールしたいので、毎回サインインすることでインストールするワークスペースを明示的に指定しています。

このように puppeteer を使っているのですが、正直つらい場面も多いです。 特に「ランダムでページ遷移がタイムアウトしてしまう問題」 にはとても頭を悩まされました。waitForSelector などで待っているので大体の場合は成功するのにも関わらず、ランダムでたまに失敗してしまいます。サーバのスペック等が影響していると考えられるのですが、根本的な対策は出来ていません。現在は try-catch で囲んで、タイムアウトしたらリトライするようにハンドリングしています。

共有チャンネルにおける外部ユーザの取り扱い

共有チャンネルとは、「外部のオーガナイゼーションとチャンネルを共有し、1つのチャンネルでコミュニケーションを取ることができる機能」です。

ひし形のマークが特徴

ひし形のマークが特徴

外部のオーガナイゼーションとチャンネルを共有する | Slack

当たり前ですが、共有チャンネルには外部のユーザが参加しています。しかし、Google グループは DeNA の社員しか参加できません。そのため、何も対策せずに共有チャンネルの同期を設定してしまうと、外部ユーザが全員キックされてしまう、といった問題が発生します。

共有チャンネルの同期を無効にすることで簡単に対策できますが、今回はできるだけ同期をしたいということで、外部ユーザを同期の対象から外すことにしました。
実装としては「適切なスコープを与えたトークンを利用した Slack API で外部ユーザの情報を取得したとしても、メールアドレスは取得できない」ことを利用して、メールアドレスの有無によって外部ユーザかどうか判断することにしました。

For external members and strangers, profile data will not contain email even if you have the users:read.email scope.

Working with channels between organizations | Slack

また、よく Slack API をご存知の方は Slack API のユーザオブジェクトに is_strangeris_restrictedis_ultra_restricted というフィールドが存在することをご存知かと思いますが、これらは使えません。それぞれの意味はそれぞれ以下のようになっています。

  • is_stranger: Slack App が追加されている共有チャンネルには参加していない外部ユーザであるか
  • is_restricted: ゲストユーザがどうか
  • is_ultra_restricted: シングルチャンネルゲストユーザかどうか

シングルチャンネルゲストやマルチチャンネルゲストは共有チャンネルとは違う概念なので使うことができません。(同様に連携の対象外に設定しています)
一番使えそうなのが is_stranger ですが、これは「Slack App からアクセスできるかどうか」が基準になっています。例えば、Slack App を追加した共有チャンネルに参加した外部ユーザの場合、is_stranger フィールドの値は false になってしまいます。そのため、外部ユーザかどうかのチェックには email フィールドの方をチェックしています。

外部ユーザと is_stranger の関係

外部ユーザと is_stranger の関係

マルチワークスペースチャンネルの取り扱い

マルチワークスペースチャンネルとは、Slack Enterprise Grid プランのみで作成できる「オーガナイゼーション内の別々のワークスペースでチャンネルを共有する機能」です。 共有チャンネルは外部のワークスペースと接続するのに対し、マルチワークスペースチャンネルは内部のワークスペースと接続します。 この機能を使うことで、部署 A と部署 B のワークスペース両方で同じチャンネルを使ってコミュニケーションできるようになります。

リングマークが特徴

リングマークが特徴

便利な機能ですが、Slack API でマルチワークスペースチャンネルを扱おうとすると様々な問題が発生します。その中でも特に問題になるのが「API トークンごとにアクセス可能なリソースが違う点」 です。

招待できない図

招待できない図

具体例を上げて説明します。
部署 A と部署 B でマルチワークスペースチャンネルを作成したとします。このとき Slack App は部署 A、部署 B のワークスペース両方にインストールされています。 そのため、部署 A 用の API トークンと部署 B 用の API トークンの2つが取得出来ています。
このとき、同期の設定を部署 A のワークスペースで行い、差分を計算したところ部署 B のメンバーを招待する必要になったとします。 愚直に実装すると、部署 A 用の API でメンバーの招待を試みます。しかし、招待するメンバーは部署 B にしか存在しないので user_not_found エラーになってしまいます。

これを解決するには、部署 A の API トークンから部署 B の API トークンへフォールバックする仕組みが必要になります。しかし、Slack API には「どの」ワークスペースと接続しているかを取得する API がありません。そのため、どのワークスペースにフォールバックするかを自動で判定することが出来ません。
次点の解決方法として、Admin 向け API を利用して部署 A のワークスペースに対象のユーザを招待する、という方法も考えられます。しかし、ワークスペースに勝手に招待するのは情報漏えいの観点から現実的ではなく、この方法でも解決には至りませんでした。

他にも様々なアイデアを考えたのですが、きれいに解決する手段は見つからず、最終的にユーザの設定手順を 1 ステップ増やすことにしました。全社ワークスペースと接続している場合はそのワークスペースで連携してもらい、その他の場合でもできるだけメンバーが多いワークスペースで実行してもらうようにしました。また、それでも招待できないユーザがいる場合は手動で追加してもらうようにしています。今後、より良い方法がないか模索していきたいです。

まとめ

この記事では、以下の 3 つのトピックをピックアップして、Slack Enterprise Grid における Slack App 開発の悩みどころを紹介しました。

  • 全ワークスペースに Slack App をインストールする方法
  • 共有チャンネルにおける外部ユーザの取り扱い
  • マルチワークスペースチャンネルの取り扱い

それらの解決策として以下のような方針を取りました。

  • puppeteer
  • email フィールドを見て外部ユーザかどうか判断
  • ユーザの設定手順を追加

この記事がどなたかの役に立てば幸いです。最後までお読みいただきありがとうございました。