Slack-Integrationsmuster für Warnungen und Workflows

Slack ist eine Workflow-Oberfläche und eine Schicht für die Alert-Lieferung.

Inhaltsverzeichnis

Slack-Integrationen täuschen oft durch ihre Einfachheit, da Sie eine Nachricht mit einer einzigen HTTP-Anfrage senden können. Der interessante Teil beginnt, wenn Sie Slack interaktiv und zuverlässig machen möchten.

Slack-Integration Alerts

Dieser vertiefte Artikel behandelt Slack als drei verschiedene Integrationsoberflächen:

  • Benachrichtigungsspeicher für einseitige Warnungen über eingehende Webhooks.
  • Workflow-Engine über Workflow Builder und benutzerdefinierte Workflow-Schritte.
  • Ereignisschnittstelle über Block Kit-Schaltflächen, Slash-Befehle und Aktions-Payloads.

Diese Seite beschreibt, wie Systeme die Grenze zu einer gemeinsamen Benutzeroberfläche überschreiten, die ihrerseits Ereignisse zurück in Ihre Architektur senden kann, und nicht über die Philosophie von Warnungen. Für Warnstrategien und Routing siehe Design moderner Warnsysteme für Observability-Teams.

Lesen Sie auch:

Kanonische Einordnung und Platzierung in Integrationsmustern

Slack ist nicht nur der Ort, an dem Warnungen sterben. Wenn es gut genutzt wird, wird Slack zu einer System-Schnittstelle, in der Nachrichten zustandsbehaftete Artefakte sind und Benutzerinteraktionen Ereignisse darstellen.

Diese Seite ist kanonisch unter /app-architecture/integration-patterns/slack/ platziert, weil die Hauptfrage nicht „sollen wir warnen" ist, sondern „welcher Vertrag besteht zwischen unserem System und Slack".

Wenn Ihre Lösung eines der folgenden Elemente erfordert, befinden Sie sich im Bereich der Integrationsmuster und nicht im Bereich einfacher Benachrichtigungen:

  • Eine Entscheidungs-Schleife, bei der eine menschliche Genehmigung eine Aktion freigibt.
  • Ein Workflow, bei dem Slack Kontext sammelt und Schritte auslöst.
  • Eine Ereignisschleife, bei der Slack Aktionen aussendet, auf die Ihr System abonniert.

Die Slack-Plattform unterstützt absichtlich sowohl einseitige Nachrichten als auch bidirektionale Interaktionen über Request-URLs und Interaktions-Payloads. Eingehende Webhooks sind eine primäre Methode, um JSON-Payloads, einschließlich Block Kit-Bausteinen, in einen Kanal zu senden (Nachrichten senden mit eingehenden Webhooks). Interaktivität wird an Ihre App als HTTP-POST-Anfragen an eine konfigurierte Request-URL zurückgegeben, und diese Payloads sind als Formular kodiert mit einem JSON-Payload-Feld (Verarbeitung von Benutzerinteraktionen).

Slack als Benachrichtigungsspeicher

Eingehende Webhooks sind der schnellste Weg, um Wert für Warnungen und Statusaktualisierungen zu generieren. Ein Webhook ist eine eindeutige URL, die an eine App-Installation gebunden ist, und Sie senden eine JSON-Nachricht daran (Nachrichten senden mit eingehenden Webhooks).

Meine persönliche Meinung: Webhooks sind ein ausgezeichneter Standardwert, wenn Sie „senden und vergessen"-Nachrichten wünschen und Sie nicht benötigen, dass Slack eine Steuerungsoberfläche darstellt. Webhooks sind auch ein ausgezeichneter Weg, um Ihre Onboarding-Prozesse von Ihrer späteren App-Architektur zu entkoppeln.

Slack als Workflow-Engine

Der Workflow Builder existiert, weil Chat der Ort ist, an dem Arbeit tatsächlich stattfindet. Workflows können einfach oder komplex sein und können mit Apps verbunden werden (Leitfaden zum Workflow Builder).

Benutzerdefinierte Workflow-Schritte ermöglichen es Ihnen, Ihre Systeme als wiederverwendbare Bausteine innerhalb des Workflow Builders verfügbar zu machen (Workflow-Schritte). Dies ist eine andere Integrationsform als Bots in Kanälen. Es rückt Ihre Integration näher an „Werkzeuge innerhalb von Slack" heran als an „Nachrichten von außen".

Meine persönliche Meinung: Wenn Ihre Organisation bereits in Workflows und Genehmigungen denkt, können Workflow-Schritte natürlicher wirken als maßgeschneiderte Bots.

Slack als Ereignisschnittstelle

Block Kit verwandelt eine Nachricht in eine Benutzeroberfläche (Block Kit). Interaktive Komponenten wie Schaltflächen generieren Aktions-Payloads, typischerweise block_actions-Payloads, die an Ihre App gesendet werden, wenn ein Benutzer klickt (block_actions-Payload).

Schaltflächen haben explizite Identifikatoren action_id und einen optionalen value und müssen in section- oder actions-Blöcken gehostet werden (Schaltflächen-Element). Wenn Sie eine Nachricht mit einer Schaltfläche entwerfen, entwerfen Sie eine Ereignisquelle.

Hier werden FAQ-Themen wie Anfrageverifizierung, erforderliche Berechtigungen (Scopes) und sichere interne Trigger zum Zentrum des Designs.

Architekturmuster, die skalieren

Webhook-Flow für einseitige Warnungen

[Dienst] -> [Warnformatierer] -> [Slack eingehender Webhook] -> [Kanal]

Eingehende Webhooks akzeptieren JSON-Payloads und unterstützen Block Kit-Layouts (Nachrichten senden mit eingehenden Webhooks).

Wenn die FAQ nach dem schnellsten Weg zum Senden von Warnungen fragt, ist dies meist die Antwort.

Broker-Flow mit einer Warteschlange für Zuverlässigkeit und Backpressure

[Dienste] -> [Warteschlangen-Topic] -> [Slack-Dispatcher] -> [Slack-API]
                     |                 |
                     |                 +-> [Rate-Limit-Handler]
                     +-> [Dead-Letter-Queue]

Slack-Rate-Limits gelten für HTTP-basierte APIs, einschließlich eingehender Webhooks, und Slack gibt HTTP 429 mit einem Retry-After-Header zurück, wenn Sie die Limits überschreiten (Rate Limits).

Meine persönliche Meinung: Wenn Sie Warnungen direkt von jedem Dienst senden, wird der erste Vorfall zu einer verteilten Denial-of-Service-Angriff gegen Ihre eigene Slack-Integration. Ein Dispatcher hinter einer Warteschlange neigt dazu, eine ruhigere Architektur zu sein.

Workflow-Automatisierungsmuster mit Genehmigungen

[Warnung] -> [Slack-Nachricht mit Schaltfläche] -> [Schaltflächen-Klick]
   -> [Aktions-Payload] -> [Genehmigungs-Handler] -> [interne API] -> [Nachricht aktualisieren]

Slack-Interaktivität erfordert die Konfiguration einer Request-URL und die Aktivierung der Interaktivität. Slack sendet Interaktions-Payloads als application/x-www-form-urlencoded mit einem payload-Parameter, der JSON enthält, und Sie müssen innerhalb von 3 Sekunden mit HTTP 200 antworten (Verarbeitung von Benutzerinteraktionen).

Dies ist das Muster hinter dem FAQ-Eintrag über das sichere Auslösen interner Aktionen.

Slack-Interaktionsflussdiagramm

Slack-Interaktionsdiagramm

Webhook vs. App und Implementierungsmechaniken

Empfohlene Bibliotheken

Go:

Python:

Webhook vs. App-Bot-Ansatz

Ein praktischer Vergleich:

Fähigkeit Eingehender Webhook Slack-App mit Bot-Token
Nachrichten senden Ja Ja
Block Kit-Layouts senden Ja Ja
Schaltflächen-Klicks empfangen Nur, wenn an eine App mit Interaktivität gebunden Ja
Slash-Befehle Nein Ja
Workflow-Schritte Nein Ja
Sicherheitsfläche Geheimhaltung der Webhook-URL OAuth-Tokens plus Signing Secret
Bestes Einsatzgebiet Einseitige Warnungen Workflows, Genehmigungen, interaktive UI

Slack unterstützt explizit Block Kit-Layouts mit eingehenden Webhooks (Nachrichten senden mit eingehenden Webhooks). Interaktivität wird pro App konfiguriert und an eine Request-URL geliefert (Verarbeitung von Benutzerinteraktionen).

Meine persönliche Meinung: Webhooks sind ein großartiger erster Meilenstein, aber sobald Sie möchten, dass Slack eine Steuerungsoberfläche ist, bauen Sie eine App. Tun Sie nicht so, als wäre es anders.

Berechtigungen und Scopes

Slack-Scopes definieren, was Ihre App tun kann. Es gibt eine zentrale Scopes-Referenz und einzelne Scope-Seiten (Scopes Referenz). Für das Senden von Nachrichten über die Web-API ist chat:write der kanonische Scope (chat:write Scope).

Für Slash-Befehle benötigen Sie typischerweise den commands-Scope und eine konfigurierte Command-Request-URL (Befehle sind Teil der Interaktivitäts-Dokumentation, und jeder Befehl hat seine eigene Request-URL) (Verarbeitung von Benutzerinteraktionen).

FAQ-Hinweis: Die Lieferung von Schaltflächen-Payloads ist „kein Scope", es ist eine App-Einstellung. Ihre App empfängt Payloads, wenn Interaktivität aktiviert ist und eine Request-URL gesetzt ist, aber das Senden von Nachrichtenaktualisierungen erfordert im Allgemeinen immer noch chat:write.

Rate Limits und Retries

Slack-Rate-Limits geben HTTP 429 zurück und enthalten Retry-After in Sekunden, und dies gilt für HTTP-basierte APIs, einschließlich eingehender Webhooks (Rate Limits).

In der Praxis:

  • Retry-After beachten
  • Backoff mit Jitter für vorübergehende 5xx-Fehler anwenden
  • Slack-Lieferung in einem Dispatcher zentralisieren, wenn das Volumen wächst

Idempotenz und Deduplizierung

Slack erwartet eine Bestätigung für Interaktions-Payloads innerhalb von 3 Sekunden, sonst sehen Benutzer einen Fehler und Slack kann das Lieferverhalten je nach Funktion erneut versuchen (Verarbeitung von Benutzerinteraktionen). Für die Events API stellt Slack explizit Retry-Metadaten-Header x-slack-retry-num bereit (Events API).

Auch ohne explizite Retries treten Duplikate auf, weil Benutzer doppelt klicken und weil verteilte Systeme erneut übertragen. Wenn Ihre Schaltfläche eine interne Aktion auslöst, behandeln Sie Klicks als „mindestens einmal"-Ereignisse und deduplizieren Sie.

Eine praktische Idempotenzstrategie für Genehmigungen:

  • Idempotenz-Schlüssel = team_id + channel_id + message_ts + action_id + user_id
  • Schlüssel in Redis mit TTL speichern, die Ihrem Workflow-Fenster entspricht
  • Die interne Aktions-API erzwingt ebenfalls Idempotenz, nicht nur der Slack-Handler

Sicherheitsgrundlagen und Anfrageverifizierung

Slack signiert Anfragen an Ihren Server unter Verwendung Ihres App-Signing-Secrets. Slack sendet die Header X-Slack-Signature und X-Slack-Request-Timestamp, und Slack empfiehlt, Anfragen, die älter als fünf Minuten sind, abzulehnen, um Replay-Angriffe zu verhindern (Verifizierung von Anfragen von Slack).

Zwei Fallstricke, die in echten Code-Reviews auftreten:

  • Sie müssen die Signatur über den rohen Request-Body berechnen, bevor JSON-Parsing oder Formular-Dekodierung (Verifizierung von Anfragen von Slack).
  • Sie müssen interaktive Payloads innerhalb von 3 Sekunden bestätigen, also führen Sie schwere Arbeiten asynchron aus und verwenden Sie response_url, um Ergebnisse mitzuteilen (Verarbeitung von Benutzerinteraktionen).

Das Python Slack SDK enthält eine Anfrage-Signatur-Verifizierer-Nutzung in Code und Docs (python-slack-sdk signature verifier).

Design von Nachrichten und Interaktionen

Vorlage für Warnnachrichten

Wenn Sie möchten, dass Slack wie eine System-Schnittstelle fungiert, strukturieren Sie Ihre Nachrichten so, dass Entscheidungen offensichtlich sind. Eine Nachrichtenvorlage, die in Teams gut funktioniert:

  • Titel
  • Schweregrad
  • Kontext
  • Aktionshinweis
  • Links

Eine minimale Vorlage:

Titel: Checkout-Fehlerquote erhöht Schweregrad: warn Kontext: service=checkout env=prod region=us-east Aktionshinweis: Klicken Sie auf „Approve restart", um einen sicheren Neustart auszulösen

Beispiel für eingehenden Webhook-Payload

Eingehende Webhooks akzeptieren JSON-Payloads und können reichhaltige Layouts unter Verwendung von Block Kit enthalten (Nachrichten senden mit eingehenden Webhooks).

{
  "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." }
    }
  ]
}

Design von Schaltflächen und Identifikatoren

Schaltflächen müssen in section- oder actions-Blöcken enthalten sein und action_id sowie einen optionalen value beinhalten (Schaltflächen-Element). action_id ist Ihr Routing-Schlüssel. value ist Ihre Payload. Zusammen bilden sie Ihr Ereignisschema.

Meine persönliche Meinung: Wählen Sie action_id-Werte wie stabile API-Endpunkte. Namen wie „approve_restart" altern besser als „button_1".

Verarbeitung von Interaktions-Payloads, response_url und Timing

Slack sendet Interaktions-Payloads an Ihre Request-URL als formular-kodierte Daten mit einem payload-Parameter, der JSON enthält. Die Payload enthält ein type-Feld, das die Quelle definiert, z. B. block_actions für Schaltflächen-Klicks (Verarbeitung von Benutzerinteraktionen, Interaktions-Payloads).

Sie müssen innerhalb von 3 Sekunden HTTP 200 für die Bestätigungsrückantwort zurückgeben (Verarbeitung von Benutzerinteraktionen). Verwenden Sie response_url, um die ursprüngliche Nachricht zu aktualisieren oder im Kanal oder in einem Thread zu antworten, und Slack begrenzt die Nutzung von response_url auf bis zu fünfmal innerhalb von dreißig Minuten (Verarbeitung von Benutzerinteraktionen).

Diese Timing-Beschränkung ist eine Designbeschränkung. Sie zwingt Sie, „bestätigen" von „arbeiten" zu entkoppeln.

Interaktionsmuster, die zu Slack passen

  • Schaltflächen in Block Kit für Genehmigungen und Verzweigungen.
  • Slash-Befehle für explizite Benutzerabsicht und Parameter.
  • Workflow-Schritte für wiederholbare Geschäftsprozesse im Workflow Builder (Workflow-Schritte).
  • Shortcuts und Modals, wenn Sie strukturierte Eingaben benötigen, mit trigger_id-Beschränkungen, die in den Interaktivitäts-Docs beschrieben sind (Verarbeitung von Benutzerinteraktionen).

Go- und Python-Beispiele

Verlagshinweis: Diese können in dedizierte Beispiel-Seiten aufgeteilt werden:

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

Die Beispiele priorisieren eine Sache, die Systeme stabil hält:

  • Slack-Signaturen verifizieren
  • Innerhalb von drei Sekunden bestätigen (ack)
  • Aktionen deduplizieren
  • Eine interne HTTP-POST-Anfrage auslösen
  • Optional Slack unter Verwendung von response_url aktualisieren

Go-Beispiel: Warnung senden und Schaltflächen-Genehmigung verarbeiten

Voraussetzungen:

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 (
  // In der Produktion in Redis mit TTL speichern
  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") // z. B. :8080

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

  api := slack.New(botToken)

  // Eine Warnnachricht mit einer Genehmigungsschaltfläche senden.
  // Schaltflächen sind interaktive Block Kit-Elemente mit action_id und value.
  // Siehe Slack Block Kit Schaltflächen-Element-Docs.
  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 fehlgeschlagen: %v", err)
  }
  log.Printf("Warnnachricht message_ts=%s gesendet", ts)

  // Interaktivitäts-Endpoint
  http.HandleFunc("/slack/actions", func(w http.ResponseWriter, r *http.Request) {
    rawBody, err := io.ReadAll(r.Body)
    if err != nil {
      http.Error(w, "Lesen des Body fehlgeschlagen", http.StatusBadRequest)
      return
    }
    r.Body.Close()

    // Slack-Anfragesignatur auf dem rohen Body und Timestamp verifizieren.
    // Siehe Slack-Docs zur Verifizierung von Anfragen.
    if !verifySlackRequest(r.Header, rawBody, signingSecret) {
      http.Error(w, "ungültige Signatur", http.StatusUnauthorized)
      return
    }

    // Slack sendet application/x-www-form-urlencoded mit payload=JSON
    vals, err := url.ParseQuery(string(rawBody))
    if err != nil {
      http.Error(w, "schleifer Formular-Body", http.StatusBadRequest)
      return
    }
    payloadStr := vals.Get("payload")
    if payloadStr == "" {
      http.Error(w, "fehlende Payload", http.StatusBadRequest)
      return
    }

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

    // Innerhalb von 3 Sekunden bestätigen. Echte Arbeit asynchron erledigen und response_url für Updates verwenden.
    // Siehe Slack-Interaktivitäts-Docs zur Bestätigungstiming.
    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
      }

      // Genehmigungen deduplizieren
      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("interne Aktion fehlgeschlagen: %v", err)
        _ = replyViaResponseURL(p.ResponseURL, "Aktion fehlgeschlagen, Protokolle prüfen")
        return
      }

      _ = replyViaResponseURL(p.ResponseURL, "Genehmigung erhalten, interne Aktion ausgelöst")
    }()
  })

  log.Printf("lauschen auf %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
  }

  // Anfragen älter als 5 Minuten ablehnen, um Replay-Risiko zu reduzieren.
  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 akzeptiert JSON-Payloads und kann standardmäßig ephemere senden.
  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-Beispiel: Warnung senden und Schaltflächen-Genehmigung verarbeiten

Voraussetzungen:

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__)

# In der Produktion in Redis speichern
_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 erfordert chat:write Scope.
    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 empfiehlt, den rohen Body vor dem Parsen zu verifizieren
    if not verifier.is_valid_request(raw_body, request.headers):
        return make_response("ungültige Signatur", 401)

    # Slack sendet application/x-www-form-urlencoded mit einem payload-Feld, das JSON enthält.
    payload_str = request.form.get("payload", "")
    if not payload_str:
        return make_response("fehlende Payload", 400)
    payload = json.loads(payload_str)

    # Innerhalb von 3 Sekunden bestätigen. Slack-Benutzer sehen Fehler, wenn Sie das nicht tun.
    # Siehe Slack-Interaktivitäts-Docs zur Bestätigung.
    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", ""), "Genehmigung erhalten, interne Aktion ausgelöst")
        except Exception:
            reply_via_response_url(payload.get("response_url", ""), "Aktion fehlgeschlagen, Protokolle prüfen")

    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")))

Wann Slack vs. Paging-Tools vs. Discord verwenden

Diese Seite handelt von Mechaniken. Routing ist Strategie. Dennoch ist die Grenze einfach zu beschreiben.

Kanal Am besten für Fehlermodus
PagerDuty oder äquivalent Dringender Einfluss auf Benutzer, der sofortige Reaktion erfordert Menschen schlafen über Slack hinweg
Slack Koordination, Genehmigungen, Workflow-Ausführung Lärm und Kanal-Ermüdung
Discord Teams, die in Discord leben, leichtgewichtigere Regelkreise Weniger Unternehmens-Workflow-Struktur

Verwenden Sie Slack, wenn Sie möchten, dass die Konversation und der Workflow die Schnittstelle sind. Verwenden Sie Paging-Tools, wenn die Warnung nicht optional ist. Wenn Sie das Design der Slack-Interaktion gegen Service-Grenzen und Persistenzentscheidungen abwägen, hilft diese App-Architektur-Übersicht diese Entscheidung im größeren System zu verorten. Für ein tieferes Routing-Modell siehe Design moderner Warnsysteme für Observability-Teams. Für eine Discord-Integration-Alternative siehe Discord-Integration Pattern für Warnungen und Regelkreise.

Hinweise zu Barrierefreiheit und UX

  • Legen Sie Warnungen mit hohem Volumen in ihren eigenen Kanal und behalten Sie menschliche Diskussionen in Threads.
  • Verwenden Sie Threads für Kontext und Updates pro Vorfall. response_url kann im Kanal und in Threads posten, wenn thread_ts bereitgestellt wird (Verarbeitung von Benutzerinteraktionen).
  • Verwenden Sie ephemere Antworten bei der Bestätigung von Benutzeraktionen, um Kanalspam zu vermeiden, aber denken Sie daran, dass die Lieferung von ephemeren Nachrichten nicht garantiert ist und sessionsabhängig ist (chat.postEphemeral).
  • Verwenden Sie den Block Kit Builder, um Layouts schnell zu prototypisieren (Block Kit).
  • Wenn Sie Bilder hinzufügen, fügen Sie sinnvolle Alt-Texte hinzu, wo unterstützt, und behalten Sie einen Plain-Text-Fallback im obersten Textfeld.

Sicherheits-Checkliste

  • Jede eingehende Slack-Anfrage unter Verwendung von Signing-Secret-Headern und dem rohen Body verifizieren (Verifizierung von Anfragen von Slack).
  • Anfragen mit Zeitstempeln, die älter als fünf Minuten sind, ablehnen, um das Replay-Risiko zu reduzieren (Verifizierung von Anfragen von Slack).
  • Tokens und Webhook-URLs in einem Secret-Manager speichern, niemals in Git.
  • Least-Privilege OAuth-Scopes verwenden und Geheimnisse rotieren, wenn sich Rollen ändern (Scopes Referenz).
  • Die interne Aktions-API separat authentifizieren und autorisieren, behandeln Sie Slack nicht als Authentifizierungsgrenze.
  • Genehmigungen idempotent und dedupliziert machen.
  • Genehmigungen in einer auditfreundlichen Weise protokollieren, einschließlich Team, Kanal, Nachricht-Zeitstempel, Benutzer und Aktion.

Fazit

Slack ist am besten, wenn Sie es als Systemgrenze und nicht als Nachrichten-Speicher behandeln. Eingehende Webhooks decken eine schnelle Warnlieferung ab. Apps plus Interaktivität verwandeln Slack in eine Workflow-Engine und eine Ereignisschnittstelle. Die schwierigen Teile sind die Signaturverifizierung, Timing-Beschränkungen, Deduplizierung und die Wahl, wo Slack in Ihrem Warn-Routing-Modell passt.

Nächste Links: