2022/09/26

Slackの次世代プラットフォームを試してみた

Slackの「次世代プラットフォーム」がこの記事を書いてる2週間ほど前、しれっとパブリックベータになりました。 今回はこの公開したてのプラットフォームでアプリをつくってみたので、それを紹介しつつ、感想を書きます。

全く新しいプラットフォーム

「new Slack platform」と謳う新しいプラットフォームは現在の「Slack API」を使ったSlackアプリをつくるものとは別物です。 パブリックベータになったばかりでかつ、Workspaceが有料プランではないと今のところデプロイできません。 それもあってか公式以外に情報が皆無の状態で、手探りながら試してみました。 結果わかったのはとにかく「今までとは全く違う」ということでした。 特徴を紹介しましょう。

Slackのクラウドで動く

まずこれが一番大きいです。 アプリケーションをSlackのクラウドへデプロイすることになります。 いわゆるサーバーレスです。 これにより、我々はサーバーを用意する必要がなくなります。 Google App Scriptで動かすか、料金を気にながら、AWS Lambdaで動かそうかとか悩まなくて済みます。

CLIがある

slackコマンドで動くCLIが用意されています。 これを使ってローカルでの開発から認証、Slackクラウドへのデプロイ、はたまたログの閲覧までをすることになります。

以下がヘルプの一部です。スクリーンショットに載せきれないほどコマンドがたくさんあります。

CLIだと各個人のローカルマシンからデプロイするだけではなく、 GitHub Actionsなどで動かせば、CI/CDっぽいこともできます。

Denoで書く

これも大きな特徴です。 このプラットフォームで動かすSlackのアプリケーションは「Deno」で書きます。 現在のプラットフォームは各々のサーバーで各々の言語を使ってアプリを実装することになりますが、 今回は「Denoで書いて、Slackのサーバーで動かす」のです。

「Bolt」というJavaScript用のSDKが使えます。 後述しますが「Types」の効きが強力で、文字列リテラルをみて、動的にタイプをつくってたりします。 補完のおかげで、今のところドキュメントには載っていない「この関数の引数はなんだろう」といった細かいことが分かったりします。

追記 「Bolt」が使えると記述したのですが、 中の人に「このデプロイ環境を提供する基盤で動かす場合は Bolt ではなく github.com/slackapi/deno-slack-sdk を使ってアプリを書く感じになっています!」と 指摘してもらいました

先日、Deno公式からもこの件についてリリースがでてました。

3つの概念

これから詳しく紹介する3つの概念でアプリが成り立っています。

  • Functions
  • Workflows
  • Triggers

最初、この概念を理解するのに時間がかかりました。 一度分かってしまえば「レゴブロック」のようにそれぞれを組み合わせて作れることがわかりますし、 コードの再利用性が高いです。 Functionsに至っては個別にテストを書けるのがよいです。

Datastore

サーバーレスの環境に「Datastore」が付属します。 例えば「ボットが受け取ったメッセージをDatastoreにためて、あとからScheduled Triggersで定期的に発言する」 なんてユースケースに使えるでしょう。 これで、Slackのクラウドだけで完結させることができちゃいます。 ちなみに、データはテーブル構造で表し、フィルタにDynamoDBのシンタックスが使えるようです。

Manifests

ボットの名前、アクセス権限、アイコンの画像含む、アプリケーションのメタ情報をプロジェクトで管理できます。 これでわかりにくい管理画面と格闘しなくて済みます。

つくってみた

実際に新しいプラットフォーム上で動くアプリをつくってみて、動かしてみました。

つくったアプリ

つくったアプリは「Karma Bot」というものです。

ある文字列に対してインクリメント、デクリメントできます。つまりこういうことです。

@karma yusukebe++
yusukebe: 1
@karma yusukebe++
yusukebe: 2
@karma yusukebe--
yusukebe: 1

文字列とそれに対する現在の数値をDatastoreに保存します。

以前、 全く同じ機能のもの をSlashコマンドでつくったことがあるのですが、 今回はボットに対してメンションを飛ばすことにします。

ちなみに、現在Slashコマンドの機能は新しいプラットフォームにありません。

コードを書く

さて、コードを書いていきます。 Denoのプロジェクトを作り、SDKに相当するライブラリを読み込みます。 公式のドキュメント サンプルレポジトリ を参考に進めていきます。

最初だいぶわかりにくかったです。 というのもコアとなっている以下4つの概念を理解しながら、試し試し書いていったからです。

  • Functions
  • Workflows
  • Triggers
  • Manifests

Functions

Functionsはいわば、パーツです。今回は以下の3つのFunctionsを作りました。

  • Extract - メッセージから対象の文字列とオペレーション「++」か「–」を取り出す。
  • Karma - パラメータを受け取り、値を更新、Datastoreにあればアップデート、なければ追加する。
  • Send - 結果をメッセージとして送る。

それぞれのFunctionsには入力出力の値をinput_parametersoutput_parametersで定義します。 JSON Schemaのイメージです。

以下はExtractのものです。

// string型の"body"パラメータを受け取る。値は必須。
input_parameters: {
  properties: {
    body: {
      type: Schema.types.string,
    },
  },
  required: [
    "body"
  ],
},
// オペレーションを表すboolean型の「plus」と
// 対象の文字列を表すstring型の「target」を返す。
output_parameters: {
  properties: {
    plus: {
      type: Schema.types.boolean,
      default: true,
    },
    target: {
      type: Schema.types.string,
    },
  },
  required: [],
},

Extractではbodyの中身から正規表現で値をとってきて、それに応じてplustargetを返します。 面白いのは、inputsという入力オブジェクトに型、というかタイプがしっかりとついてることです。

これは上記のinput_parametersで定義したからなんですが、なかなか高度な型推論です。 Denoのコードは全体的にタイプの効き方が「激しく」たいていのオブジェクトに付きます。 これは便利でDXを高めている一方、エラーメッセージが見にくくなるので、TypeScriptに慣れていないと辛いでしょう。

Functionsに対してテストを書くことができます。 特にExtractの場合は書きやすいです。

Deno.test("Extract function test", async () => {
  let inputs = {
    body: "<@karma> foo++",
  };
  let res = await ExtractFunction(createContext({ inputs }));
  assertEquals(res.outputs?.target, "foo");
  assertEquals(res.outputs?.plus, true);

  //...

Workflows

Functionsを束ねるのがWorkflowsです。今回のKarma Botの場合はこのような内容になりました。

  1. Triggersから受け取る値の定義を書く
  2. 値をExtractに渡す
  3. 値をKarmaに渡す
  4. 結果をSendで送る

「2」「3」「4」は上記で定義したFunctionsです。コードを見てください。

const extractStep = Workflow.addStep(ExtractFunction, {
  body: Workflow.inputs.text,
});

const karmaStep = Workflow.addStep(KarmaFunction, {
  target: extractStep.outputs.target,
  plus: extractStep.outputs.plus,
});

Workflow.addStep(SendFunction, {
  channelId: Workflow.inputs.channelId,
  target: extractStep.outputs.target,
  karma: karmaStep.outputs.karma,
});

このようにひとつひとつのFunctionsをブロックのように繋げていくのです。

これは面白い方法で、例えば「ExtractとKarmaの間に別のFunctionsを追加する」なんてことが、直感的に書けます。 各Functionsは独立していてユニットテストも可能なので、保守性にも優れています。

Triggers

Workflowsの一番最初に値を渡し、イベントをキックするのがTriggersです。 Triggersは以下4種類があります。

  • Link Triggers - 発行されたURLをSlackに貼り付けると発火する。
  • Scheduled Triggers - 毎日、毎週、毎月など定期的に実行される。
  • Event Triggers - メンション、リアクション、チャンネルの出入りなどのイベントで発火。
  • Webhook Triggers - 外部からのPOSTリクエストを受け取り発火。

Link Triggersは今回新しく導入された概念です。 CLIで発行したURLをSlackのメッセージとして貼り付けるとイベントが発生します。 今後そのTriggersを使いたい場合はURLをブックマークにしておくと便利だよとのこと。 イマイチ使う様子がイメージできないので、今回はあまり触らないようにしました。

Karma Botはapp_mentionedというEvent Triggersで発火します。 ドキュメントにはどのTriggersでどんなデータが取得できるかが書いてあるので、それを参考にします。 app_mentionedの欄には以下が書かれていました。

"data": {
  "event_type": "slack#/events/app_mentioned",
  "user_id": "U0123ABC",
  "text": "<@U0LAN0Z89> is it everything a river should be?",
  "channel_id": "C0123ABC",
  "channel_type": "public/private/im/mpim",
  "channel_name": "cool-channel"
}

今回はtextと、メッセージを書き込む時に必要なchannel_idを使います。 TriggerのコードはEventの種類の指定と、どんなデータを送るかを書くだけでした。

Manifest

最後に、Manifestという概念と実装を紹介します。 これはその名の通り、アプリケーションのメタ情報を記載するものです。 YAMLやJSONでもなく、TypeScriptで書きます。

export default Manifest({
  name: "karma",
  description: "Karma Bot",
  icon: "assets/icon.png",
  workflows: [Workflow],
  outgoingDomains: [],
  datastores: [Datastore],
  botScopes: [
    "app_mentions:read",
    "chat:write",
    "chat:write.public",
    "datastore:read",
    "datastore:write",
  ],
});

興味深いのはアイコンやbotScopesを指定していることです。 この新しいプラットフォームでは、このManifestにそれらを記載することで、 管理画面を一切触ることなくアプリを登録できます。 これはいいですね。あの管理画面はどこでなにを設定すればいいのか迷子になるので、 コードで管理できるのはありがたいです。

構成

最終的なプロジェクトの構成はこのようになりました。

.
├── README.md
├── assets
│   └── icon.png
├── deno.jsonc
├── env.sample.ts
├── env.ts
├── import_map.json
├── manifest.ts
├── slack.json
└── src
    ├── datastore.ts
    ├── functions
    │   ├── extract.test.ts
    │   ├── extract.ts
    │   ├── karma.ts
    │   └── send.ts
    ├── triggers
    │   └── mention.ts
    └── workflow.ts

ローカルでの開発

今回のプラットフォームの素晴らしい点はローカルの開発環境があることです。 slack runコマンドを入力すると手元でローカルサーバーが立ちます。

これでローカルのサーバーがイベントを待ちうけます。 次にCLIでTriggersを登録します。TriggersはWorkflowsとはまったく独立した存在なので、別途登録が必要です。

これで、ボットへのメンションが行われると、 このローカルサーバーが動き、Workflowsが実行され、結果がSlackへ投稿されます。

これまでのプラットフォームだと、 自前でトンネリングしない限りはサーバーへデプロイしてはじめてSlackで確認できたのですが、 これだとデプロイの手間が必要ありません。 さらに、変更を検知してサーバーが自動で再起動するので、 「コードを書く => Slackで確認する」が非常に素早く行えるのです。

デプロイする

デプロイしてみます。 これもコマンド一発です。

slack deploy

するとManifestのアプリケーションのメタ情報を参考に、対象のWorkspaceへのインストールが始まります。 アイコンもアップロードされているのが分かります。

では、いよいよ動かしてみましょう。

動いてますね。開発からデプロイまでSlack CLIひとつで出来ちゃいました。

ひとつ気になるのは、反応が遅いです。 ローカルで開発していた時にも感じていたのですが、 本番でもメンションしてから、メッセージが返ってくるまで5秒以上かかります。 うーん。一応「Denoにしたし速い」と公式が謳っていますが、この遅さはそれ以前の話な気がします。 そもそもそういうものなのか、ベータ版だからなのか、気になります。

以上、新しいプラットフォームでの初めてのアプリケーションが完成しました。

感想

感想を箇条書きにします。

  • サーバーを別途用意しなくていいのは楽です。どこで動かそうか迷うこともなくなります。
  • Denoはよい。最近はTypeScriptが好きなので、これは嬉しいです。
  • 依存のインストール作業が必要ない。つまりnpm installとかですね。Denoなのでやりません。node_modulesもできないです。
  • DenoのTypesが強力。めちゃくちゃ補完が効きます。個人的には好きです。
  • このくらいのアプリだったらDatastoreで十分です。GASを使ってGoogle Spreadsheetでデータを永続化するってパターンを置き換えることができるかもしれません。
  • 管理画面を触らなくて済む。パーミッションやトークンの管理などもしなくて済むのはとても楽です。

総じて、Developer Experience = DXがよいです。

加えて、思ったのは、SlackアプリというカジュアルなユースケースにDenoが採用されることで、 Denoの普及が進むのではないかということです。 言語とランタイムがロックインされてしまうのは賛否両論でしょう。 ただ、もしその中からひとつ選べと言われれば、Denoは最適な解のひとつだとも思いました。

まとめ

以上、Slackの新しいプラットフォームを、実際に動くアプリケーションを作りながら試してみました。 最近、トラベルブックでは社内で使えるアプリを新しくつくろうという試みがあるので、 そこで採用してもいいかもしれません。 ベータ版ということで、パフォーマンスの向上など今後の改善に期待です。

エンジニア募集中

トラベルブックではエンジニアを募集中です。 新しい技術に興味があるかた、フロント、バックエンドにかかわらずご興味があればご応募ください!