アラートとワークフローのための Slack 連携パターン

Slack はワークフロー UI およびアラート配信レイヤーです。

目次

Slack の統合は、1 つの HTTP コールでメッセージを送信できるため、欺瞞的に簡単に見えるかもしれません。 しかし、Slack を対話的で信頼性の高いものにする必要が出てきた時が、本物の面白い部分です。

Slack Integration Alerts

この深掘り記事では、Slack を 3 つの異なる統合の表面(サーフェス)として扱います:

  • 入力 Webhook を介した一方通行のアラート用の通知シンク。
  • Workflow Builder とカスタムワークフローステップを介したワークフローエンジン。
  • Block Kit ボタン、スラッシュコマンド、アクションペイロードを介したイベントインターフェース。

このページでは、システムが境界を越えて共有 UI に入り込み、さらにアーキテクチャ内にイベントを戻して発する仕組みについて説明しますが、アラートの哲学については扱いません。 アラート戦略とルーティングについては、可観測性チームのためのモダンなアラートシステム設計 を参照してください。

関連する読み物:

標準的な枠組みと統合パターンにおける位置づけ

Slack は、単にアラートが死んでいく場所ではありません。適切に使用すれば、Slack はメッセージがステートフルなアーティファクトであり、ユーザーのインタラクションがイベントであるようなシステムインターフェースとなります。

このページは、/app-architecture/integration-patterns/slack/ 下に標準的に配置されています。なぜなら、主な問いは「アラートを送るべきか」ではなく、「システムと Slack の間の契約(コントラクト)は何か」だからです。

以下のいずれかを必要とするソリューションの場合、あなたは単なる通知の領域ではなく、統合パターンの領域にいることを意味します:

  • 人間の承認がアクションをゲートする意思決定ループ。
  • Slack がコンテキストを収集し、ステップをトリガーするワークフロー。
  • Slack がシステムが購読するアクションを発するイベントループ。

Slack プラットフォームは、意図的に、リクエスト URL とインタラクションペイロードを通じて、一方通行のメッセージングと双方向のインタラクションの両方をサポートしています。入力 Webhook は、チャンネルに Block Kit のビルディングブロックを含む JSON ペイロードを投稿するためのファーストクラスの方法です(入力 Webhook を使用したメッセージ送信)。インタラクティブ性は、設定されたリクエスト URL への HTTP POST リクエストとしてアプリに送り返され、そのペイロードは JSON ペイロードフィールドを持つ form 形式でエンコードされています(ユーザーインタラクションの処理)。

通知シンクとしての Slack

入力 Webhook は、アラートとステータス更新の価値に最も早く到達するためのパスです。Webhook はアプリインストールに紐付いた一意の URL であり、そこに JSON メッセージを POST します(入力 Webhook を使用したメッセージ送信)。

意見のある見解:Webhook は、「送信して忘れる」メッセージを必要とし、Slack をコントロールサーフェスとして必要としない場合に、優れたデフォルトです。また、Webhook は、オンボーディングを最終的なアプリアーキテクチャから切り離す優れた方法でもあります。

ワークフローエンジンとしての Slack

Workflow Builder が存在するのは、チャットが実際に作業が行われる場所だからです。ワークフローは単純でも複雑でもあり、アプリに接続できます(Workflow Builder のガイド)。

カスタムワークフローステップにより、システムを Workflow Builder 内の再利用可能なビルディングブロックとして公開できます(ワークフローステップ)。これは、チャンネル内のボットとは異なる統合の形態です。これは、「外からのメッセージ」よりも「Slack 内でのツール」に近い統合へと移行させます。

意見のある見解:もしあなたの組織がすでにワークフローと承認の文脈で考えているなら、ワークフローステップは、独自に作られたボットよりもネイティブに感じられるかもしれません。

イベントインターフェースとしての Slack

Block Kit は、メッセージを UI サーフェスに変えます(Block Kit)。ボタンなどのインタラクティブなコンポーネントは、ユーザーがクリックした際にアプリに送られるアクションペイロード(通常は block_actions ペイロード)を生成します(block_actions ペイロード)。

ボタンには、明示的な識別子 action_id とオプションの value があり、section または actions ブロック内に配置されなければなりません(ボタン要素)。ボタン付きのメッセージを設計するということは、イベントソースを設計していることになります。

ここが、リクエストの検証、必要なスコープ、安全な内部トリガーなどの FAQ トピックが設計の中心となる場所です。

拡張可能なアーキテクチャパターン

一方通行のアラートのための Webhook フロー

[service] -> [alert formatter] -> [slack incoming webhook] -> [channel]

入力 Webhook は JSON ペイロードを受け取り、Block Kit レイアウトをサポートします(入力 Webhook を使用したメッセージ送信)。

FAQ で「アラートを送信する最も速い方法は何か」と問われた場合、通常これが答えです。

信頼性とバックプレッシャーのためのキューを介した仲介フロー

[services] -> [queue topic] -> [slack dispatcher] -> [slack api]
                     |                 |
                     |                 +-> [rate limit handler]
                     +-> [dead letter queue]

Slack のレート制限は、入力 Webhook を含む HTTP ベースの API に適用され、制限を超えると Retry-After ヘッダー付きの HTTP 429 を返します(レート制限)。

意見のある見解:すべてのサービスから直接アラートをポストする場合、最初のインシデントが、自らの Slack 統合に対する分散型サービス拒否(DDoS)に変わります。キューの背後にあるディスパッチャーは、より落ち着いたアーキテクチャになる傾向があります。

承認を伴うワークフロー自動化パターン

[alert] -> [slack message with button] -> [button click]
   -> [action payload] -> [approval handler] -> [internal API] -> [update message]

Slack のインタラクティブ性には、リクエスト URL の設定とインタラクティブ性の有効化が必要です。Slack は、interaction ペイロードを application/x-www-form-urlencoded として送信し、JSON を含むパラメータを持ち、3 秒以内に HTTP 200 で応答する必要があります(ユーザーインタラクションの処理)。

これは、内部アクションを安全にトリガーする FAQ の項目の背後にあるパターンです。

Slack インタラクションフローチャート図

Slack interaction diagram

Webhook とアプリの比較と実装の仕組み

推奨ライブラリ

Go:

Python:

Webhook とアプリボットアプローチの比較

実用的な比較:

機能 入力 Webhook ボットトークン付き Slack アプリ
メッセージ投稿 はい はい
Block Kit レイアウト投稿 はい はい
ボタンクリック受信 インタラクティブ性付きアプリに紐付いている場合のみ はい
スラッシュコマンド いいえ はい
ワークフローステップ いいえ はい
セキュリティサーフェス Webhook URL の秘密保持 OAuth トークンと署名シークレット
最適な用途 一方通行のアラート ワークフロー、承認、インタラクティブ UI

Slack は、入力 Webhook で Block Kit レイアウトを明示的にサポートしています(入力 Webhook を使用したメッセージ送信)。インタラクティブ性はアプリごとに設定され、リクエスト URL に配信されます(ユーザーインタラクションの処理)。

意見のある見解:Webhook は素晴らしい最初のマイルストーンですが、Slack をコントロールサーフェスにしたいと思った瞬間、アプリを構築していることになります。それ以外だと見なすことは避けてください。

スコープと権限

Slack スコープは、アプリが何ができるかを定義します。中央のスコープ参照と個別のスコープページがあります(スコープ参照)。Web API を介したメッセージ送信には、chat:write が標準のスコープです(chat:write スコープ)。

スラッシュコマンドには、通常 commands スコープと設定されたコマンドリクエスト URL が必要です(コマンドはインタラクティブ性ドキュメントの一部であり、各コマンドには独自のリクエスト URL があります)(ユーザーインタラクションの処理)。

FAQ の注記:ボタンペイロードの配信は「スコープ」ではなく、アプリの設定です。アプリは、インタラクティブ性が有効化され、リクエスト URL が設定された場合にペイロードを受信しますが、メッセージ更新の投稿には依然として一般的に chat:write が必要です。

レート制限とリトライ

Slack のレート制限は HTTP 429 を返し、Retry-After に秒数を含み、これは入力 Webhook を含む HTTP ベースの API に適用されます(レート制限)。

実際には:

  • Retry-After を尊重する
  • 一時的な 5xx に対してジッター付きのバックオフを適用する
  • ボリュームが増加した際、ディスパッチャーで Slack 配信を一元化する

冪等性と重複排除

Slack は、インタラクションペイロードの承認を 3 秒以内に期待しており、それ以外の場合はユーザーがエラーを表示し、機能に応じて Slack が配信の再試行を行う可能性があります(ユーザーインタラクションの処理)。Events API の場合、Slack は明示的に x-slack-retry-num などの再試行メタデータヘッダーを提供します(Events API)。

明示的なリトライがなくても、ユーザーのダブルクリックや分散システムの再送信により重複が発生します。ボタンが内部アクションをトリガーする場合、クリックを少なくとも 1 回イベントとして扱い、重複を排除してください。

承認のための実用的な冪等性戦略:

  • 冪等性キー = team_id + channel_id + message_ts + action_id + user_id
  • ワークフローウィンドウに一致する TTL で Redis にキーを保存
  • 内部アクション API も、Slack ハンドラーだけでなく冪等性を強制する

セキュリティの基礎とリクエストの検証

Slack は、アプリの署名シークレットを使用してサーバーへのリクエストに署名します。Slack は X-Slack-Signature と X-Slack-Request-Timestamp ヘッダーを送信し、リプレイ攻撃を防ぐために 5 分より古いリクエストを拒否することを推奨します(Slack からのリクエストの検証)。

実際のコードレビューで現れる 2 つの足かせ:

  • 署名は、JSON パースやフォームデコード前の生リクエストボディに対して計算する必要があります(Slack からのリクエストの検証)。
  • インタラクティブペイロードは 3 秒以内に承認(ack)する必要があります。そのため、重い処理は非同期で行い、結果を通信するために response_url を使用してください(ユーザーインタラクションの処理)。

Python Slack SDK には、コードとドキュメントにリクエスト署名検証ユーティリティが含まれています(python-slack-sdk 署名検証器)。

メッセージとインタラクションの設計

アラートメッセージテンプレート

Slack をシステムインターフェースのように動作させたい場合は、意思決定が明確になるようにメッセージを構造化してください。チーム間でよく機能するメッセージテンプレート:

  • タイトル
  • 深刻度
  • コンテキスト
  • アクションヒント
  • リンク

最小限のテンプレート:

タイトル:checkout エラーレートが上昇 深刻度:warn コンテキスト:service=checkout env=prod region=us-east アクションヒント:Approve restart をクリックして安全な再起動をトリガー

入力 Webhook ペイロード例

入力 Webhook は JSON ペイロードを受け取り、Block Kit を使用したリッチなレイアウトを含めることができます(入力 Webhook を使用したメッセージ送信)。

{
  "text": "checkout error rate elevated",
  "blocks": [
    {
      "type": "header",
      "text": { "type": "plain_text", "text": "checkout error rate elevated" }
    },
    {
      "type": "section",
      "fields": [
        { "type": "mrkdwn", "text": "*severity*\\nwarn" },
        { "type": "mrkdwn", "text": "*context*\\nservice=checkout env=prod region=us-east" }
      ]
    },
    {
      "type": "section",
      "text": { "type": "mrkdwn", "text": "*action_hint*\\nClick Approve to trigger a safe restart." }
    }
  ]
}

ボタンと識別子の設計

ボタンは section または actions ブロック内に配置され、action_id とオプションの value を含める必要があります(ボタン要素)。action_id はあなたのルーティングキーです。value はあなたのペイロードです。これらは合わせてあなたのイベントスキーマとなります。

意見のある見解:action_id の値は、安定した API エンドポイントのように選んでください。“button_1” よりも “approve_restart” のような名前は、時間が経っても古くなりません。

インタラクションペイロードの処理、response_url、およびタイミング

Slack は、インタラクションペイロードを、JSON を含む payload パラメータを持つ form 形式のデータとしてリクエスト URL に送信します。ペイロードには、ボタンクリックなどのソースを定義する type フィールドが含まれています(ユーザーインタラクションの処理, インタラクションペイロード)。

承認応答に対しては、3 秒以内に HTTP 200 を返す必要があります(ユーザーインタラクションの処理)。元のメッセージを更新するか、チャンネル内またはスレッド内で応答するために response_url を使用し、Slack は response_url の使用を 30 分以内に最大 5 回に制限します(ユーザーインタラクションの処理)。

このタイミングの制約は設計上の制約です。これは、「承認」を「作業実行」から切り離すように強制します。

Slack に適合するインタラクションパターン

  • 承認と分岐のための Block Kit 内のボタン。
  • 明示的なユーザー意図とパラメータのためのスラッシュコマンド。
  • Workflow Builder 内の反復可能なビジネスプロセスのためのワークフローステップ(ワークフローステップ)。
  • 構造化された入力が必要な場合のショートカットとモーダル(インタラクティブ性ドキュメントで説明されている trigger_id 制約付き)(ユーザーインタラクションの処理)。

Go と Python の例

出版者注:これらは専用の例ページに分割できます:

  • /app-architecture/integration-patterns/slack/go-example
  • /app-architecture/integration-patterns/slack/python-example

これらの例は、システムを安定に保つ 1 つのことに優先順位を置いています:

  • Slack 署名を検証する
  • 3 秒以内に承認(ack)する
  • アクションを重複排除する
  • 内部 HTTP POST をトリガーする
  • オプションで response_url を使用して Slack を更新する

Go 例:アラート送信とボタン承認の処理

事前要件:

package main

import (
  "bytes"
  "crypto/hmac"
  "crypto/sha256"
  "encoding/hex"
  "encoding/json"
  "io"
  "log"
  "net/http"
  "net/url"
  "os"
  "strconv"
  "strings"
  "sync"
  "time"

  "github.com/slack-go/slack"
)

type BlockActionPayload struct {
  Type  string `json:"type"`
  Team  struct{ ID string `json:"id"` } `json:"team"`
  User  struct{ ID string `json:"id"` } `json:"user"`
  Channel struct {
    ID string `json:"id"`
  } `json:"channel"`
  Message struct {
    Ts string `json:"ts"`
  } `json:"message"`
  ResponseURL string `json:"response_url"`
  Actions []struct {
    ActionID string `json:"action_id"`
    Value    string `json:"value"`
    Type     string `json:"type"`
  } `json:"actions"`
}

type InternalAction struct {
  Action     string `json:"action"`
  TeamID     string `json:"team_id"`
  ChannelID  string `json:"channel_id"`
  MessageTS  string `json:"message_ts"`
  UserID     string `json:"user_id"`
  Value      string `json:"value"`
}

var (
  // 本番環境では、TTL を設定して Redis に保存します
  seenMu sync.Mutex
  seen   = map[string]time.Time{}
  ttl    = 10 * time.Minute
)

func main() {
  botToken := os.Getenv("SLACK_BOT_TOKEN")
  signingSecret := os.Getenv("SLACK_SIGNING_SECRET")
  channelID := os.Getenv("SLACK_CHANNEL_ID")
  internalURL := os.Getenv("INTERNAL_API_URL")
  listenAddr := os.Getenv("LISTEN_ADDR") // e.g. :8080

  if botToken == "" || signingSecret == "" || channelID == "" || internalURL == "" || listenAddr == "" {
    log.Fatal("missing env vars SLACK_BOT_TOKEN SLACK_SIGNING_SECRET SLACK_CHANNEL_ID INTERNAL_API_URL LISTEN_ADDR")
  }

  api := slack.New(botToken)

  // 承認ボタン付きのアラートメッセージを送信します。
  // ボタンは、action_id と value を持つインタラクティブな Block Kit 要素です。
  // Slack Block Kit ボタン要素のドキュメントを参照してください。
  blocks := slack.Blocks{
    BlockSet: []slack.Block{
      slack.NewHeaderBlock(slack.NewTextBlockObject("plain_text", "checkout error rate elevated", false, false)),
      slack.NewSectionBlock(
        slack.NewTextBlockObject("mrkdwn", "*severity*\\nwarn\\n*context*\\nservice=checkout env=prod", false, false),
        nil,
        nil,
      ),
      slack.NewActionBlock(
        "actions_1",
        slack.NewButtonBlockElement("approve_restart", "restart", slack.NewTextBlockObject("plain_text", "Approve restart", false, false)),
      ),
    },
  }

  _, ts, err := api.PostMessage(channelID, slack.MsgOptionBlocks(blocks.BlockSet...))
  if err != nil {
    log.Fatalf("PostMessage failed: %v", err)
  }
  log.Printf("posted alert message_ts=%s", ts)

  // インタラクティブ性エンドポイント
  http.HandleFunc("/slack/actions", func(w http.ResponseWriter, r *http.Request) {
    rawBody, err := io.ReadAll(r.Body)
    if err != nil {
      http.Error(w, "read body failed", http.StatusBadRequest)
      return
    }
    r.Body.Close()

    // 生ボディとタイムスタンプに対する Slack リクエスト署名を検証します。
    // Slack のリクエスト検証ドキュメントを参照してください。
    if !verifySlackRequest(r.Header, rawBody, signingSecret) {
      http.Error(w, "invalid signature", http.StatusUnauthorized)
      return
    }

    // Slack は、payload=JSON を持つ application/x-www-form-urlencoded を送信します
    vals, err := url.ParseQuery(string(rawBody))
    if err != nil {
      http.Error(w, "bad form body", http.StatusBadRequest)
      return
    }
    payloadStr := vals.Get("payload")
    if payloadStr == "" {
      http.Error(w, "missing payload", http.StatusBadRequest)
      return
    }

    var p BlockActionPayload
    if err := json.Unmarshal([]byte(payloadStr), &p); err != nil {
      http.Error(w, "bad payload json", http.StatusBadRequest)
      return
    }

    // 3 秒以内に承認します。実際の作業は非同期で行い、更新には response_url を使用します。
    // Slack のインタラクティブ性ドキュメントの承認タイミングを参照してください。
    w.WriteHeader(http.StatusOK)
    _, _ = w.Write([]byte(""))

    go func() {
      if p.Type != "block_actions" || len(p.Actions) == 0 {
        return
      }
      a := p.Actions[0]
      if a.ActionID != "approve_restart" {
        return
      }

      // 承認の重複排除
      key := strings.Join([]string{p.Team.ID, p.Channel.ID, p.Message.Ts, p.User.ID, a.ActionID, a.Value}, "|")
      if !tryOnce(key) {
        return
      }

      req := InternalAction{
        Action:    "approve_restart",
        TeamID:    p.Team.ID,
        ChannelID: p.Channel.ID,
        MessageTS: p.Message.Ts,
        UserID:    p.User.ID,
        Value:     a.Value,
      }

      if err := postJSON(internalURL, req); err != nil {
        log.Printf("internal action failed: %v", err)
        _ = replyViaResponseURL(p.ResponseURL, "action failed, check logs")
        return
      }

      _ = replyViaResponseURL(p.ResponseURL, "approval received, internal action triggered")
    }()
  })

  log.Printf("listening on %s", listenAddr)
  log.Fatal(http.ListenAndServe(listenAddr, nil))
}

func tryOnce(key string) bool {
  now := time.Now()

  seenMu.Lock()
  defer seenMu.Unlock()

  for k, t := range seen {
    if now.Sub(t) > ttl {
      delete(seen, k)
    }
  }
  if _, ok := seen[key]; ok {
    return false
  }
  seen[key] = now
  return true
}

func verifySlackRequest(h http.Header, body []byte, signingSecret string) bool {
  ts := h.Get("X-Slack-Request-Timestamp")
  sig := h.Get("X-Slack-Signature")
  if ts == "" || sig == "" {
    return false
  }

  tsInt, err := strconv.ParseInt(ts, 10, 64)
  if err != nil {
    return false
  }

  // リプレイリスクを減らすために、5 分以上前のリクエストを拒否します。
  if time.Since(time.Unix(tsInt, 0)) > 5*time.Minute {
    return false
  }

  base := "v0:" + ts + ":" + string(body)
  mac := hmac.New(sha256.New, []byte(signingSecret))
  mac.Write([]byte(base))
  sum := hex.EncodeToString(mac.Sum(nil))
  expected := "v0=" + sum

  return hmac.Equal([]byte(expected), []byte(sig))
}

func postJSON(url string, body any) error {
  b, err := json.Marshal(body)
  if err != nil {
    return err
  }
  req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(b))
  if err != nil {
    return err
  }
  req.Header.Set("Content-Type", "application/json")
  c := &http.Client{Timeout: 5 * time.Second}
  res, err := c.Do(req)
  if err != nil {
    return err
  }
  defer res.Body.Close()
  if res.StatusCode < 200 || res.StatusCode >= 300 {
    return io.ErrUnexpectedEOF
  }
  return nil
}

func replyViaResponseURL(responseURL string, text string) error {
  if responseURL == "" {
    return nil
  }
  // response_url は JSON ペイロードを受け取り、デフォルトでは一時的(ephemeral)に投稿できます。
  b, _ := json.Marshal(map[string]string{
    "text": text,
  })
  req, _ := http.NewRequest(http.MethodPost, responseURL, bytes.NewReader(b))
  req.Header.Set("Content-Type", "application/json")
  c := &http.Client{Timeout: 5 * time.Second}
  res, err := c.Do(req)
  if err != nil {
    return err
  }
  defer res.Body.Close()
  return nil
}

Python 例:アラート送信とボタン承認の処理

事前要件:

import os
import json
import time
import threading
import requests
from flask import Flask, request, make_response
from slack_sdk import WebClient
from slack_sdk.signature import SignatureVerifier

SLACK_BOT_TOKEN = os.environ["SLACK_BOT_TOKEN"]
SLACK_SIGNING_SECRET = os.environ["SLACK_SIGNING_SECRET"]
SLACK_CHANNEL_ID = os.environ["SLACK_CHANNEL_ID"]
INTERNAL_API_URL = os.environ["INTERNAL_API_URL"]

client = WebClient(token=SLACK_BOT_TOKEN)
verifier = SignatureVerifier(signing_secret=SLACK_SIGNING_SECRET)

app = Flask(__name__)

# 本番環境では、これらを Redis に保存します
_seen = {}
_TTL_SECONDS = 600

def try_once(key: str) -> bool:
    now = int(time.time())
    expired = [k for k, t in _seen.items() if now - t > _TTL_SECONDS]
    for k in expired:
        _seen.pop(k, None)
    if key in _seen:
        return False
    _seen[key] = now
    return True

def post_internal_action(payload: dict) -> None:
    requests.post(INTERNAL_API_URL, json=payload, timeout=5)

def reply_via_response_url(response_url: str, text: str) -> None:
    if not response_url:
        return
    requests.post(response_url, json={"text": text}, timeout=5)

def send_alert_with_button() -> None:
    blocks = [
        {"type": "header", "text": {"type": "plain_text", "text": "checkout error rate elevated"}},
        {"type": "section", "text": {"type": "mrkdwn", "text": "*severity*\\nwarn\\n*context*\\nservice=checkout env=prod"}},
        {
            "type": "actions",
            "block_id": "actions_1",
            "elements": [
                {
                    "type": "button",
                    "text": {"type": "plain_text", "text": "Approve restart"},
                    "action_id": "approve_restart",
                    "value": "restart"
                }
            ]
        }
    ]
    # chat.postMessage には chat:write スコープが必要です。
    client.chat_postMessage(channel=SLACK_CHANNEL_ID, text="checkout error rate elevated", blocks=blocks)

@app.post("/slack/actions")
def slack_actions():
    raw_body = request.get_data()  # Slack はパース前の生ボディを検証することを推奨
    if not verifier.is_valid_request(raw_body, request.headers):
        return make_response("invalid signature", 401)

    # Slack は、JSON を含む payload フィールドを持つ application/x-www-form-urlencoded を送信します。
    payload_str = request.form.get("payload", "")
    if not payload_str:
        return make_response("missing payload", 400)
    payload = json.loads(payload_str)

    # 3 秒以内に承認します。そうしないと Slack ユーザーはエラーを表示します。
    # Slack のインタラクティブ性ドキュメントの承認を参照してください。
    resp = make_response("", 200)

    def work():
        if payload.get("type") != "block_actions":
            return
        actions = payload.get("actions", [])
        if not actions:
            return

        a = actions[0]
        if a.get("action_id") != "approve_restart":
            return

        team_id = payload.get("team", {}).get("id", "")
        channel_id = payload.get("channel", {}).get("id", "")
        user_id = payload.get("user", {}).get("id", "")
        message_ts = payload.get("message", {}).get("ts", "")
        value = a.get("value", "")

        key = "|".join([team_id, channel_id, message_ts, user_id, "approve_restart", value])
        if not try_once(key):
            return

        internal_payload = {
            "action": "approve_restart",
            "team_id": team_id,
            "channel_id": channel_id,
            "message_ts": message_ts,
            "user_id": user_id,
            "value": value,
        }

        try:
            post_internal_action(internal_payload)
            reply_via_response_url(payload.get("response_url", ""), "approval received, internal action triggered")
        except Exception:
            reply_via_response_url(payload.get("response_url", ""), "action failed, check logs")

    threading.Thread(target=work, daemon=True).start()
    return resp

if __name__ == "__main__":
    send_alert_with_button()
    app.run(host="0.0.0.0", port=int(os.getenv("PORT", "8080")))

オペレーションズ注記:ルーティング、UX、セキュリティ、リンク、および SEO

Slack、ページングツール、Discord の使用タイミング

このページは仕組みについてです。ルーティングは戦略です。それでも、境界は簡単に説明できます。

チャンネル 最適な用途 失敗モード
PagerDuty または同等のもの 即時対応が必要な緊急性の高いユーザーへの影響 Slack では寝てしまう
Slack 調整、承認、ワークフローの実行 ノイズとチャンネル疲労
Discord Discord に住んでいるチーム、軽量なコントロールループ エンタープライズワークフロー構造が少ない

会話とワークフローをインターフェースにしたい場合は Slack を使用してください。アラートが選択肢ではない場合は、ページングツールを使用してください。Slack のインタラクション設計とサービス境界、永続性の選択をバランスさせたい場合は、このアプリアーキテクチャの概要 が、その決定をより大きなシステムに位置づけるのに役立ちます。より深いルーティングモデルについては、可観測性チームのためのモダンなアラートシステム設計 を参照してください。Discord 統合の代替案については、アラートとコントロールループのための Discord 統合パターン を参照してください。

アクセシビリティと UX の注記

  • 高ボリュームのアラートを独自のチャンネルに配置し、人間の議論はスレッドに留めます。
  • インシデントごとのコンテキストと更新にはスレッドを使用します。response_url は、thread_ts が提供された場合にチャンネル内とスレッド内に投稿できます(ユーザーインタラクションの処理)。
  • チャンネルのスパムを避けるために、ユーザーアクションを承認する際には一時的(ephemeral)な応答を使用しますが、一時的な配信は保証されず、セッションに依存することに注意してください(chat.postEphemeral)。
  • レイアウトを素早くプロトタイプするには Block Kit Builder を使用します(Block Kit)。
  • 画像を追加する場合は、サポートされている場合に意味のある代替テキストを含め、トップレベルの text フィールドにプレーンテキストフォールバックを保持します。

セキュリティチェックリスト

  • 署名シークレットヘッダーと生ボディを使用して、すべての受信 Slack リクエストを検証します(Slack からのリクエストの検証)。
  • リプレイリスクを減らすために、5 分以上前のタイムスタンプを持つリクエストを拒否します(Slack からのリクエストの検証)。
  • トークンと Webhook URL をシークレットマネージャーに保存し、git には決して保存しません。
  • 最小権限の OAuth スコープを使用し、役割が変更されたらシークレットをローテーションします(スコープ参照)。
  • 内部アクション API を別個に認証および認可し、Slack を認証境界として扱いません。
  • 承認を冪等かつ重複排除するようにします。
  • チーム、チャンネル、メッセージタイムスタンプ、ユーザー、アクションを含め、監査に適した方法で承認をログに記録します。

結論

Slack は、メッセージシンクではなく、システム境界として扱うときに最も機能します。入力 Webhook は、高速なアラート配信をカバーします。アプリとインタラクティブ性は、Slack をワークフローエンジンとイベントインターフェスに変えます。難しい部分は、署名検証、タイミング制約、重複排除、およびアラートルーティングモデルにおける Slack の位置づけの選択です。

次のリンク: