はじめに
SlackワークフローからAmazon Q Developer in chat applications(旧AWS Chatbot)経由でLambda関数を実行する運用をしていると、「処理結果をコマンドを実行したスレッドに返信したい」という要望が出てきます。
しかし、Amazon Q Developer経由でLambdaを実行する場合、Lambda関数にはスレッドを特定するための情報(thread_tsなど)を渡すことができません。(SlackWorkflowにも変数が用意されてない)
そのため、何も工夫しないと処理結果は元のスレッドとは別のメッセージとしてチャンネルに投稿することになります。
本記事では、この課題を解決するためのTipsを紹介します。
課題
SlackワークフローからAmazon Q Developer経由でLambda関数を実行する流れは以下のようになります。
- ユーザーがSlackワークフローを実行
- ワークフローがAmazon Q Developerに対して
@Amazon Q lambda invoke ...を送信 - Amazon Q Developerがスレッドで確認メッセージを返信( [Run] command ボタンで実行)
- ユーザーが承認
- Lambda関数が実行される
この時、Lambda関数には--payloadで指定した任意のJSONしか渡すことができません。
SlackWorkflowからデフォルトで取得できる情報は以下の通りで、メッセージを特定するための情報はありません。
- ワークフローをトリガーした人の情報(表示名、ユーザーIDなど)
- ワークフローが実行されたOrgの情報
- ワークフローが開始した時刻(形式はUTC/現地時間/タイムスタンプなどから選択可能)
- ワークフローが実行されたチャンネル情報(チャンネル名、チャンネルID)
- ワークフローが実行されたcanvasの情報
上記以外にも、フォームで情報を収集するステップを構築している場合は、そのフォームの入力内容を引数として渡すことができますが、フォームでスレッドの情報を入力することはできません。
そのため、Lambda関数からSlackに通知を送る場合、元のスレッドに返信する形ではなく、チャンネルに新しいメッセージとして投稿することになってしまいます。
解決策
この問題を解決するアプローチは以下の通りです。
- Lambda関数の引数にメッセージを特定するための情報を含める(タイムスタンプなど)
- Lambda関数内でSlackのHistory APIを実行してメッセージを取得
- 取得したメッセージから引数で受け取った情報と一致するメッセージを検索
- 一致するメッセージの
thread_tsを取得してSlack通知に利用
アーキテクチャ
┌─────────────────────┐
│ Slackワークフロー │
└────────┬────────────┘
│ @Amazon Q lambda invoke --payload {"ts": "xxx", ...}
▼
┌────────────────────┐
│ Amazon Q Developer │ ─── スレッドで確認 ───▶ ユーザーが承認
└────────┬───────────┘
│
▼
┌──────────────────┐
│ Lambda関数 │
│ │
│ 1. payload受信 │
│ 2. History API │──▶ Slack API (conversations.history)
│ 3. メッセージ検索 │
│ 4. thread_ts取得 │
│ 5. 結果をスレッド │──▶ Slack API (chat.postMessage)
│ に投稿 │
└──────────────────┘
実装
前提条件
- Slack Botトークン(
conversations.historyとchat:writeのスコープが必要) - チャンネルIDとBotトークンをAWS Secrets Managerに保存
Slackワークフローでの呼び出し例
Slackワークフローから以下のようなコマンドを実行します。
@Amazon Q lambda invoke --function-name my-lambda-function --payload {"ts": "{}ワークフローが開始した時刻", "args": "hogehoge", "env": "staging"}
ポイントは--payloadにts(タイムスタンプ)を含めることです。
{}ワークフローが開始した時刻には、SlackWorkflowの変数を挿入でワークフローが開始した時刻を指定して、形式はタイムスタンプにします。
このタイムスタンプを使って、後でSlackのメッセージ履歴から該当メッセージを特定します。
Lambda関数のペイロード定義
type Payload struct {
TS string `json:"ts"` // メッセージ特定用のタイムスタンプ
Env string `json:"env"`
Args string `json:"args"`
}
Slackセッションの管理
Slackクライアントとスレッド情報を保持する構造体を定義します。
// SlackSession holds Slack client and thread information for posting messages
type SlackSession struct {
Client *slack.Client
ChannelID string
ThreadTS string // Empty string means post to channel directly
}
スレッド解決のコア実装
メッセージ履歴から該当するメッセージを検索し、thread_tsを取得する関数です。
// ResolveSlackThread finds the thread timestamp for a given message timestamp
// by searching recent messages in the specified channel.
// Returns a SlackSession with ThreadTS="" if the message is not found (fallback to channel posting).
func ResolveSlackThread(ctx context.Context, ts string) (*SlackSession, error) {
logger := slog.With("component", "slack_thread", "ts", ts)
// Slack設定を初期化
slackCtx, err := InitSlackContext(ctx)
if err != nil {
return nil, fmt.Errorf("failed to initialize slack context: %w", err)
}
// tsが空の場合はスレッド解決不要
if ts == "" {
logger.InfoContext(ctx, "no ts provided, posting to channel directly")
return &SlackSession{
Client: slackCtx.Client,
ChannelID: slackCtx.ChannelID,
ThreadTS: "",
}, nil
}
client := slackCtx.Client
channelID := slackCtx.ChannelID
// チャンネルの最近のメッセージを取得
logger.InfoContext(ctx, "searching for message with ts in channel")
params := &slack.GetConversationHistoryParameters{
ChannelID: channelID,
Limit: 20, // 直近20件を検索
}
history, err := client.GetConversationHistoryContext(ctx, params)
if err != nil {
return nil, fmt.Errorf("failed to get conversation history: %w", err)
}
// メッセージを検索
for _, msg := range history.Messages {
text := msg.Text
// Lambda invokeコマンドを含むメッセージかチェック
if !strings.Contains(text, "lambda invoke") {
continue
}
// 対象のLambda関数名を含むかチェック
if !strings.Contains(text, "--function-name my-lambda-function") {
continue
}
// --payloadからJSONを抽出
payloadStart := strings.Index(text, "--payload ")
if payloadStart == -1 {
continue
}
payloadStart += len("--payload ")
payloadText := text[payloadStart:]
payloadEnd := strings.Index(payloadText, " --")
if payloadEnd != -1 {
payloadText = payloadText[:payloadEnd]
}
payloadText = strings.TrimSpace(payloadText)
// JSONをパース
var payload struct {
TS string `json:"ts"`
}
if err := json.Unmarshal([]byte(payloadText), &payload); err != nil {
logger.WarnContext(ctx, "failed to parse payload JSON",
"error", err,
"message_ts", msg.Timestamp)
continue
}
// TSが一致するかチェック
if payload.TS != ts {
continue
}
// 一致したらthread_tsを取得
threadTS := msg.ThreadTimestamp
if threadTS == "" {
// トップレベルメッセージの場合、そのタイムスタンプをthread_tsとして使用
threadTS = msg.Timestamp
}
logger.InfoContext(ctx, "found matching message",
"thread_ts", threadTS,
"message_ts", msg.Timestamp)
return &SlackSession{
Client: client,
ChannelID: channelID,
ThreadTS: threadTS,
}, nil
}
// 見つからない場合はチャンネル直接投稿にフォールバック
logger.WarnContext(ctx, "message not found, falling back to channel posting")
return &SlackSession{
Client: client,
ChannelID: channelID,
ThreadTS: "",
}, nil
}
Lambdaハンドラーでの使用
func HandleLambdaEvent(ctx context.Context, payload Payload) error {
// スレッド情報を解決してcontextに保存
slackSession, err := ResolveSlackThread(ctx, payload.TS)
if err != nil {
return err
}
ctx = WithSlackSession(ctx, slackSession)
// 以降の処理でSlackSessionを利用
executeSomething(ctx, payload)
}
スレッドへのメッセージ投稿
func postSlackReport(ctx context.Context, summary string) error {
session, ok := SlackSessionFromContext(ctx)
if !ok {
return fmt.Errorf("slack session not found in context")
}
msgOptions := []slack.MsgOption{slack.MsgOptionText(summary, false)}
// thread_tsがある場合はスレッドに返信
if session.ThreadTS != "" {
msgOptions = append(msgOptions, slack.MsgOptionTS(session.ThreadTS))
}
_, _, err := session.Client.PostMessageContext(ctx, session.ChannelID, msgOptions...)
return err
}
Context経由でのセッション管理
アプリケーション全体でSlackSessionを共有するためのヘルパー関数です。
type slackSessionKey struct{}
// WithSlackSession adds SlackSession to the context
func WithSlackSession(ctx context.Context, session *SlackSession) context.Context {
return context.WithValue(ctx, slackSessionKey{}, session)
}
// SlackSessionFromContext retrieves SlackSession from the context
func SlackSessionFromContext(ctx context.Context) (*SlackSession, bool) {
session, ok := ctx.Value(slackSessionKey{}).(*SlackSession)
return session, ok
}
実装のポイント
検索条件の工夫
メッセージ履歴から該当メッセージを特定するため、複数の条件を組み合わせて検索します。
// 1. lambda invokeコマンドを含むか
if !strings.Contains(text, "lambda invoke") {
continue
}
// 2. 対象のLambda関数名を含むか
if !strings.Contains(text, "--function-name my-lambda-function") {
continue
}
// 3. payload内のTSが一致するか
if payload.TS != ts {
continue
}
フォールバック戦略
該当メッセージが見つからない場合でも、処理を継続できるようにフォールバックを実装します。
// 見つからない場合はチャンネル直接投稿にフォールバック
return &SlackSession{
Client: client,
ChannelID: channelID,
ThreadTS: "", // 空文字列 = チャンネルに直接投稿
}, nil
検索範囲の設定
Limitパラメータで検索するメッセージ数を調整します。
多すぎるとAPI呼び出しが遅くなり、少なすぎると該当メッセージが見つからない可能性があります。
ただし、多くのケースではメッセージが投稿された直後にLambdaが実行されるため、最新の1件がほとんどの場合で該当するはずです。
params := &slack.GetConversationHistoryParameters{
ChannelID: channelID,
Limit: 20, // 適切な値を設定
}
注意点
Slack APIのスコープ
Botトークンには以下のスコープが必要です。
-
channels:historyまたはgroups:history(プライベートチャンネルの場合) chat:write
API Rate Limit
Slack APIにはRate Limitがあるため、頻繁に実行される処理では注意が必要です。
メッセージの保持期間
Slackの無料プランではメッセージの保持期間に制限があるため、古いメッセージは検索できない場合があります。
まとめ
本記事では、Amazon Q Developer in chat applications経由でLambda関数を実行する際に、処理結果を元のスレッドに返信する方法を紹介しました。
直接的にスレッド情報を渡すことはできませんが、以下の工夫により元のスレッドに返信する形で処理結果を通知することが可能です。
- Lambda関数の引数にメッセージを特定するための情報(タイムスタンプなど)を含める
- Slack History APIでメッセージ履歴を取得
- 引数の情報と一致するメッセージを検索して
thread_tsを特定 - 取得した
thread_tsを使ってスレッドに返信
このアプローチを活用することで、SlackワークフローとAmazon Q Developerを組み合わせた運用においても、ユーザー体験を向上させることができます。
ところでAmazon Q Developer in chat applicationsって名前長いですよね。AWS Chatbotの方が覚えやすかったなぁ。