TerraformでLambdaにSlackBotをデプロイしてみる

# TerraformでLambdaにSlackBotをデプロイしてみる

## はじめに

みなさんSlackは使っているでしょうか。
私の職場には先輩方の作った便利なBotがたくさんあります。自分でもこういうのを作れるようになりたい！と思い、まずは簡単なBotを作ってみました。
本記事はその際の手順や知見等をまとめたものです。

:::note info
Terraformを使う都合、ソースを全部載せるとみづらくなってしまうので、重要なところだけ載せてます。
公開はしてるので↓で全部見られます！
https://github.com/ymat19/SlackHelloWorldBot
:::

## アプリ概要

botにメンションすると、メンション付きで`Hello`が返ってくる。
```
こんな感じ

Hoge: @Bot
Bot:  Hello @Hoge!
```

## 構築手順
アプリの構築手順です。ブラウザ上でのSlackの設定編集と、terraformのデプロイ作業があります。

### Slack側の設定

#### アプリの追加
まず、Slackアプリの設定画面を開きます。
https://api.slack.com/apps/
<br />

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3489175/cdc14f16-b628-452b-9026-3e0911fc1a95.png)
Create New Appを選択
<br />
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3489175/81ed6b48-fa92-493a-0fa2-53e579b8a0dd.png)
From Scratchを選択
<br />
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3489175/cdfcee9d-68c4-54cc-cc34-3c6f9704220f.png)
アプリ名と導入先のワークスペースを選択
<br />
#### トークン発行
Botを動かすには以下の二つのトークンが必要です。その取得手順です。
- Signing Secret
    - BotがSlackのリクエストを検証するためのもの
- Bot User OAuth Token
    - SlackがBotのリクエストを検証するためのもの
<br />

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3489175/5fd6c224-d1c8-1309-8eb6-e9eb5ed72db8.png)
アプリ作成が成功するとこんな感じの画面になるので、Signing Secretを控えておいてください。
<br />
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3489175/5cf32a81-c54e-f058-e9bf-e4a7e20a5e5f.png)
サイドバーからOAuth & Permissionsを選択して、少し下にスクロールすると、Bot Token Scopesというのが表示されます。
以下の三つを追加します
- app_mentions:read
    - 今回はメンションで発火するので必要
- chat:write
    - HelloWorldを書き込みたいので必要
- users:read
    - メンション元のユーザに挨拶を返したいので必要(これがないとユーザ名を取れないっぽい)
<br />

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3489175/afd27c08-85da-1127-db77-6cac3c127a4a.png)
先の手順で、権限が付与出来たらInstallボタンを押します。
<br />
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3489175/8cc17dc6-8730-d48a-41ba-39b5cfe9d191.png)
Bot User OAuth Tokenが発行されるので、これも控えておきます。
<br />
### terraformでデプロイ

#### セットアップ

terraformディレクトリに諸々置いてます。
https://github.com/ymat19/SlackHelloWorldBot/tree/main/terraform

まず`terraform/terraform.tfvars.example` を参考にvariablesへ、先ほど取得したトークン2種とterraformの実行ロールを設定します。

```hcl
deploy_role_arn = "arn:aws:iam::..."
slack_bot_token = "xoxb-xxxxxxxxxxxxx..."
slack_signing_secret = "xxx..."
```

#### デプロイ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3489175/5b0fec03-fb67-7680-3042-0c22f1aef629.png)
`terraform apply`でデプロイするとこんな感じにエンドポイントがoutputされるので、控えておきます。
<br />
### tfファイルの解説

<details><summary>lambda関連</summary>

```hcl
# create lambda execution role
resource "aws_iam_role" "slack_bot" {
  name = "slack-lambda-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Principal = {
          Service = "lambda.amazonaws.com"
        },
        Action = "sts:AssumeRole"
      }
    ]
  })
}

resource "aws_iam_role_policy" "slack_bot" {
  name = "slack-lambda_policy"
  role = aws_iam_role.slack_bot.id
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents",
        ],
        Resource = "*"
      }
    ]
  })
}

# null_resource to update pip package
resource "null_resource" "slack_bot" {
  triggers = {
    always_run = timestamp()
  }
  provisioner "local-exec" {
    command = "pip install -r ../src/requirements.txt -t ../src/package && cp ../src/lambda_function.py ../src/package"
  }
}

# lambda source data
data "archive_file" "slack_bot" {
  depends_on  = [null_resource.slack_bot]
  type        = "zip"
  source_dir  = "../src/package"
  output_path = "lambda.zip"
}

# create python lambda function
resource "aws_lambda_function" "slack_bot" {
  depends_on       = [data.archive_file.slack_bot]
  function_name    = "slack-bot"
  role             = aws_iam_role.slack_bot.arn
  handler          = "lambda_function.lambda_handler"
  runtime          = "python3.12"
  filename         = data.archive_file.slack_bot.output_path
  source_code_hash = data.archive_file.slack_bot.output_base64sha256
  timeout          = 15
  architectures    = ["arm64"]
  environment {
    variables = {
      SLACK_BOT_TOKEN      = var.slack_bot_token
      SLACK_SIGNING_SECRET = var.slack_signing_secret
    }
  }
}

```
</details>

Lambdaの実行ロール作ったりpythonのコードをアップロードしたりしてます。
SlackBot特有の話があるとしたら、下でトークンを環境変数にセットしてるぐらいです。

``` hcl
    variables = {
      SLACK_BOT_TOKEN      = var.slack_bot_token
      SLACK_SIGNING_SECRET = var.slack_signing_secret
    }
```

<details><summary>API Gateway関連</summary>

```hcl
resource "aws_apigatewayv2_api" "slack_bot" {
  name          = "slack-bot-api"
  protocol_type = "HTTP"
}

resource "aws_lambda_permission" "slack_bot" {
  statement_id  = "AllowAPIGatewayInvoke"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.slack_bot.arn
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_apigatewayv2_api.slack_bot.execution_arn}/*/*"
}

resource "aws_apigatewayv2_integration" "slack_bot" {
  api_id                 = aws_apigatewayv2_api.slack_bot.id
  integration_type       = "AWS_PROXY"
  integration_uri        = aws_lambda_function.slack_bot.invoke_arn
  integration_method     = "POST"
  payload_format_version = "2.0"
}

resource "aws_apigatewayv2_route" "slack_bot" {
  api_id    = aws_apigatewayv2_api.slack_bot.id
  route_key = "POST ${var.end_point_path}"
  target    = "integrations/${aws_apigatewayv2_integration.slack_bot.id}"
}

resource "aws_apigatewayv2_stage" "slack_bot" {
  api_id      = aws_apigatewayv2_api.slack_bot.id
  name        = "$default"
  auto_deploy = true
  default_route_settings {
    throttling_rate_limit  = 1
    throttling_burst_limit = 10
  }
}
```
</details>

SlackがAPI Gatewayを叩けるように公開しています。
SlackがAPI叩く時のグローバルIPの範囲が公開されていたら絞り込み等した方が良い気もしますが、調べた感じなさそうでした。
代わりではないですがDDoS対策でRate制限は厳しめにしてます。
```hcl
  default_route_settings {
    throttling_rate_limit  = 1
    throttling_burst_limit = 10
  }
```

### Slackにイベント設定

再びSlack側に戻って、イベントを設定します。
<br />
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3489175/8c4fc232-e32c-c74e-4c32-2dc9366bea4c.png)
サイドバーからEvent SubScriptionに移って、イベントの通知先と、通知するイベントを設定します。
- Request URLには先ほどterraform applyで出力されたエンドポイントを入力
    - APIがちゃんと動いてたらVerifiedになるはず
- Subscrie to bot eventsにapp_mentionを登録
    - 今回はメンションに反応したいだけなのでこれのみです

埋まったら保存します。Save Changesの位置が見落としやすいので注意。
<br />

## 使ってみる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3489175/331e3532-84dd-07bd-0a20-3bcf4888ed4e.png)
こんな感じに挨拶が返ってきたら成功です！
<br />
## 補足バックエンドの話

構築手順で触れる機会がありませんでしたが、バックエンドはこんな感じです。
<details><summary>バックエンド（Python）</summary>

```python
from slack_bolt.adapter.aws_lambda import SlackRequestHandler
from slack_bolt import App

# Boltアプリのインスタンスを作成
app = App(process_before_response=True)


# メンションされたときのイベントハンドラ
@app.event("app_mention")
def handle_app_mention(event, say):
    user = event["user"]
    say(f"Hello, <@{user}>!")


# Lambdaのハンドラを作成
def lambda_handler(event, context):
    slack_handler = SlackRequestHandler(app)
    return slack_handler.handle(event, context)
```
</details>

ここのオプションがめちゃくちゃ重要で、FaaSで動かすなら必須です。　
https://tools.slack.dev/bolt-python/ja-jp/concepts/lazy-listeners/
```python
app = App(process_before_response=True)
```

これがないとBOTが動いたり動かなかったりします（多分Lambdaのインスタンスがウォームスタートな時しか成功しない）。
生成AIに書いてもらったソースにこれが抜けていて結構な時間を失いました。まずドキュメント読むの大事:innocent:。
ちなみに、トークン周りは既定の環境変数( `SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET`)が勝手に読まれてるっぽいです。

## 最後に

ここまで読んでいただきありがとうございました！
とりあえずSlack起点でLambdaを叩ける仕組みができたので、何かしら作って遊んでみたいです。
SlackアプリのフレームワークであるBoltはかなり便利そうで、コマンド対応やGUI追加も割と簡単にできそうで良い感じです。
なんと日本語のチュートリアルもあります。
https://tools.slack.dev/bolt-python/ja-jp/getting-started

## 参考
[【全手順を網羅】AWSを利用したSlackアプリの作り方](https://www.pci-sol.com/business/service/product/blog/develop-aws-slack-app/)
タイトル通り網羅的でLambda環境でのBot作成手順を理解することができました。手順だけでなく成果物のBot自体も参考にさせていただいてます。
[Amazon BedrockとSlackで生成AIチャットボットアプリを作る (その2：Lambda＋API Gatewayで動かす)](https://dev.classmethod.jp/articles/amazon-bedrock-slack-chat-bot-part2/)
こちらの記事のおかげで`process_before_response`に気づきました。ありがとうございます。