TerraformでLambdaにSlackBotをデプロイしてみる
はじめに
みなさんSlackは使っているでしょうか。
私の職場には先輩方の作った便利なBotがたくさんあります。自分でもこういうのを作れるようになりたい!と思い、まずは簡単なBotを作ってみました。
本記事はその際の手順や知見等をまとめたものです。
Terraformを使う都合、ソースを全部載せるとみづらくなってしまうので、重要なところだけ載せてます。
公開はしてるので↓で全部見られます!
https://github.com/ymat19/SlackHelloWorldBot
アプリ概要
botにメンションすると、メンション付きでHello
が返ってくる。
こんな感じ
Hoge: @Bot
Bot: Hello @Hoge!
構築手順
アプリの構築手順です。ブラウザ上でのSlackの設定編集と、terraformのデプロイ作業があります。
Slack側の設定
アプリの追加
まず、Slackアプリの設定画面を開きます。
https://api.slack.com/apps/
Create New Appを選択
From Scratchを選択
アプリ名と導入先のワークスペースを選択
トークン発行
Botを動かすには以下の二つのトークンが必要です。その取得手順です。
- Signing Secret
- BotがSlackのリクエストを検証するためのもの
- Bot User OAuth Token
- SlackがBotのリクエストを検証するためのもの
アプリ作成が成功するとこんな感じの画面になるので、Signing Secretを控えておいてください。
サイドバーからOAuth & Permissionsを選択して、少し下にスクロールすると、Bot Token Scopesというのが表示されます。
以下の三つを追加します
- app_mentions:read
- 今回はメンションで発火するので必要
- chat:write
- HelloWorldを書き込みたいので必要
- users:read
- メンション元のユーザに挨拶を返したいので必要(これがないとユーザ名を取れないっぽい)
先の手順で、権限が付与出来たらInstallボタンを押します。
Bot User OAuth Tokenが発行されるので、これも控えておきます。
terraformでデプロイ
セットアップ
terraformディレクトリに諸々置いてます。
https://github.com/ymat19/SlackHelloWorldBot/tree/main/terraform
まずterraform/terraform.tfvars.example
を参考にvariablesへ、先ほど取得したトークン2種とterraformの実行ロールを設定します。
deploy_role_arn = "arn:aws:iam::..."
slack_bot_token = "xoxb-xxxxxxxxxxxxx..."
slack_signing_secret = "xxx..."
デプロイ
terraform apply
でデプロイするとこんな感じにエンドポイントがoutputされるので、控えておきます。
tfファイルの解説
lambda関連
# 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
}
}
}
Lambdaの実行ロール作ったりpythonのコードをアップロードしたりしてます。
SlackBot特有の話があるとしたら、下でトークンを環境変数にセットしてるぐらいです。
variables = {
SLACK_BOT_TOKEN = var.slack_bot_token
SLACK_SIGNING_SECRET = var.slack_signing_secret
}
API Gateway関連
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
}
}
SlackがAPI Gatewayを叩けるように公開しています。
SlackがAPI叩く時のグローバルIPの範囲が公開されていたら絞り込み等した方が良い気もしますが、調べた感じなさそうでした。
代わりではないですがDDoS対策でRate制限は厳しめにしてます。
default_route_settings {
throttling_rate_limit = 1
throttling_burst_limit = 10
}
Slackにイベント設定
再びSlack側に戻って、イベントを設定します。
サイドバーからEvent SubScriptionに移って、イベントの通知先と、通知するイベントを設定します。
- Request URLには先ほどterraform applyで出力されたエンドポイントを入力
- APIがちゃんと動いてたらVerifiedになるはず
- Subscrie to bot eventsにapp_mentionを登録
- 今回はメンションに反応したいだけなのでこれのみです
埋まったら保存します。Save Changesの位置が見落としやすいので注意。
使ってみる
補足バックエンドの話
構築手順で触れる機会がありませんでしたが、バックエンドはこんな感じです。
バックエンド(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)
ここのオプションがめちゃくちゃ重要で、FaaSで動かすなら必須です。
https://tools.slack.dev/bolt-python/ja-jp/concepts/lazy-listeners/
app = App(process_before_response=True)
これがないとBOTが動いたり動かなかったりします(多分Lambdaのインスタンスがウォームスタートな時しか成功しない)。
生成AIに書いてもらったソースにこれが抜けていて結構な時間を失いました。まずドキュメント読むの大事。
ちなみに、トークン周りは既定の環境変数( SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET
)が勝手に読まれてるっぽいです。
最後に
ここまで読んでいただきありがとうございました!
とりあえずSlack起点でLambdaを叩ける仕組みができたので、何かしら作って遊んでみたいです。
SlackアプリのフレームワークであるBoltはかなり便利そうで、コマンド対応やGUI追加も割と簡単にできそうで良い感じです。
なんと日本語のチュートリアルもあります。
https://tools.slack.dev/bolt-python/ja-jp/getting-started
参考
【全手順を網羅】AWSを利用したSlackアプリの作り方
タイトル通り網羅的でLambda環境でのBot作成手順を理解することができました。手順だけでなく成果物のBot自体も参考にさせていただいてます。
Amazon BedrockとSlackで生成AIチャットボットアプリを作る (その2:Lambda+API Gatewayで動かす)
こちらの記事のおかげでprocess_before_response
に気づきました。ありがとうございます。