GCP CloudRunにSlack Bolt(TypeScriptで)を導入してSlackBotを作成する

皆さん[CloudRun](https://cloud.google.com/run)を使っていますか？
素早くDocker環境に秒単位でデプロイができるこのサービスに私はメロメロです。

で本日は[CloudRun](https://cloud.google.com/run)を使って[SlackBot (Bolt)](https://slack.dev/bolt-js/ja-jp/tutorial/getting-started)を導入して簡単な[スラッシュコマンド](https://slack.com/intl/ja-jp/help/articles/201259356-%E3%83%93%E3%83%AB%E3%83%88%E3%82%A4%E3%83%B3%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%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B)を作成してみたい。と思います。

完成品は ``` /ping ``` コマンドを発行したらbotから ``` pong ``` が返ってくるものを作成します

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5564/bcabd653-126a-41dc-17fd-37464cfe6d59.png)


# 事前に必要なもの

* GCPのアカウント及び下記サービスのPermissionが必要になります
  * [Cloud Run](https://cloud.google.com/run)
  * [Cloud Build](https://cloud.google.com/cloud-build)
  * [Container Registry](https://cloud.google.com/container-registry)
* Slack Appsを作成できるPermission

# 使うもの

* [Slack Bolt](https://slack.dev/bolt-js/ja-jp/tutorial/getting-started)

# SlackBotにかかる料金についての試算目安

* Cloud Run
* Cloud Build

について説明します。この他にStorageの料金等細かい部分が発生しますが、省略させていただきます。

## CloudRunの料金について

料金はシンプルです。



[公式 GCP CloudRun 料金より](https://cloud.google.com/run/pricing?hl=ja)
課金はメモリ(GB)秒単位で課金されていきますが、無料枠(月あたり)が

> 最初の 180,000 vCPU 秒は無料

ありますので50時間分は無料枠になります。

> GB 秒単位が用いられています。GB 秒とは、たとえば 1 GB のインスタンスを 1 秒間実行することです。これは 256 MB のインスタンスを 4 秒間実行することとも言い換えられます。vCPU 秒 単位にも、同じ原則が当てはまります。


なので例えば256MBのメモリで平均で1秒でレスポンスを返すCloudRunを実行する場合は

```
180000(秒) / (256MB) * 4 / 60(分) / 60(時間) = 12.5(時間)
```

約12.5時間分の無料枠が使えます。
SlackBotでこの位の時間を使い切るのはeventなどで発言をpollingしない限りは難しいとおもいますのでほぼ無料枠で収まると思います。

## Cloud Buildの料金について

CloudRunのコンテナイメージをBuildする時に使用します。
使わなくてもCloudRun自体は使えますが、便利なので是非使って下さい。

デフォルトのマシンタイプ(n1-standard-1 1vCPU, 3.75Memory)ならば
[公式 GCP Cloud Build 料金より](https://cloud.google.com/cloud-build/pricing)
> 1 日あたり最初の 120 ビルド分は無料です。

なので超頻繁にデプロイしない限りは無料でいけそうです。

# SlackBotを作成する手順

ではSlackBotを作成してみましょう。

## Slack Appsを作成する

[Slack Bolt 入門ガイド](https://slack.dev/bolt-js/ja-jp/tutorial/getting-started)から

* アプリを作成する
* トークンとアプリのインストール

迄を済まして下さい。

後で

```
export SLACK_SIGNING_SECRET=<your-signing-secret>
```

と

```
export SLACK_BOT_TOKEN=xoxb-<your-bot-token>
```

を使います。

## Slack Bolt(ローカルを)動かすまで

とりあえずdocker-composeでBoltが動くところまでやります

### Dockerfileを作成する

```docker:Dockerfile
FROM node:10.22.0-stretch

WORKDIR /app
ARG STAGE
ENV STAGE=${STAGE}
COPY package.json /app/package.json
COPY package-lock.json /app/package-lock.json
COPY tsconfig.json /app/tsconfig.json
COPY envs/.env.$STAGE /app/envs/.env.$STAGE
COPY src/ /app/src

RUN mkdir dest/ && npm i && npm run build
CMD ["npm", "start"]
```

続いてdocker-compose.yaml
(注意事項としてCloudRunのデフォルトのポート待受は8080です)

```yaml:docker-compose.yaml
version: '3'
services:
  app:
    container_name: "bot"
    build:
      dockerfile: Dockerfile
      context: ./
      args:
        STAGE: dev
    volumes:
      - ./:/app
    ports:
      - "8080:8080"
    tty: true
    stdin_open: true
```

### NodeJSの設定

pakage.jsonを作成します(TypeScriptで動作させるのでこんな感じで)


```package.json
{
  "name": "{app name}",
  "version": "1.0.0",
  "description": "",
  "main": "dest/index.js",
  "scripts": {
    "start": "node dest/index.js",
    "build": "tsc",
    "build:watch": "tsc --watch"
  },
  .
  .
  "devDependencies": {
    "@types/node": "^13.11.1"
  },
  "dependencies": {
    "@slack/bolt": "^2.1.1",
    "dotenv": "^8.2.0",
    "typescript": "^3.8.3"
  },
  ...
}
```

インストール

```
$ npm i
```

続いてtsconfig.json(お好みがあると思うのでよしなに)

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "ES2018",
    "module": "commonjs",
    "sourceMap": true,
    "outDir": "./dest",
    "strict": true,
    "moduleResolution": "node",
    "noUnusedLocals": true
  },
  "typeRoots": [
    "node_modules/@types"
  ],
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "node_modeles"
  ],
  "compileOnSave": true,
  "types": [
    "src/types/*"
  ],
  "rules": {
    "eofline": true
  }
}
```

### ディレクトリを作成する

```bash:/path/to/dir
$ mkdir dest src envs
```

### SlackBotのSecret情報を[dotenv](https://github.com/motdotla/dotenv)に記載する

```
$ touch envs/.env.dev .gitignore
# secret情報なのでコミットしないためのおまじない
$ echo "envs/.env.dev" >> .gitignore
```

SLACK_SIGNING_SECRETとSLACK_BOT_TOKENはSlack Appページから取得できます。

```env:envs/.env.dev
SLACK_SIGNING_SECRET=<your-signing-secret>
SLACK_BOT_TOKEN=<your-bot-token>
APP_LISTEN_PORT=8080
```

## src/index.tsを書く

```typescript:src/index.ts
import { App } from "@slack/bolt";
import * as dotenv from 'dotenv';
dotenv.config({ path: `envs/.env.${process.env.STAGE}` });

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET
});

(async () => {
  // Start your app
  await app.start(process.env.APP_LISTEN_PORT);

  console.log('⚡️ Bolt app is running!');
  app.command('/ping', async ({ command, ack, say, respond }) => {
    console.log(command);

    // コマンドリクエストを確認
    await ack();
    await say(`pong`);
  });
})();
```

ここまででこんな感じのディレクトリになっていると思います。

```
$ tree -L 2 -I 'node_modules'
.
├── Dockerfile
├── dest
├── docker-compose.yaml
├── envs
│   └── .env.dev
├── package.json
├── src
│   └── index.ts
└── tsconfig.json
```

### Docker Build && Up

下記の様なメッセージが出ればOKです。

```
$ docker-compose build
$ docker-compose up

> node dest/index.js

⚡️ Bolt app is running!
```

## Cloud Runにデプロイする


### GCRにDocker Imageをpushする

Cloud Buildの設定を書いていきます。


``` <your app name> ``` の部分はGCRのリポジトリになりますのでよしなに書き換えて下さい。

```yaml:cloudbuild.yaml
steps:
- name: 'gcr.io/cloud-builders/docker'
  args: [ 'build', '-f', 'Dockerfile',
                   '-t','gcr.io/$PROJECT_ID/vqbot/${_STAGE}',
                   '--build-arg', 'STAGE=${_STAGE}',
                   '.'
        ]
substitutions:
  _STAGE: dev
images:
  - 'gcr.io/$PROJECT_ID/<your app name>/dev'
```

GCRにpush

* ``` <your GCP project> ``` は自分のGCP projectを指定して下さい

```
export STAGE=dev
export PROJECT=<your GCP project>
gcloud builds submit \
  --project="$PROJECT" \
  --config cloudbuild.yaml
```

コンソールからCloud Buildの経過が出力されるので終了するのをお待ち下さい。


### Cloud Runにデプロイ

* ``` <your GCP project> ``` は自分のGCP projectを指定して下さい
* ``` <your service name> ``` は Cloud Runの名前になります
* ``` --allow-unauthenticated ``` はCloud RunのURLが公開されるオプションです

```
export STAGE=dev
export SERVICE_NAME=<your service name>
export PROJECT=<your GCP project>
gcloud run deploy $SERVICE_NAME \
  --project="$PROJECT" \
  --image="gcr.io/$PROJECT/$SERVICE_NAME/$STAGE" \
		--platform=managed \
		--region=asia-northeast1 \
		--allow-unauthenticated
```

GCP Console Cloud Runのページへいく

```
open https://console.cloud.google.com/run?hl=ja&project=$PROJECT
```

こんな感じの結果になります。
デプロイされたURLにアクセスして ``` Cannot GET / ``` が表示されることを確認してください(Slack BotのコマンドはPostで受け付けるのでboltの内部で動いているexrepssがレスポンスを返してくれれば良いです)

<img width="1266" alt="2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5564/29f677f1-c9a2-a514-4960-0e2f84c0a3d9.png">

## SlackAppsでSlashCommandを作成する

これでやっと外部に公開されているURLが生成できたのでSlashCommandの設定ができます。

Slack Apps ページに再びいき ``` Create New Command ``` でコマンドを新規作成します

<img width="916" alt="4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5564/a83a8865-073f-6c80-b86c-05b6e7664530.png">

### Request URLに関する注意点

SlackBoltはデフォルトで ``` https://{ドメイン}/slack/events ``` というpathでSlackからの応答を待ちますので

```
{Cloud RunのURL}/slack/events
```

という形式で入力してください。

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5564/3a0a5fa2-bf43-04d4-df59-74af7a653b20.png)

## Botをチャンネルにinviteする

Botを実行したいチャンネルにinviteしないとレスポンスを返してくれませんのでBotを動作させたいSlack チャンネルで

```
/invite @{botの名前}
```

でinviteして下さい。


## 動作確認

スラッシュコマンドを実際に動かしてみたい。と思います

```
/ping
```

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5564/bcabd653-126a-41dc-17fd-37464cfe6d59.png)

お疲れ様でした。


## 最後に

Boltのチュートリアルですと[glitch](https://glitch.com/)等の紹介方法があるかと思いますが、CloudRunでもかなり高速に動作確認をすることができると思います。
Cloud Functionsに比べて自由度が高いCloud Runで素敵なBot生活をお楽しみいただけたら。と思っています。