Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

Vol. 06 SlackのBoltを使った開発体験が良かった話

こんにちは。技術本部 Bill One Engineering Unit Platformグループの前田です。昨年のAdvent Calendar以来の登場です。昨年はSREチームにいましたが、現在はPlatformグループに統合され、Bill Oneの改善などさまざまなことに向き合っています。今回は、SlackのBoltというライブラリについてお話しします。「へぇー、こういうライブラリもあるんだなー」という軽い気持ちで読める記事を目指しています。

なお、本記事はBill One 開発 Unit ブログリレー2024の第6弾、かつSansan Advent Calendar 2024の13日目の記事です。

はじめに

こんな方におすすめ

この記事は、以下のような方におすすめです。

  • Slackをエンジニアとして使い倒したい人
  • 触ったことがないフレームワークやライブラリを触るのが好きな人
  • 業務を効率化・自動化したい人

Boltに出会う前の認識

Boltを利用する以前、私がSlackのAPIでよく使っていたのは Incoming Webhook です。

api.slack.com

リンクに記載の通り、Slack側で設定した後に発行されたURLへ決められた形式のPOSTリクエストを送るだけで、どんな場所からでもSlackへの投稿が実行できます。しかし、例えば「スレッドへの投稿」や「指定したチャンネル以外への投稿」などはできません。凝ったことをやりたい場合、トークンを発行して適切な権限を割り振り、API仕様に沿ってリクエストを送ることで実現できます。仕様は次のページにまとまっています。

api.slack.com

Boltを知った経緯と紹介

こういった中で、開発ユニット内で利用する承認用のSlackアプリの構築が必要となりました。アプリの要件を満たすにはDBなど何らかの永続化層の利用が必須であり、かつ、「承認ボタンを押されたらメッセージを投稿する」ようなインタラクティブな動作が求められました。つまり、SlackからAPIリクエストを受け付ける必要が生じました。流石にこれは自前で環境を用意して実装していくのは厳しいぞ…ということで良い方法がないかと探していた時に見つけたのが、Boltです。

api.slack.com

The quickest way to start building on the Slack Platform

Bolt is a framework for JavaScript, Java, and Python that simplifies the process of creating Slack apps.

上記のページに書いてある通り、Slack App を作るプロセスを簡単にしてくれるフレームワークであり、3つの言語で提供されています。今回はJavaScriptのフレームワークをTypeScriptで利用したので、どんな感じだったか書いてみます。

使ってみて良かったところ

ドキュメント

Boltは3つの言語で提供されているのは前述の通りですが、すべての言語で日本語のドキュメントが用意されています。JavaScriptだと次のページです。

tools.slack.dev

このドキュメントには、開発中ずっとお世話になりました。やりたいこと別にページとコードが用意されているため、とりあえずコードをコピペしてSlackと疎通させて動作確認する、というサイクルを高速で回せます。いいドキュメントがあると開発が楽になるんだな、と実感しました。

Slackとの疎通

前述の通り、今回作るアプリケーションではSlackからのAPIリクエストを受け付ける必要があります。例えば、ユーザがSlashコマンドを入力したことを受け付ける場合、次の画像のようにどこにPOSTするか指定します。

Slashコマンド作成の画面。

しかし、開発用途で公開URLを用意するのは大変なので、Slack APIにはSocket Modeというものが用意されています。

api.slack.com

そして、BoltではSocket ModeになっているSlackアプリとの通信を起動時の設定1つで有効にできます。サンプルコードを記載します。

const { App } = require('@slack/bolt');

const app = new App({
  token: process.env.BOT_TOKEN,
  appToken: process.env.SLACK_APP_TOKEN,
  socketMode: true, // <- これ
});

(async () => {
  await app.start();
  console.log('⚡️ Bolt app started');
})();

Cloud Run や AWS Lambda のようなホスティング環境でSlackアプリを動作させる場合は、Socket ModeはオフにしてHTTPでリクエストを受ける必要があります。その場合はSocket Modeのオンオフを環境変数で判断するようにし、環境によって変数を分けることで簡単に実現できます。自前でWebSocketを管理したり、開発環境と本番環境で大きくコードを分けたりする必要はありません。

さらに、このSocket Modeを利用すれば、Slackと疎通した開発環境が10分程度で作れてしまいます。すぐに開発環境が用意できるのは、開発者体験がすごく良かったです。

型のある世界

Bolt for JavaScriptの中には、TypeScriptの型定義ファイルも含まれているため、TypeScriptの型システムの恩恵を受けられます。

型があると嬉しいケースとして、Slackで自前のモーダルを表示するケースを考えます。まずはモーダルの中身をどのようにするか決めるため、SlackのBlock Kit Builderを使うと良いです。そして、Block Kit Builderでモーダルを作ってみるとわかるのですが、 payload、つまり、モーダル表示に必要なJSONはかなり複雑です。加えて、ボタンを押した時に送信されるメッセージが表示される Actions Preview のJSONもとても複雑です。これらを自前でparseするのはとても大変です。

Boltでは、要素ごとの型がきちんと用意されています。入力要素ごと、表示要素ごとに型が分かれているので、自分でモーダルを組む際も型の恩恵を受けながらコードを書けます。例えば DataSource というフィールドを入力する要素を作るコードはこんな感じです。

function buildDataSourceBlock(): InputBlock {
  return {
    type: "input",
    element: {
      type: "plain_text_input",
      action_id: "data_source",
    },
    label: {
      type: "plain_text",
      text: "DataSource Name",
    },
    hint: {
      type: "plain_text",
      text: "アクセスする対象のデータを記載してね",
    },
  }
}

型があるとコーディングが安全だよね、という観点もありますが、私は単体テストが型安全に書ける恩恵がとても大きいと思っています。例えば、Slackに表示したモーダル上で、ユーザがSubmitボタンを押したとします。Boltでは、その時にSlack APIから送信されるJSONを解釈した結果を型として表現しています。モーダルSubmit結果の型定義は次のようになります。

/**
 * A Slack view_submission event wrapped in the standard metadata.
 *
 * This describes the entire JSON-encoded body of a view_submission event.
 */
export interface ViewSubmitAction {
    type: 'view_submission';
    // 中略
    user: {
        id: string;
        name: string;
        team_id?: string;
    };
    view: ViewOutput;
    api_app_id: string;
    token: string;
    trigger_id: string;
    // 以降省略
}

単体テスト上でこの型を再現します。これだけで、Slackから送られてきたリクエストを自前のアプリケーションで正しく解釈できているか、という単体テストが書けるようになります。次のような値をテスト上で用意して、必要な内容に書き換えるととても便利でした。JSONを文字列で定義してテストをする、といったことは不要です。

  const baseSubmitAction: ViewSubmitAction = {
    type: "view_submission",
    // 中略
    view: baseView,
    api_app_id: "",
    token: "",
    trigger_id: "",
    user: {
      // Rewrite each test
      id: "",
      name: "",
    },
  }

また、Slackのリクエストと自前のアプリケーションで使う型を相互変換する層を用意して、自前のアプリケーション実装とSlackの関心を切り離す、なんてこともできます。すごく便利ですね。

計装

私の前回の記事でOpenTelemetryの話をしたので、せっかくなので絡めてみます。前回の記事へのリンクを貼っておきます。

buildersbox.corp-sansan.com

Boltには、OpenTelemetryの自動計装の仕組みは現状存在しません。が、グローバルミドルウェアという仕組みを利用し、リクエストの処理の前後に処理を差し込むことで計装を行えます。

tools.slack.dev

グローバルミドルウェアを利用して、計装を実施してみましょう。あまり検証ができていないコードですが、紹介します。ルートスパンを初期化するコードは次の通りです。

import { context, trace } from "@opentelemetry/api"

/**
 * Create a span with a new TraceID.
 * @param rootSpanName name of root span
 * @param process Body of the process to be executed
 */
export async function initializeSpan<T>(rootSpanName: string, process: () => Promise<T>) {
  const tracer = trace.getTracer("app")
  // In SocketMode, all requests are tied to one span, so explicitly replace the root span
  const span = tracer.startSpan(rootSpanName, {
    root: true,
  })

  await context.with(trace.setSpan(context.active(), span), async () => {
    await process()
  })

  span.end()
}

そして上記のコードを、グローバルミドルウェアで呼び出します。

import { App } from "@slack/bolt"

const app = new App({/* 初期化処理は省略 */})

app.use(async ({ payload, next, logger }) => {
  let rootSpanName
  // `type` is not declared for SlashCommand.
  if (!("type" in payload)) {
    rootSpanName = `command ${payload.command}`
  } else {
    rootSpanName = `${payload.type}`
  }

  logger.debug("start root span.")
  await initializeSpan(rootSpanName, async () => {
    // Execution of various processes, such as commands and views
    await next()
  })
  // This part is reached after various processes such as command and view are completed.
  logger.debug("end root span.")
})

この状態で、Node.jsの自動計装を有効にし、ローカルのJaegerにトレースを送ると次の画像のようになります。計装が雑なのでスパンの後半に何をしているか分かりづらいですが、モーダルを構築した結果をSlackに送信してレスポンスを待っている状態だと思います。こうやって、計装も含めることができてしまいます。とても便利です。

Jaeger(モーダル表示時)

おわりに

実際に業務で利用してみて、良かったなと思ったBoltを紹介してみました。今回はホスティング前提の用途で考えて使ってみましたが、ローカルでも活用の余地がありそうです。Boltを使って書いたコードをSocket Modeで動かしておいて、Slackから呼び出して何かの作業を効率化する、なんて使い方ももしかしたらできるかもしれません。

また、今回は直接利用していないため調査できませんでしたが、単にSlackのWeb APIを呼び出すためのライブラリもあります。Slack Appを構築したい場合はBoltを使い、単にSlack APIを使いたい場合は node-slack-sdk を使うと良いかもしれません。

github.com

個人的な所感としては、Boltは開発者のことがよく考えられている、とてもいいフレームワークだと思いました。よくできたフレームワークを触っていると気持ちいいし、ドキュメントはこんなふうに書かれていると読みやすいんだなーという学びになりました。Slackアプリを作るための技術選定中にBoltを試した際、本当にコードが書きやすくて驚きました。JSONの構築と解釈に悩まされないって本当に良いです。

なお、最初に書いた 「へぇー、こういうライブラリもあるんだなー」という軽い気持ちで読める記事を目指しています。 がどのぐらい守られたかは…みなさまの解釈にお任せします。Boltを触っていて楽しかったんだなという気持ちがちょっとでも伝わればいいなと思います。

宣伝

Bill One 開発 Unit ブログリレー2024 と Sansan Advent Calendar 2024 は、まだまだ続きます! ブログリレーの最新の投稿について @SansanTech でお知らせしますので、ぜひフォローしてください。

© Sansan, Inc.