Bolt + TypeScript + AWS CDK + GitHub Actions で作るチャットボット風Slackアプリ

![Untitled Diagram(21).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/902821b3-efc5-d36e-d547-aa1c479f98f8.png)

## 背景

https://qiita.com/kazama1209/items/2685a825354c39ed064b

↑の続きというか改良版です。

[「Bolt」](https://slack.dev/bolt-js/ja-jp/tutorial/getting-started)と呼ばれるSlack公式のアプリケーションフレームワークを使ってより実用的な機能を簡単に作成できるようにしたほか、[「AWS CDK」](https://aws.amazon.com/jp/cdk/)によるインフラ管理や[「GitHub Actions」](https://github.co.jp/features/actions)によるCI/CD環境の構築を行う事で高いメンテナンス性を実現しました。

## 完成イメージ

![タイトルなし (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/145a90fd-4dba-5c60-d2f8-864c508eccb3.gif)

Slackの機能である「[スラッシュコマンド](https://slack.com/intl/ja-jp/help/articles/201259356-Slack-%E3%81%AE%E3%82%B9%E3%83%A9%E3%83%83%E3%82%B7%E3%83%A5%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89)」を起点にチャットボットを呼び出すイメージですね。

今回はサンプルとして「/animals」というスラッシュコマンドを作成してみました。

コマンドを実行すると「どの動物について知りたいですか？」とSlackアプリから聞かれるので、いずれかの選択肢を選ぶとそれに合わせた結果を返してくれます。

サンプルなのでとても簡素な感じですが、このロジックを上手く活用すれば簡易的なチャットボットのようなものが作れるはず。

## 仕様

- Bolt
- TypeScript
- AWS CDK（Lambda、API Gateway）
- GitHub Actions

※[前回の記事](https://qiita.com/kazama1209/items/2685a825354c39ed064b)ではRubyを使いましたが、BoltはJavaScript（TypeScript）、Python、Javaにしか対応していないため、今回はTypeScriptを使います。

## 実装

前置きはほどほどに、早速実装していきましょう。

### Slackアプリの作成

![スクリーンショット 2021-08-08 7.15.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/42a6aa8f-5268-5e95-9e40-357a95929d6b.png)

[https://api.slack.com/apps](https://api.slack.com/apps)

↑のページからSlackアプリを作成しましょう。

![スクリーンショット 2021-08-15 5.11.44_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/c79cfbed-5176-8bc9-406d-fe1966a0ba56.jpeg)

- App Name
    - 適当なアプリ名を入力
- Pick a workspace to develop your app in
    - 追加したいワークスペースを選択

![スクリーンショット 2021-08-15 5.14.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/8b269eb0-271c-cef8-b821-a475895dab4f.png)

Slackアプリの作成が終わったら、左サイドバーの「OAuth & Permissions」をクリックし、Scopes（権限）の設定を行います。今回はスラッシュコマンドの実行とメッセージの送信ができれば良いので、

- commands
- chat:write

を追加してください。

![スクリーンショット 2021-08-15 5.15.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/694ba3c7-339b-1ce6-d512-5eaca75ccc44.png)

Scopesの設定が終わったら、「Install to WorkSpace」をクリック。

![スクリーンショット 2021-08-15 5.17.52_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/b2a7806c-d18c-e4cd-8eff-d96752d3fc7b.jpeg)

すると認証トークンが発行されるので、一旦メモなどに控えておきましょう。

![スクリーンショット 2021-08-15 6.42.42_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/85aa101c-3629-d454-adc3-2aa02c78b74f.jpeg)

もう一つ、左サイドバーの「Basic Information」をクリックして下の方にスクロールすると「Signing Secret」というシークレットがあるので、こちらもメモに控えておいてください。

![スクリーンショット 2021-08-08 7.35.16_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/f88960c3-fd7a-48ca-adc8-bfba91c3d22a.jpeg)

あとはチャンネル設定の「インテグレーション」から「アプリを追加する」をクリックし、先ほど作ったSlackアプリを追加すればOKです。

### サーバーの作成

次は、スラッシュコマンドが実行された後に結果を返すためのサーバーを作成していきましょう。

#### 作業ディレクトリを作成

```
$ mkdir slack-chat-bot && cd slack-chat-bot
```

※名前は何でもOK。

#### AWS CDKを導入

具体的なアプリケーションコードを書く前に、AWS CDKを導入しておきます。（ディレクトリが空の状態じゃないと後述の「$ cdk init 〇〇」が実行できないため。）

参照記事: [AWS CDKの始め方](https://qiita.com/maripoyo/items/f30ac4e8d2047c4edbe4)

※各種CLIツールが必要になるので、まだインストールしていない方は↑の記事などを参考に準備しておいてください。

```
$ cdk init app --language typescript

...省略...

Applying project template app for typescript
# Welcome to your CDK TypeScript project!

This is a blank project for TypeScript development with CDK.

The `cdk.json` file tells the CDK Toolkit how to execute your app.

## Useful commands

 * `npm run build`   compile typescript to js
 * `npm run watch`   watch for changes and compile
 * `npm run test`    perform the jest unit tests
 * `cdk deploy`      deploy this stack to your default AWS account/region
 * `cdk diff`        compare deployed stack with current state
 * `cdk synth`       emits the synthesized CloudFormation template

Initializing a new git repository...
Executing npm install...

...省略...

✅ All done!
```

少し時間がかかるかもしれませんが、完了した後は次のような感じの構成になっているはずです。

```
slack-chat-bot
├── bin
│   └── slack-chat-bot.ts
├── cdk.json
├── jest.config.js
├── lib
│   └── slack-chat-bot-stack.ts
├── test
│   └── slack-chat-bot.test.ts
├── package-lock.json
├── package.json
├── README.md
└── tsconfig.json
```

参照記事: [CDKアプリケーションのディレクトリ構造・処理フロー入門](https://blog.3qe.us/entry/2019/10/17/200342)

```ts:./lib/slack-chat-bot-stack.ts
import * as cdk from '@aws-cdk/core';

export class SlackChatBotStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // The code that defines your stack goes here
  }
}
```

主にこの中の「./lib/slack-chat-bot-stack.ts」をいじってAWSリソースを作成していくわけですが、それはまた後ほど行うという事にしてここは一旦ストップでOK。

### Boltを導入

次にBoltを導入して各種コードを書いていきましょう。

#### 必要なライブラリをインストール

```
$ npm install @slack/bolt@^2.7.0 aws-serverless-express@^3.3.8
$ npm install -D dotenv@^10.0.0 nodemon@^2.0.1 ts-node@^9.0.0 @types/aws-serverless-express@^3.3.5
```

- @slack/bolt
    - Bolt本体
- aws-serverless-express
    - Express（Boltの基盤となっているNode.js用Webアプリケーションフレームワーク）をLambda + API Gatewayで動かしてくれる
- dotenv
    - 環境変数を管理してくれる
- nodemon
    - コードの変更を検知して自動でサーバーを再起動してくれる
- ts-node
    - TypeScriptをトランスパイルしなくてもそのまま使えるようになる
- @types/〇〇
    - 型定義


※各バージョンはお好みで構いませんが、本記事のコードにおいて上記以外の正常な動作は保証しかねますので、できれば合わせた方が良いかもです。

#### 各種コード

```
$ mkdir src src/views
$ touch src/App.ts src/views.ts src/views/question.ts src/views/dog.ts src/views/cat.ts src/views/rabbit.ts .env
```

```ts:./src/App.ts
import * as dotenv from "dotenv"

import {
  App,
  ExpressReceiver,
  BlockAction,
  StaticSelectAction,
  ViewOutput,
} from "@slack/bolt"
import { LogLevel } from "@slack/logger"

import * as awsServerlessExpress from "aws-serverless-express"
import { APIGatewayProxyEvent, Context } from "aws-lambda"

// 各種ビュー部分
import {
  question,
  dog,
  cat,
  rabbit
} from "./views"

dotenv.config()

// これを設定する事により3秒以内に処理を完了できる前提でLambda環境における正常な動作が保証される
const processBeforeResponse = true

const expressReceiver = new ExpressReceiver({
  signingSecret: process.env.SLACK_SIGNING_SECRET ?? "",
  processBeforeResponse,
})

// 初期設定
const app = new App({
  logLevel: LogLevel.DEBUG,
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  receiver: expressReceiver,
  processBeforeResponse,
})

// aws-serverless-expressによってプロキシ
const server = awsServerlessExpress.createServer(expressReceiver.app)
export const handler = (
  event: APIGatewayProxyEvent,
  context: Context
): void => {
  awsServerlessExpress.proxy(server, event, context)
}

// スラッシュコマンド「/animals」が実行された際の処理
app.command("/animals", async ({ ack, context, payload }) => {
  await ack()

  // 質問用のモーダル画面を表示
  await app.client.views.open({
    token: context.botToken as string,
    trigger_id: payload.trigger_id,
    view: question,
  })
})

interface SlackActionBody extends BlockAction {
  view: ViewOutput
}

// action_id = select-optionのアクションが実行された際の処理（今回で言えば選択肢を選んだ際）
app.action("select-option", async ({ ack, action, body, context }) => {
  await ack()

  const selectedOption: string = (action as StaticSelectAction).selected_option
    .value

  // 選択肢によって返すビューを変更
  const answer = (selectedOption: string) => {
    switch (selectedOption) {
      case "dog":    // イヌの場合
        return dog
      case "cat":    // ネコの場合
        return cat
      case "rabbit": // ウサギの場合
        return rabbit
      default:
        return question
    }
  }

  // 選択肢に合わせてモーダル画面を更新
  await app.client.views.update({
    token: context.botToken as string,
    view_id: (body as SlackActionBody).view.id,
    view: answer(selectedOption),
  })
})

// action_id = select-option-resetのアクションが実行された際の処理（今回で言えば戻るボタンを押した際）
app.action("select-option-reset", async ({ ack, body, context }) => {
  await ack()

  // モーダル画面を更新（最初の質問に戻す）
  await app.client.views.update({
    token: context.botToken as string,
    view_id: (body as SlackActionBody).view.id,
    view: question,
  })
})

// ローカル環境においては通常起動
if (process.env.IS_LOCAL === "true") {
  void (async () => {
    await app.start(process.env.PORT || 3000)
    console.log("⚡️ Bolt app is running!")
  })()
}
```

```ts:./src/views/question.ts
import { View } from "@slack/bolt"

// 質問を投げるビュー
export const question: View = {
  type: "modal",
  title: {
    type: "plain_text",
    text: "Slack Chatbot",
    emoji: true,
  },
  blocks: [
    {
      type: "section",
      text: {
        type: "mrkdwn",
        text: "どの動物について知りたいですか？",
        verbatim: true,
      },
    },
    {
      type: "divider",
    },
    {
      type: "actions",
      elements: [
        {
          type: "static_select",
          action_id: "select-option",
          placeholder: {
            type: "plain_text",
            text: "選択してください",
            emoji: true,
          },
          options: [
            {
              text: {
                type: "plain_text",
                text: "イヌについて",
                emoji: true,
              },
              value: "dog",
            },
            {
              text: {
                type: "plain_text",
                text: "ネコについて",
                emoji: true,
              },
              value: "cat",
            },
            {
              text: {
                type: "plain_text",
                text: "ウサギについて",
                emoji: true,
              },
              value: "rabbit",
            },
          ],
        },
      ],
    },
  ]
}
```

```ts:./src/views/dog.ts
import { View } from "@slack/bolt"

// 「イヌ」が選ばれた場合に返すビュー
export const dog: View = {
  type: "modal",
  title: {
    type: "plain_text",
    text: "Slack Chatbot",
    emoji: true,
  },
  blocks: [
    {
      type: "section",
      text: {
        type: "mrkdwn",
        text: "イヌについて",
        verbatim: true,
      },
      accessory: {
        type: "button",
        action_id: "select-option-reset",
        text: {
          type: "plain_text",
          text: "戻る",
          emoji: true,
        },
      },
    },
    {
      type: "divider",
    },
    {
      type: "image",
      alt_text: "イヌについて",
      image_url:
      "https://www.pakutaso.com/shared/img/thumb/PPW_uturogenashibaken_TP_V.jpg",
    },
    {
      type: "divider",
    },
    {
      type: "section",
      text: {
        type: "mrkdwn",
        text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
        verbatim: true,
      },
    },
  ]
}
```

```ts:./src/views/cat.ts
import { View } from "@slack/bolt"

// 「ネコ」が選ばれた場合に返すビュー
export const cat: View = {
  type: "modal",
  title: {
    type: "plain_text",
    text: "Slack Chatbot",
    emoji: true,
  },
  blocks: [
    {
      type: "section",
      text: {
        type: "mrkdwn",
        text: "ネコについて",
        verbatim: true,
      },
      accessory: {
        type: "button",
        action_id: "select-option-reset",
        text: {
          type: "plain_text",
          text: "戻る",
          emoji: true,
        },
      },
    },
    {
      type: "divider",
    },
    {
      type: "image",
      alt_text: "ネコについて",
      image_url:
      "https://www.pakutaso.com/shared/img/thumb/AME19716064_TP_V.jpg",
    },
    {
      type: "divider",
    },
    {
      type: "section",
      text: {
        type: "mrkdwn",
        text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
        verbatim: true,
      },
    },
  ]
}
```

```ts:./src/views/rabbit.ts
import { View } from "@slack/bolt"

// 「ウサギ」が選ばれた場合に返すビュー
export const rabbit: View = {
  type: "modal",
  title: {
    type: "plain_text",
    text: "Slack Chatbot",
    emoji: true,
  },
  blocks: [
    {
      type: "section",
      text: {
        type: "mrkdwn",
        text: "ウサギについて",
        verbatim: true,
      },
      accessory: {
        type: "button",
        action_id: "select-option-reset",
        text: {
          type: "plain_text",
          text: "戻る",
          emoji: true,
        },
      },
    },
    {
      type: "divider",
    },
    {
      type: "image",
      alt_text: "ウサギについて",
      image_url:
      "https://www.pakutaso.com/shared/img/thumb/USAGI0I9A6075_TP_V.jpg",
    },
    {
      type: "divider",
    },
    {
      type: "section",
      text: {
        type: "mrkdwn",
        text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
        verbatim: true,
      },
    },
  ]
}
```

「./src/views/〇〇」ファイル内の各JSONについては、 [Block Kit Builder](https://app.slack.com/block-kit-builder) を使って作成したものを使っています。簡単に言うと、Slackのメッセージをよりリッチな感じで作れるツールですね。

![スクリーンショット 2021-08-15 7.13.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/a3964948-457a-9835-270a-abb8735e1590.png)

ここでお好みのデザインを作り、右側に生成されたJSONをコピペすればOKです。

```ts:./src/views.ts
import { question } from "./views/question"
import { dog } from "./views/dog"
import { cat } from "./views/cat"
import { rabbit } from "./views/rabbit"

// まとめてexport
export {
  question,
  dog,
  cat,
  rabbit
}
```

```:.env
PORT=3000
SLACK_BOT_TOKEN=先ほど取得したトークン
SLACK_SIGNING_SECRET=先ほど取得したシークレット
```

### スラッシュコマンドを作成

![スクリーンショット 2021-08-15 7.17.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/f171a53d-f4ee-c903-c481-096f73096cd3.png)

Slackアプリの設定画面を開き、左サイドバーの「Slash Commands」から「Create New Command」をクリック。

![スクリーンショット 2021-08-15 7.19.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/aa4db950-56ae-13c4-905b-e939dbbc58e7.png)

- Command: スラッシュコマンド
    - /animals
- Request URL: スラッシュコマンドを実行した際に飛ぶリクエスト先URL
    - https://***********.ngrok.io/slack/events
    - localhostでは動かないため、ngrokを使って独自のドメインを割り当てる
    - 参照: [ngrokの利用方法](https://qiita.com/Marusoccer/items/7033c1bb9c85bf6789bd)
    - Boltのポート番号は3000なので「$ ngrok http 3000」
- Short Description: コマンドの説明
    - 選択した動物の情報を返してくれるチャットボットを呼び出すコマンド
- Usage Hint: 使い方のヒント
    - 空欄でOK。

各項目を入力し、保存してください。

これで今回の場合、「/animals」というスラッシュコマンドを実行すると「https://***********.ngrok.io/slack/events」にPOSTリクエストが飛ぶようになり、先ほど「./src/App.ts」ファイル内で定義した

```ts:./src/App.ts
// スラッシュコマンド「/animals」が実行された際の処理
app.command("/animals", async ({ ack, context, payload }) => {
  await ack()

  // 質問用のモーダル画面を表示
  await app.client.views.open({
    token: context.botToken as string,
    trigger_id: payload.trigger_id,
    view: question,
  })
})
```

この部分の処理が走るというわけですね。

### Interactivityを有効化

最後に、Interactivityの有効化を行います。

今のままだと、スラッシュコマンドで呼び出したチャットボットに対してリアクションを起こした際、その内容をサーバーに伝える術がありません。つまり、何も起こらずエラーになってしまいます。

![スクリーンショット 2021-08-15 7.23.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/9b4523bf-e605-5d6c-5716-5513227adca3.png)

これを解決するためには、Slackアプリ管理画面の左サイドバーの「Interactivity & Shortcuts」からInteractivityをONにする必要があります。

![スクリーンショット 2021-08-15 7.24.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/3dabeb7f-84d8-77ee-d0e0-cec5adea4b26.png)


- Request URL
    - https://***********.ngrok.io/slack/events

ONに変更後、Request URLに上記のようなURLを入力しましょう。

すると、今後Interactiveなコンポーネント（先ほど [Block Kit Builder](https://app.slack.com/block-kit-builder) で作成したようなボタン、テキストフォーム、チェックボックスなど）に対してリアクションを起こした際には「https://***********.ngrok.io/slack/events」にPOSTリクエストが飛ぶようになり、先ほど「./src/App.ts」ファイル内で定義した

```ts:./src/App.ts
// action_id = select-optionのアクションが実行された際の処理（今回で言えば選択肢を選んだ際）
app.action("select-option", async ({ ack, action, body, context }) => {
  await ack()

  const selectedOption: string = (action as StaticSelectAction).selected_option
    .value

  // 選択肢によって返すビューを変更
  const answer = (selectedOption: string) => {
    switch (selectedOption) {
      case "dog":    // イヌの場合
        return dog
      case "cat":    // ネコの場合
        return cat
      case "rabbit": // ウサギの場合
        return rabbit
      default:
        return question
    }
  }

  // 選択肢に合わせてモーダル画面を更新
  await app.client.views.update({
    token: context.botToken as string,
    view_id: (body as SlackActionBody).view.id,
    view: answer(selectedOption),
  })
})
```

この部分の処理が走るという仕組みですね。

### 動作確認

これで大半の実装は完了したので、一旦動作確認してみましょう。

```
$ touch nodemon.json
```

```json:./nodemon.json
{
  "watch": ["src"],
  "ext": "ts,tsx",
  "exec": "./node_modules/.bin/ts-node ./src/App.ts"
}
```

```json:./package.json
{
  ...省略...
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "test": "jest",
    "dev": "IS_LOCAL=true ./node_modules/.bin/nodemon", ←追加
    "cdk": "cdk"
  },
  "devDependencies": {
    ...省略...
  },
  "dependencies": {
    ...省略...
  }
}
```

nodemonの設定ファイルを作成した後、「./package.json」ファイル内の「scripts」に「"dev": "IS_LOCAL=true ./node_modules/.bin/nodemon"」という記述を追加します。

```
$ npm run dev

> IS_LOCAL=true ./node_modules/.bin/nodemon

[nodemon] 2.0.12
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): src/**/*
[nodemon] watching extensions: ts,tsx
[nodemon] starting `./node_modules/.bin/ts-node ./src/App.ts`
[DEBUG]  web-api:WebClient:0 initialized
[DEBUG]  web-api:WebClient:0 apiCall('auth.test') start
[DEBUG]  web-api:WebClient:0 will perform http request
[DEBUG]  web-api:WebClient:0 apiCall('auth.test') start
[DEBUG]  web-api:WebClient:0 will perform http request
⚡️ Bolt app is running!
```

こんな感じでアプリが起動し始めればOK。

Slackアプリを追加したチャンネル内で「/animals」と入力し、

![スクリーンショット 2021-08-15 7.37.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/12a40798-397d-461f-9a09-0e2fee563e4d.png)

こんな感じで対話できるようになっていれば成功です。

### デプロイ

それでは、いよいよ本番環境へデプロイしていきましょう。最初にAWS CDKを導入しているので、そちらを使いコードでAWSリソースを管理していきます。

#### 必要なライブラリをインストール

```
$ npm install -D @aws-cdk/aws-lambda-nodejs@^1.104.0 @aws-cdk/aws-apigateway@^1.104.0 @aws-cdk/aws-ssm@^1.104.0
```

- @aws-cdk/aws-lambda-nodejs
    - Lambdaに合わせたTypeScriptのトランスコンパイルとバンドルを簡単に行ってくれる
        - 参照記事: [aws-lambda-nodejs ってなに？](https://dev.classmethod.jp/articles/easy-bundle-ts-for-lambda/)
- @aws-cdk/〇〇
    - 各種AWSリソース用

※AWS CDKはかなり高頻度でアップデート（破壊的な変更を含む）が行われているため、バージョンが異なると途端に動作しなくなってしまう可能性があります。できるだけ揃えるもしくは近いものをインストールしてください。

#### 各種AWSリソースを定義

「./lib/slack-chat-bot-stack.ts」ファイルを以下のように修正してください。

```ts:./lib/slack-chat-bot-stack.ts
import * as cdk from "@aws-cdk/core"
import { Runtime } from "@aws-cdk/aws-lambda"
import { NodejsFunction } from "@aws-cdk/aws-lambda-nodejs"
import * as apigateway from "@aws-cdk/aws-apigateway"
import * as ssm from "@aws-cdk/aws-ssm"
import * as dotenv from "dotenv"

dotenv.config()

export class SlackChatBotStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    // パラメータストアからSLACK_BOT_TOKENを取得
    const slackBotToken = ssm.StringParameter.fromStringParameterAttributes(this, "slackSigningSecret", {
      parameterName: "/slack-chat-bot/SLACK_BOT_TOKEN",
    }).stringValue

    // パラメータストアからSLACK_SIGNING_SECRETを取得
    const slackSigningSecret = ssm.StringParameter.fromStringParameterAttributes(this, "channelAccessToken", {
      parameterName: "/slack-chat-bot/SLACK_SIGNING_SECRET",
    }).stringValue

    // Lambda関数
    const lambdaFunction = new NodejsFunction(this, "lambdaFunction", {
      functionName: "slack-chat-bot", // 関数名
      entry: "src/App.ts",            // ファイル
      handler: "handler",             // 実行する関数
      runtime: Runtime.NODEJS_14_X,   // ランタイム（言語）
      environment: {                  // 環境変数
        SLACK_BOT_TOKEN: slackBotToken || process.env.SLACK_BOT_TOKEN || "",
        SLACK_SIGNING_SECRET: slackSigningSecret || process.env.SLACK_SIGNING_SECRET || "",
      },
    })

    // API Gateway
    new apigateway.LambdaRestApi(this, "apigateway", {
      handler: lambdaFunction,
    })
  }
}
```

#### SSMパラメータストアにSLACK_BOT_TOKENとSLACK_SIGNING_SECRETを登録

Lambda側で環境変数として取得するために、AWC CLIツールを使い

- SLACK_BOT_TOKEN
- SLACK_SIGNING_SECRET

の２つをSSMパラメータストアに登録しておきます。

```
$ aws ssm put-parameter --name "/slack-chat-bot/SLACK_BOT_TOKEN" --type "String" --value "トークンの値"
$ aws ssm put-parameter --name "/slack-chat-bot/SLACK_SIGNING_SECRET" --type "String" --value "シークレットの値"
```

![スクリーンショット 2021-08-15 8.08.18_censored (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/0bc27cf1-664d-dc04-7483-48ba956c02dc.jpeg)

こんな感じで追加されていればOK。

#### cdk deploy

デプロイを実行しましょう。

```
$ cdk deploy --require-approval never

Sending build context to Docker daemon  95.74kB
Step 1/10 : ARG IMAGE=public.ecr.aws/sam/build-nodejs12.x
Step 2/10 : FROM $IMAGE
 ---> 18b23d117812
Step 3/10 : RUN npm install --global yarn@1.22.5
 ---> Using cache
 ---> a6fe6d58ad52
Step 4/10 : ARG ESBUILD_VERSION=0
 ---> Using cache
 ---> 5507692c0029
Step 5/10 : RUN npm install --global --unsafe-perm=true esbuild@$ESBUILD_VERSION
 ---> Using cache
 ---> ebd9b315f742
Step 6/10 : RUN mkdir /tmp/npm-cache &&     chmod -R 777 /tmp/npm-cache &&     npm config --global set cache /tmp/npm-cache
 ---> Using cache
 ---> 69ade31ba324
Step 7/10 : RUN mkdir /tmp/yarn-cache &&     chmod -R 777 /tmp/yarn-cache &&     yarn config set cache-folder /tmp/yarn-cache
 ---> Using cache
 ---> 7aa33c7d4f11
Step 8/10 : RUN npm config --global set update-notifier false
 ---> Using cache
 ---> 7ccaa9775816
Step 9/10 : RUN /sbin/useradd -u 1000 user && chmod 711 /
 ---> Using cache
 ---> 8153f81198e8
Step 10/10 : CMD [ "esbuild" ]
 ---> Using cache
 ---> 62d438359723
Successfully built ****************
Successfully tagged ****************:latest
Bundling asset SlackChatBotStack/lambdaFunction/Code/Stage...
esbuild cannot run locally. Switching to Docker bundling.

  asset-output/index.js  1.6mb ⚠️

⚡ Done in 1138ms
SlackChatBotStack: deploying...
[0%] start: Publishing ****************:current
[100%] success: Published ****************:current
SlackChatBotStack: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (16/16)

 ✅  SlackChatBotStack

Outputs:
SlackChatBotStack.apigatewayEndpoint**************** = https://****************.execute-api.ap-northeast-1.amazonaws.com/prod/

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:****************:stack/SlackChatBotStack/****************
```

※--require-approval neverは「Yes/No」の選択をスキップするオプション。

![スクリーンショット 2021-08-15 8.17.39_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/7846b441-831d-d09d-1ec7-852056ab5382.jpeg)

無事、Lambda関数が作成されているはずです。

![スクリーンショット 2021-08-15 8.20.28_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/b917b861-0944-a34c-be27-2e1798a6a9ce.jpeg)

あとは「設定」→「トリガー」からAPI Gatewayのエンドポイントをコピーし、

![スクリーンショット 2021-08-15 8.23.00_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/7f51968a-8eeb-1249-99b7-1926bcd0a0e9.jpeg)

![スクリーンショット 2021-08-15 8.23.50_censored (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/87372236-ac12-1ae0-7617-b7214672d57b.jpeg)


先ほどngrokで代用していた箇所に当てはめてください。

![タイトルなし (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/145a90fd-4dba-5c60-d2f8-864c508eccb3.gif)

特に問題が無ければこんな感じで動くはず。もしエラーになった場合、

![スクリーンショット 2021-08-15 8.30.29_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/9c7a8b4c-33bb-410c-8f62-72119928fdaf.jpeg)

CloudWatchにログが吐かれているはずなのでそちらを確認して適宜デバッグしてください。

### CI/CD環境を構築（GitHub Actions）

ここから先はお好みです。

今回作ったSlackアプリを今後も更新し続けていく場合、毎回手動でデプロイするのはさすがに面倒なので、GitHubにプッシュすれば後は自動でデプロイしてくれるようにしておきたいところ。

```
$ mkdir .github .github/workflows
$ touch .github/workflows/build-deploy.yml
```

```yml:./.github/workflows/build-deploy.yml
name: Buld and Deploy

on: push # GitHubへのプッシュをトリガーに

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Set Node.js 14.x
        uses: actions/setup-node@v1
        with:
          node-version: 14.x
      - name: Setup Dependencies
        run: npm ci
      - name: Build
        run: npm run build
  deploy:
    runs-on: ubuntu-latest
    needs: build # buildが成功した場合のみ実行
    if: "contains(github.ref, 'master')" # masterブランチの場合のみ実行
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Setup AWS CDK
        run: npm install -g aws-cdk@1.104.0
      - name: Setup Dependencies
        run: npm ci
      - name: Deploy
        run: cdk deploy --require-approval never
        env:
          AWS_DEFAULT_REGION: ap-northeast-1
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
```

これで全ブランチにpushした際にビルドチェックが入り、masterブランチに限りデプロイまで走るようになります。

より本格的にしたい場合、各種Linterなどを入れてソースコードの品質チェックを行うなどが考えられそうですが、今回はあくまでサンプル的な内容なので省略させていただきました。

![スクリーンショット 2021-08-15 8.48.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/4a850235-aa5b-80ef-eb3a-1bec29f1723f.png)

なお、「./.github/workflows/build-deploy.yml」ファイル内で読み込む環境変数

- AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY

は事前にGitHubリポジトリの「settings」→「secret」から登録しておきましょう。

![スクリーンショット 2021-08-15 8.55.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/931fb356-87ae-0faf-9de7-8586e42a3314.png)

あとはmasterブランチに適当に変更を加えてみればこんな感じでワークフローが回り始まるので、無事デプロイが完了するまで祈っていてください。

![スクリーンショット 2021-08-15 8.57.05_censored (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/5e88d899-7bb8-9a09-c55d-708d89fbcfb6.jpeg)

デプロイ完了後、Lambda関数の「最終更新」を見てみると直前の時間が表示されているので上手くいってそうですね！

## あとがき

以上、Bolt + TypeScript で作ったSlackアプリを AWS CDK + GitHub Actions で管理できるようにしてみました。Boltを使えばかなり簡単に実装できたので、ぜひとも試してみてください。

個人的にSlackは日々の業務でかなり高頻度に使うので、色々なSlackアプリを作って業務効率化を図っていきたいです。

https://github.com/kazama1209/slack-caht-bot