🦔

next generation Slack platformで障害対応のフローを統一&効率化した【実装編】

2023/07/30に公開

はじめに

こんにちは、さざなみです。この記事は前編のnew Slack platformで障害対応のフローを統一&効率化した【問題解決編】に続く後編の【実装編】です。
next generation Slack platformを使ったコードの事例を知りたい、何ができるのか知りたい方を対象にしています。next generation Slack platformは2022年の9月にリリースされたものであり、現在ベータ版です。そのため、今後のアップデート次第で本記事の内容とは動作が異なる可能性があります。
注意:以下のコードは2023年5月のものです。
公式ページにおいて、new Slack platformという表記からnext generation Slack platformに変わっていました。同じものを指します。

next generation Slack platform とは

クラウド上で動作する2022年9月にリリースされた新しいSlack Appです。

LambdaやGASなどを利用せずに実行することが可能で、用意されたCLIを使って開発環境実行、デプロイなどの操作をコマンドで簡単に実行することができます。

また、データストアも無料で利用可能で、Denoの簡単なコード実装で保存や更新などを行うことができます。

https://api.slack.com/automation

基本構造と振る舞い

Triger

ワークフローを開始する方法とタイミングを定義します。

今回作成した障害対応ボットの場合、報告開始のコマンドやショートカットボタンを指します。

Workflow

Triggerから呼び出され、FunctionをStepとして組み合わせてやりたいことの流れを実装します。

障害対応ボットの報告開始ワークフローの場合、フォーム表示ステップ、メッセージ送信ステップ、チャンネル作成ステップなどを呼び出しています。

Function

Workflowから呼び出されるStep(Function)の中身の実装が記述されている部分です。入力と出力を持ちます。

datastore

DynamoDBのようなデータストアです。CRUDの標準的なデータベース操作を行うことができます。

実装

障害の発見時にそれを報告するワークフローの作成を例に解説します。
完成物
https://youtu.be/qnO6GMP_cQs

環境構築

まず最初にSlackで用意されているCLIを使うためにインストールする必要があります。

curl -fsSL https://downloads.slack-edge.com/slack-cli/install.sh | bash

https://api.slack.com/automation/quickstart

Triggerの実装

TriggerはSlack上でのどのような操作を起点にしてワークフローを実行させるかを記述するものです。

今回はスラッシュコマンドやショートカットのクリックで利用できるボットを作成したいのでLink Triggerを作成しました。

https://api.slack.com/automation/triggers/link

トリガーを生成する
slack trigger create --workflow "#/workflows/start_form" --interactivity
start_form_trigger.ts
import { Trigger } from "deno-slack-api/types.ts";
import IncidentStartFormWorkflow from "../workflows/incident_start_form_workflow.ts";

const incidentStartFormTrigger: Trigger<typeof IncidentStartFormWorkflow.definition> = {
  type: "shortcut",
  name: "インシデント報告開始",
  description: "インシデント報告を始める",
  workflow: "#/workflows/incident_start_form_workflow",
  inputs: {
    interactivity: {
      value: "{{data.interactivity}}",
    },
    channel: {
      value: "{{data.channel_id}}",
    },
    userId: {
      value: "{{data.user_id}}",
    },
  },
};

export default incidentStartFormTrigger;

Workflowの実装

今回実装するWorkflowの必要な機能は以下です。ワークフローでは以下のものを順番にコードで記述していきます。

  1. 障害発生を報告するフォームが現れる。
  • タイトル
  • メンション
  • 何が起こっていますか
  • 予想規模
  • 新しい障害発生対応チャンネルを作成するか(yes, no)
  1. 入力後の作成ボタンを押すと、実行したチャンネルでbotから報告メッセージが送られる。
  2. 報告者にしか見えないメッセージで障害報告書とgithubのissueの新規作成をお願いする旨を送信する。
  3. datastoreに入力された内容を保存する。
  4. 「チャンネルを作成する」にyesと回答していた場合、障害専用の障害対応チャンネルを作成し、関係者を招待する。

0. 定義

incident_start_form_workflow.ts
import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";
import { IncidentStartFormFunctionDefinition } from "../functions/incident_start_form_function.ts";
import { IncidentAddDatastoreFunctionDefinition } from "../functions/incident_add_datastore_function.ts";
import { IncidentEndWorkflowFunctionDefinition } from  "../functions/incident_end_workflow_function.ts";
import { SCALE_TEXTS } from "../constant/index.ts"

/**
 * ワークフローの定義
 */
const IncidentStartFormWorkflow = DefineWorkflow({
  callback_id: "incident_start_form_workflow",
  title: "incident start form",
  description: "troubleshooting workflow",
  input_parameters: {
    properties: {
      interactivity: {
        type: Schema.slack.types.interactivity,
      },
      channel: {
        type: Schema.slack.types.channel_id,
      },
      userId: {
        type: Schema.slack.types.user_id
      }
    },
    required: ["interactivity"],
  },
});

1. 障害発生を報告するフォームが現れる

フォームを表示させる関数はSlackで最初から用意されているためそれを使いました。
Slackで用意されている関数は以下のページにまとめられています。

  • open_form フォームを開く
  • send_message メッセージを送信
  • add_user_to_usergroup ユーザーをグループに追加
  • create_channel チャンネル作成  など...

https://api.slack.com/automation/functions

フォームを開く
/**
 * 入力フォームを開く
 */
const inputForm = IncidentStartFormWorkflow.addStep( // 作成ボタンを押した時のデータがinputFormに格納される
  Schema.slack.functions.OpenForm, 
  {
    title: "障害対応 起票フォーム",
    interactivity: IncidentStartFormWorkflow.inputs.interactivity,
    submit_label: "作成",
    fields: {
      elements: [{
        name: 'title',
        title: 'タイトル',
        type: Schema.types.string,
      },{
        name: "mention",
        title: "メンション",
        type: Schema.types.array,
        items: {
          type: Schema.slack.types.user_id,
        },
      },{
        name: 'description',
        title: '何が起こっていますか',
        type: Schema.types.string,
        long: true,
      },      {
        name: "scale",
        title: "予想規模",
        type: Schema.types.string,
        choices: [
          { value: "S: 動作不全でユーザーに著しい不利益があるバグ",    title: "S: 動作不全でユーザーに著しい不利益があるバグ" },
          { value: "A: 一部動作不全でユーザ不利益があるバグ",         title: "A: 一部動作不全でユーザ不利益があるバグ" },
          { value: "B: 機能は動作するがユーザー不利益があるバグ",      title: "B: 機能は動作するがユーザー不利益があるバグ" },
          { value: "C: 機能は動作していないがユーザー不利益がないバグ", title: "C: 機能は動作していないがユーザー不利益がないバグ" },
          { value: "D: バグとは言えないが気になるもの",              title: "D: バグとは言えないが気になるもの" },
          { value: "わからない",                                 title: "わからない" }
      ],
        enum: ["s", "a", "b", "c", "d", "uncertain"],
        default: "uncertain",
      },{
        name: "create_channel",
        title: "新しい障害対応チャンネルを作成する",
        type: Schema.types.boolean,
        default: true,
      },],
      required: ['title','description', 'scale', 'create_channel'],
    },
  },
);

2. 入力後の作成ボタンを押すと、実行したチャンネルでbotから報告メッセージが送られる

1で入力された内容をメッセージとして送信する。

メッセージを送信
/**
 * 入力されたフォームの内容からbotが送信するメッセージを作成する
 */
const incidentStartFormFunctionStep = IncidentStartFormWorkflow.addStep(
IncidentStartFormFunctionDefinition,
  {
    title: inputForm.outputs.fields.title,
    mention: inputForm.outputs.fields.mention,
    description: inputForm.outputs.fields.description,
    scale:   inputForm.outputs.fields.scale,
    userId: IncidentStartFormWorkflow.inputs.userId,
  },
);

/**
 * botでメッセージを送信
 */
const sendIncidentStartMessageInIncidentChannel = IncidentStartFormWorkflow.addStep(Schema.slack.functions.SendMessage, {
  channel_id: IncidentStartFormWorkflow.inputs.channel,
  message    : incidentStartFormFunctionStep.outputs.report,
});

3. 報告者にしか見えないメッセージで障害報告書とgithubのissueの新規作成をお願いする旨を送信する。

Schema.slack.functions.SendEphemeralMessageを使うと以下のようなメッセージを送れる

特定の人にしか見えないメッセージ送信
/**
 * 起票者のみに以下のことを促すephemeral messageを送信
 * - kintone記載(必須)
 * - issue作成(任意)
 */
IncidentStartFormWorkflow.addStep(Schema.slack.functions.SendEphemeralMessage, {
  channel_id: IncidentStartFormWorkflow.inputs.channel,
  user_id: IncidentStartFormWorkflow.inputs.interactivity.interactor.id,
  message: `<@${ IncidentStartFormWorkflow.inputs.interactivity.interactor.id }>\n
  以下二つを作成・記入していただき、上スレッドでURLの共有をお願いいたします。:ham: \n
  - <https://xxxx.com/xxxx|kintone記載(必須)>\n
  - <https://github.com/Lancers/xxxx/issues/new?assignees=&labels=BUG,CRE&template=basic-template.md&title=|issue作成(任意)>\n
  `,
});

4. datastoreに入力された内容を保存する。

データ追加
/**
 * datastoreにデータ追加
 */
IncidentStartFormWorkflow.addStep(IncidentAddDatastoreFunctionDefinition, {
  title:  inputForm.outputs.fields.title,
  report: incidentStartFormFunctionStep.outputs.report,
  message_link: sendIncidentStartMessageInIncidentChannel.outputs.message_link,
});

5. 「チャンネルを作成する」にyesと回答していた場合、障害専用の障害対応チャンネルを作成し、関係者を招待する。

内容

  • incident_yyyymmdd_titleのチャンネルを作成
  • botで障害の内容のメッセージ送信
  • 報告者・mentionされた人・botをチャンネルに追加

workflow内で条件分岐の処理が使えなかったため、引数がtrueの場合はそれ以降の処理を終了させる関数を作成して条件分岐のような振る舞いを実装した。

チャンネル作成
/**
 * チャンネルを作成しない場合、これ以降のステップを実行しないためのfunctionを実行する
 *  
 *  この実装になった経緯
 *    ワークフロー内でif文を使うことができない。
 *    そのため、function内で実行中のワークフローを終了させる関数を作成して条件分岐のようにしている
 * 
 */
IncidentStartFormWorkflow.addStep(IncidentEndWorkflowFunctionDefinition, {
  end_workflow_flg: inputForm.outputs.fields.create_channel,
})

/**
 * チャンネルを作る
 */
const createChannelStep = IncidentStartFormWorkflow.addStep(Schema.slack.functions.CreateChannel, {
  channel_name : `incident_${String(new Date().getFullYear()) + String(('00' + (new Date().getMonth() + 1)).slice(-2)) + String(('00' + new Date().getDate()).slice(-2))}_${inputForm.outputs.fields.title}`,
  is_private   : false,
});

/**
 * botでメッセージを送信
 */
const sendIncidentStartMessageInCreatedChannel = IncidentStartFormWorkflow.addStep(Schema.slack.functions.SendMessage, {
  channel_id : createChannelStep.outputs.channel_id,
  message    : incidentStartFormFunctionStep.outputs.report,
});

/**
 * チャンネルにユーザーを追加する
 */
IncidentStartFormWorkflow.addStep(Schema.slack.functions.InviteUserToChannel, {
  channel_ids : [createChannelStep.outputs.channel_id],
  user_ids    : incidentStartFormFunctionStep.outputs.inviteUsers,
});

Functionの実装

カスタムFunctionの一部を紹介します。(export default部分のみ)

datastoreにデータを追加するFunction

export default SlackFunction(IncidentAddDatastoreFunctionDefinition, async ({ inputs, client }) => {
    let completed  = true;
    const uuid     = crypto.randomUUID();

    const started = new Date()
      .toLocaleDateString("ja-JP", {
        year: "numeric",
        month: "2-digit",
        day: "2-digit",
        hour: "2-digit",
        minute: "2-digit",
        second: "2-digit",
      })
      .replaceAll(/\/|:| /g, '')
    
    const response = await client.apps.datastore.put({
      datastore: "incidents",
      item: {
        id      : uuid,
        title   : inputs.title,
        report  : inputs.report,
        message_link : inputs.message_link,
        started : started,
        closed  : null
      },
    });

    if (!response.ok) {
      console.log(`エラーになりました: ${response.error}`);
      console.log(response.errors);
      completed = false;
    } else {
      console.log(`データ追加しました: ${response.item}`);
    }
    return { outputs: { completed } };
  },

boolの引数によってワークフローを終了させるFunction

export default SlackFunction(IncidentEndWorkflowFunctionDefinition, ({ inputs }) => {
  // end_workflow_flgがfalseの場合以降のワークフローをスキップする(停止させる)
  if (!inputs.end_workflow_flg) {
    return { completed: false };
  }
  return { outputs: { }};
});

datastoreを使えるようにする

datastoreのスキーマの定義を実装します。

datastores/incidents_datastore.ts
import { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts";

export const IncidentsDatastore = DefineDatastore({
  name        : "incidents",
  primary_key : "id",
  attributes  : {
    id            : { type: Schema.types.string },
    title         : { type: Schema.types.string },
    report        : { type: Schema.types.string },
    message_link  : { type: Schema.types.string },
    started       : { type: Schema.types.string },
    closed        : { type: Schema.types.string },
  },
});

https://api.slack.com/automation/datastores

Manifest

manifestにはこのbotがSlack上で動作するために必要な情報と設定を記載します。
botのアイコン、使用するworkflow, datastore、botに付与する権限など

export default Manifest({
  name: "incident_report_app",
  description:
    "create a incident report",
  icon: "assets/icon.png",
  workflows: [IncidentStartFormWorkflow, IncidentCloseWorkflow, IncidentCloseFormWorkflow],
  outgoingDomains: ["cdn.skypack.dev"],
  datastores: [IncidentsDatastore],
  botScopes: ["commands", "chat:write", "chat:write.public", "datastore:read", "datastore:write", "channels:manage", "groups:write",],
});

デプロイ

slack deployコマンドを実行します。割愛しますが、初回のデプロイ時やトリガーとそれに紐づく新しいワークフローを実装したときは、デプロイ後にトリガーの追加もする必要があります。

slack deploy

Tips

datastoreに格納されたデータを確認したい

incident-bot % slack datastore query '{"datastore": "datastore_name"}'

https://qiita.com/seratch/items/44fc82c54d8531e4201f

複数人で開発、デプロイしたい

共同開発者として追加する必要があります。

slack collaborator add [開発に参加してもらう方のメールアドレス]

その他のコマンド

incident-bot % slack collaborator -h
Manage app collaborators

USAGE
  $ slack collaborator <subcommand> [flags]

ALIASES
  collaborator, collaborators, owners

SUBCOMMANDS
  add         Add a collaborator to your app
  list        List all collaborators of an app
  remove      Remove a collaborator from an app

おわりに

問題解決編記事の投稿から1ヶ月ほど実装編の記事を書くのが遅れてしまい、その間にnext generation Slack platformもめちゃくちゃ進化していてとても焦っていますっ。最近ものすごい速度でアップデートされているslack APIなので、実装する際には公式のドキュメントをまず参照するのが良いと思います。これを実装し始めた9ヶ月前は公式にもほとんど情報がなく手探りで実装していましたが、今ではコードの例も含めてかなり丁寧に書かれている印象でしたので、とても参考になるのではと思います。

最新の情報は以下で見ることができます。
https://api.slack.com/lang/ja-jp/automation/announcement

Discussion