Skip to content
Published on

Slack Bot開発完全ガイド — Bolt SDKで作るAI業務自動化ボット

Authors
  • Name
    Twitter
Slack Bot Bolt SDK

はじめに

Slackは開発チームの中核コミュニケーションプラットフォームです。よくできたSlack Botは、障害アラート、デプロイ管理、コードレビューリマインダー、AI問い合わせなど、さまざまな業務を自動化します。

Bolt for PythonはSlack公式フレームワークで、簡潔なAPIでさまざまなSlack機能を活用できます。

Slackアプリの設定

1. アプリの作成

1. https://api.slack.com/apps にアクセス
2. "Create New App""From scratch"
3. App Name: "DevOps Bot"
4. Workspaceを選択

2. Bot Token Scopesの設定

OAuth & PermissionsBot Token Scopes:
- chat:write          (メッセージ送信)
- commands            (スラッシュコマンド)
- im:history          (DM履歴読み取り)
- im:read             (DMチャンネル情報)
- channels:history    (チャンネルメッセージ読み取り)
- channels:read       (チャンネル情報)
- users:read          (ユーザー情報)
- reactions:write     (リアクション追加)
- files:write         (ファイルアップロード)

3. Event Subscriptions

Event SubscriptionsEnable Events
Subscribe to bot events:
- message.im          (DM受信)
- message.channels    (チャンネルメッセージ)
- app_mention         (メンション)
- app_home_opened     (アプリホームタブを開く)

プロジェクト設定

# 依存関係のインストール
pip install slack-bolt slack-sdk python-dotenv openai

# プロジェクト構造
slack-bot/
├── app.py              # メインアプリ
├── handlers/
│   ├── __init__.py
│   ├── commands.py     # スラッシュコマンド
│   ├── events.py       # イベントハンドラー
│   ├── actions.py      # ボタン/アクションハンドラー
│   └── modals.py       # モーダルビュー
├── services/
│   ├── __init__.py
│   ├── ai.py           # LLM統合
│   ├── deploy.py       # デプロイサービス
│   └── github.py       # GitHub連携
├── .env
└── requirements.txt
# .env
SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_APP_TOKEN=xapp-your-app-token
SLACK_SIGNING_SECRET=your-signing-secret
OPENAI_API_KEY=sk-your-key

基本アプリ構成

# app.py
import os
from dotenv import load_dotenv
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler

load_dotenv()

# Socket Modeでアプリ作成(Webhookサーバー不要)
app = App(token=os.environ["SLACK_BOT_TOKEN"])


# ===== イベントハンドラー =====

# メンションに応答
@app.event("app_mention")
def handle_mention(event, say, client):
    user = event["user"]
    text = event["text"]

    # ユーザー情報を取得
    user_info = client.users_info(user=user)
    name = user_info["user"]["real_name"]

    say(f"こんにちは{name}さん!何かお手伝いしましょうか? 🤖")


# DMメッセージの処理
@app.event("message")
def handle_dm(event, say, client, logger):
    # ボット自身のメッセージは無視
    if event.get("bot_id"):
        return

    channel_type = event.get("channel_type")
    if channel_type == "im":
        text = event.get("text", "")
        say(f"お伝えいただいた内容を処理いたします: '{text}'")


# ===== スラッシュコマンド =====

@app.command("/deploy")
def handle_deploy_command(ack, body, client):
    ack()  # 3秒以内の応答が必須!

    # モーダルを開く
    client.views_open(
        trigger_id=body["trigger_id"],
        view={
            "type": "modal",
            "callback_id": "deploy_modal",
            "title": {"type": "plain_text", "text": "デプロイ管理"},
            "submit": {"type": "plain_text", "text": "デプロイ開始"},
            "blocks": [
                {
                    "type": "input",
                    "block_id": "service_block",
                    "element": {
                        "type": "static_select",
                        "placeholder": {"type": "plain_text", "text": "サービスを選択"},
                        "options": [
                            {"text": {"type": "plain_text", "text": "API Server"}, "value": "api"},
                            {"text": {"type": "plain_text", "text": "Web Frontend"}, "value": "web"},
                            {"text": {"type": "plain_text", "text": "Worker"}, "value": "worker"},
                        ],
                        "action_id": "service_select",
                    },
                    "label": {"type": "plain_text", "text": "サービス"},
                },
                {
                    "type": "input",
                    "block_id": "env_block",
                    "element": {
                        "type": "static_select",
                        "options": [
                            {"text": {"type": "plain_text", "text": "Staging"}, "value": "staging"},
                            {"text": {"type": "plain_text", "text": "Production"}, "value": "production"},
                        ],
                        "action_id": "env_select",
                    },
                    "label": {"type": "plain_text", "text": "環境"},
                },
                {
                    "type": "input",
                    "block_id": "version_block",
                    "element": {
                        "type": "plain_text_input",
                        "placeholder": {"type": "plain_text", "text": "v1.2.3 または latest"},
                        "action_id": "version_input",
                    },
                    "label": {"type": "plain_text", "text": "バージョン"},
                },
            ],
        },
    )


# モーダル送信の処理
@app.view("deploy_modal")
def handle_deploy_submission(ack, body, client, view):
    ack()

    values = view["state"]["values"]
    service = values["service_block"]["service_select"]["selected_option"]["value"]
    env = values["env_block"]["env_select"]["selected_option"]["value"]
    version = values["version_block"]["version_input"]["value"]
    user_id = body["user"]["id"]

    # デプロイ通知を送信
    client.chat_postMessage(
        channel="#deployments",
        blocks=[
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": f"🚀 *デプロイ開始*\n"
                           f"• サービス: `{service}`\n"
                           f"• 環境: `{env}`\n"
                           f"• バージョン: `{version}`\n"
                           f"• 依頼者: <@{user_id}>"
                }
            },
            {
                "type": "actions",
                "elements": [
                    {
                        "type": "button",
                        "text": {"type": "plain_text", "text": "✅ 承認"},
                        "style": "primary",
                        "value": f"{service}|{env}|{version}",
                        "action_id": "deploy_approve",
                    },
                    {
                        "type": "button",
                        "text": {"type": "plain_text", "text": "❌ 拒否"},
                        "style": "danger",
                        "value": f"{service}|{env}|{version}",
                        "action_id": "deploy_reject",
                    },
                ],
            },
        ],
    )


# ボタンアクションの処理
@app.action("deploy_approve")
def handle_approve(ack, body, client, action):
    ack()
    service, env, version = action["value"].split("|")
    user_id = body["user"]["id"]

    # 元のメッセージを更新
    client.chat_update(
        channel=body["channel"]["id"],
        ts=body["message"]["ts"],
        blocks=[
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": f"✅ *デプロイ承認済み*\n"
                           f"• サービス: `{service}` → `{env}` ({version})\n"
                           f"• 承認者: <@{user_id}>\n"
                           f"• ステータス: デプロイ進行中... ⏳"
                }
            }
        ],
    )

    # 実際のデプロイロジックを実行
    # deploy_service(service, env, version)


# ===== ステータス照会コマンド =====

@app.command("/status")
def handle_status(ack, say, command):
    ack()

    say(
        blocks=[
            {
                "type": "header",
                "text": {"type": "plain_text", "text": "📊 サービスステータス"}
            },
            {
                "type": "section",
                "fields": [
                    {"type": "mrkdwn", "text": "*API Server*\n🟢 Healthy (v1.2.3)"},
                    {"type": "mrkdwn", "text": "*Web Frontend*\n🟢 Healthy (v2.0.1)"},
                    {"type": "mrkdwn", "text": "*Worker*\n🟡 Degraded (v1.1.0)"},
                    {"type": "mrkdwn", "text": "*Database*\n🟢 Healthy (CPU: 45%)"},
                ]
            },
            {"type": "divider"},
            {
                "type": "context",
                "elements": [
                    {"type": "mrkdwn", "text": f"最終更新: <!date^{int(__import__('time').time())}^{{date_short_pretty}} {{time}}|just now>"}
                ]
            }
        ]
    )


# ===== AIチャットボット機能 =====

@app.command("/ask")
def handle_ask(ack, say, command):
    ack()

    question = command["text"]
    if not question:
        say("質問を入力してください。例: `/ask Kubernetes PodがCrashLoopBackOffの時の対処法`")
        return

    # ローディング表示
    say(f"🤔 '{question}'について考えています...")

    # LLM呼び出し
    from openai import OpenAI
    client_ai = OpenAI()

    response = client_ai.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": "You are a helpful DevOps assistant. Answer in Korean. Be concise."},
            {"role": "user", "content": question}
        ],
        max_tokens=1000,
    )

    answer = response.choices[0].message.content

    say(
        blocks=[
            {
                "type": "section",
                "text": {"type": "mrkdwn", "text": f"*Q: {question}*"}
            },
            {"type": "divider"},
            {
                "type": "section",
                "text": {"type": "mrkdwn", "text": answer}
            },
            {
                "type": "context",
                "elements": [
                    {"type": "mrkdwn", "text": "💡 AI生成回答 — 正確性をご確認ください"}
                ]
            }
        ]
    )


# ===== アプリホームタブ =====

@app.event("app_home_opened")
def update_home_tab(client, event):
    client.views_publish(
        user_id=event["user"],
        view={
            "type": "home",
            "blocks": [
                {
                    "type": "header",
                    "text": {"type": "plain_text", "text": "🤖 DevOps Bot Dashboard"}
                },
                {
                    "type": "section",
                    "text": {
                        "type": "mrkdwn",
                        "text": "*利用可能なコマンド:*\n"
                               "• `/deploy` — サービスデプロイ\n"
                               "• `/status` — サービスステータス確認\n"
                               "• `/ask [質問]` — AIに質問\n"
                               "• `/incident` — 障害報告"
                    }
                },
                {"type": "divider"},
                {
                    "type": "section",
                    "text": {"type": "mrkdwn", "text": "*最近のデプロイ:*\n✅ API Server v1.2.3 → Production(2時間前)\n✅ Web v2.0.1 → Production(昨日)"}
                },
            ],
        },
    )


# ===== スケジュールメッセージ =====

def send_daily_standup_reminder():
    """毎朝のスタンドアップリマインダー"""
    import time
    app.client.chat_postMessage(
        channel="#engineering",
        text="🌅 *スタンドアップリマインダー*\n今日の計画をスレッドで共有してください!\n1. 昨日やったこと\n2. 今日やること\n3. ブロッカー",
    )


# ===== エラーハンドリング =====

@app.error
def handle_errors(error, body, logger):
    logger.exception(f"Error: {error}")
    logger.info(f"Request body: {body}")


# ===== 実行 =====
if __name__ == "__main__":
    # Socket Mode(Webhookサーバー不要、ファイアウォール内でも動作)
    handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
    handler.start()

障害管理ボット

@app.command("/incident")
def handle_incident(ack, body, client):
    ack()
    client.views_open(
        trigger_id=body["trigger_id"],
        view={
            "type": "modal",
            "callback_id": "incident_modal",
            "title": {"type": "plain_text", "text": "🚨 障害報告"},
            "submit": {"type": "plain_text", "text": "報告"},
            "blocks": [
                {
                    "type": "input",
                    "block_id": "severity",
                    "element": {
                        "type": "static_select",
                        "options": [
                            {"text": {"type": "plain_text", "text": "🔴 P1 - Critical"}, "value": "P1"},
                            {"text": {"type": "plain_text", "text": "🟠 P2 - Major"}, "value": "P2"},
                            {"text": {"type": "plain_text", "text": "🟡 P3 - Minor"}, "value": "P3"},
                        ],
                        "action_id": "severity_select",
                    },
                    "label": {"type": "plain_text", "text": "深刻度"},
                },
                {
                    "type": "input",
                    "block_id": "description",
                    "element": {
                        "type": "plain_text_input",
                        "multiline": True,
                        "placeholder": {"type": "plain_text", "text": "障害状況を説明してください"},
                        "action_id": "desc_input",
                    },
                    "label": {"type": "plain_text", "text": "説明"},
                },
            ],
        },
    )


@app.view("incident_modal")
def handle_incident_submit(ack, body, client, view):
    ack()

    values = view["state"]["values"]
    severity = values["severity"]["severity_select"]["selected_option"]["value"]
    description = values["description"]["desc_input"]["value"]
    reporter = body["user"]["id"]

    # 障害チャンネルを作成
    import time
    channel_name = f"incident-{int(time.time())}"

    channel = client.conversations_create(
        name=channel_name,
        is_private=False,
    )
    channel_id = channel["channel"]["id"]

    # 障害の初期メッセージ
    client.chat_postMessage(
        channel=channel_id,
        blocks=[
            {
                "type": "header",
                "text": {"type": "plain_text", "text": f"🚨 障害報告 ({severity})"}
            },
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": f"*報告者:* <@{reporter}>\n"
                           f"*深刻度:* {severity}\n"
                           f"*説明:*\n{description}"
                }
            },
            {"type": "divider"},
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": "⏱ *タイムラインをこのスレッドに記録してください*"
                }
            },
        ],
    )

    # 通知チャンネルにアナウンス
    client.chat_postMessage(
        channel="#incidents",
        text=f"🚨 新規障害発生: {severity}\n📝 {description[:100]}...\n🔗 <#{channel_id}>",
    )

Dockerデプロイ

FROM python:3.12-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["python", "app.py"]
# docker-compose.yml
version: '3.8'
services:
  slack-bot:
    build: .
    env_file: .env
    restart: unless-stopped
    healthcheck:
      test: ['CMD', 'python', '-c', "import requests; requests.get('http://localhost:3000/health')"]
      interval: 30s
      timeout: 10s
      retries: 3

クイズ

Q1. Slack Boltでスラッシュコマンドハンドラーが最初にやるべきことは?

**ack()**の呼び出しです。Slackは3秒以内に応答(acknowledge)を受け取れないとタイムアウトエラーを表示します。

Q2. Socket Modeの利点は?

WebSocket接続を使用するため、公開URLやWebhookサーバーが不要です。ファイアウォール内でも、ローカル開発でもすぐに動作します。

Q3. Block Kitの主要なブロックタイプ3つは?

section(テキスト/フィールド)、actions(ボタン/セレクトなどのインタラクティブ要素)、input(モーダルでのユーザー入力)です。

Q4. @app.actionと@app.viewの違いは?

@app.actionはボタンクリックなどのインタラクションを、@app.viewはモーダルの送信(submit)を処理します。それぞれaction_idとcallback_idでマッチングされます。

Q5. Bot Token(xoxb-)とApp Token(xapp-)の違いは?

Bot Tokenはチャンネルメッセージ送信などのSlack API呼び出しに使用し、App TokenはSocket ModeのWebSocket接続に使用します。

Q6. 障害発生時に自動でSlackチャンネルを作成する理由は?

障害関連のコミュニケーションを一か所に集中させ、タイムラインを記録し、事後分析(ポストモーテム)のための履歴を残すためです。

Q7. Slack BotにLLMを統合する際の注意点は?

LLMの応答時間が3秒を超える可能性があるため、まずack()で応答した後、非同期でLLMを呼び出し、結果を別メッセージで送信する必要があります。

まとめ

Slack Botはチームの生産性を大きく向上させるツールです。Bolt SDKを使えば、スラッシュコマンド、モーダル、ボタンインタラクションなどを簡潔なPythonコードで実装でき、Socket Modeで別途サーバーなしですぐにデプロイできます。

参考資料