AWS Lambda durable functionsでBacklog課題の完了サマリー自動生成しSlackでの承認フローを構築してみた

AWS Lambda durable functionsでBacklog課題の完了サマリー自動生成しSlackでの承認フローを構築してみた

2026.02.01

リテールアプリ共創部のるおんです。

皆さんは、プロジェクトの管理ツールに何を使っていますか?自分はBacklogというツールを使って顧客との課題の管理やマイルストーンを管理しています。その際に、最終的な課題の結論や決定事項がどこにあるかわからなくて悩むことがよくありました。

私は普段、Backlogの課題が完了すると、その結論や討議内容を完了サマリーとしてまとめるようにしています。こうすることで、後からBacklogを見たときに「この課題で最初に読むべきところ」が一目でわかるからです。

例:
スクリーンショット 2026-01-31 13.34.59

私は普段の開発でBacklog Exporterというツールを使ってBacklogの課題をローカルにエクスポートしています。これをすることで、AIエージェントがBacklogの情報を読み取れるようになり、仕様書やシーケンス図の作成にも活用できます。

そしてその流れで、Claude Codeのカスタムスラッシュコマンドを用意して、それを使うことにより課題のコメント全てを読ませ、完了サマリーを生成していました。

❯ /generate-issue-summary '/Users/ryuhei202/Backlog/issues/2025/Githubアカウントについてのご相談.md'
カスタムスラッシュコマンドの内容
---
description: Backlogの課題をClose(完了)する際に、課題の結論を生成するために使用
---
ユーザーから要求された「$ARGUMENTS」の課題の結論を生成して下さい。

## 手順
- 他の完了済み課題の完了サマリーを複数必ず参照してください。
- 課題の結論を生成します。

## 課題の結論生成ルール
### 1. 基本構造
課題の結論は以下の構造で作成すること:

# 完了サマリー
## 課題の内容
[課題の内容を記載]
## 課題の結論
[課題の結論を記載]
## 完了条件
[「◯/◯の定例にて◯◯さん確認済み」や「◯◯様から承認済み」など]
## 備考・補足事項
[追加で記録すべき情報、他の課題へのリンク等]

### 2. 記載のポイント
- Markdown形式で出力
- 読みやすさを重視し、適切に箇条書きと表を使用
- 重要な決定事項は太字で強調
- 長くなりすぎないように簡潔にまとめる

自動化のモチベーション

この運用自体は便利なのですが、毎回手動でやるのは正直めんどくさいです。。

  • Backlog Exporterでエクスポート
  • カスタムスラッシュコマンドでサマリー生成
  • 生成されたサマリーをBacklogにコピペ

この一連の作業を自動化したい。そこで今回、AWS Lambda durable functionsを使って、課題が完了したら自動でAIがサマリーを生成し、Slackで承認フローを経てからBacklogにコメントを投稿するシステムを構築しました。

https://github.com/takagakiryuheiCM/backlog-completion-notifier/tree/main

デモ

まずは実際の動作を見てみましょう。

1. Backlogで課題を完了にする

Backlogの課題ステータスを「完了」に変更します。今回は、会員登録の名前のフィールドに関する課題だと仮定します。

スクリーンショット 2026-02-01 15.36.40

2. Slackに承認リクエストが届く

数秒後、Slackに以下のような承認リクエストメッセージが届きます。AIが生成したサマリーと、「承認して投稿」「却下」のボタンが表示されます。

スクリーンショット 2026-02-01 15.44.33

3. 承認するとBacklogにコメントが投稿される

「承認して投稿」ボタンをクリックすると、AIが生成したサマリーがBacklogの課題にコメントとして自動投稿されます。

スクリーンショット 2026-02-01 15.40.18

スクリーンショット 2026-02-01 15.40.34

これで手動作業がゼロになりました!

なぜdurable functionsを使うのか

このシステムの肝はSlackでの人間による承認フローです。AIが生成したサマリーをそのまま投稿するのではなく、一度人間が確認してから投稿するようにしています。

しかし、通常のLambda関数は最大15分のタイムアウト制限があり、人間の承認を待つような長時間のワークフローには向いていません。

そこで登場するのがAWS Lambda durable functionsです。2025年12月にre:Inventで発表されたこの機能は、最大1年間実行可能なワークフローを構築できます。しかも、待機中はコンピュート料金が発生しません。

https://dev.classmethod.jp/articles/aws-lambda-durable-functions-awsreinvent/

アーキテクチャ

今回構築するシステムの全体像は以下の通りです。

architecture

使用技術

技術 用途
AWS Lambda API用とdurable functions用の2つ
Amazon Bedrock (Claude Opus 4.5) サマリー生成
AWS CDK インフラのコード管理
Hono API Lambdaのルーティング
Backlog API 課題情報取得・コメント投稿
Slack API 承認リクエスト送信・結果通知

セットアップ

このシステムを動かすために必要なセットアップを説明します。

Backlogの設定

バックログにコメントを追加するためのAPIキーの取得と、完了イベントを受け取るためのWebhookの設定をします。

1. 詳細

APIキーの取得

  1. Backlogにログイン
  2. 右上のアカウントアイコン → 「個人設定」
  3. 「API」タブ → 「新しいAPIキーを発行」
  4. 発行されたAPIキーを控えておく

Webhookの設定

  1. Backlogのプロジェクト設定 → 「インテグレーション」→「Webhook」
  2. 「Webhookを追加する」をクリック
  3. 以下を設定:
    • WebHook URL: デプロイ後に出力されるURLを設定(後で設定)
    • イベント: 「課題の更新」にチェック

参考

https://support-ja.backlog.com/hc/ja/articles/360035641754-APIの設定
https://support-ja.backlog.com/hc/ja/articles/360036147713-Webhook

Slackの設定

Slack通知をするためのBotの作成と、承認ボタンクリック時のリクエストを受け取るためのInteractivity設定、そしてメッセージ送信用のBot Tokenの取得を実施します。

2. 詳細

Slack Appの作成

  1. Slack APIにアクセス
  2. 「Create New App」→「From scratch」
  3. App名とワークスペースを設定

Bot Token Scopesの設定

「OAuth & Permissions」で以下のスコープを追加:

  • chat:write - メッセージ送信用

Interactivityの有効化

「Interactivity & Shortcuts」で以下を設定:

  1. 「Interactivity」をONにする
  2. Request URL: デプロイ後に出力されるURLを設定(後で設定)

Bot Tokenの取得

「OAuth & Permissions」で「Install to Workspace」をクリックし、表示されるxoxb-で始まるBot User OAuth Tokenを控えておきます。

参考

https://docs.slack.dev/quickstart
https://api.slack.com/scopes
https://api.slack.com/interactivity/handling

3. AWSの設定

SSMパラメータの作成

秘匿情報(Backlog APIキーとSlack Bot Token)はSSM Parameter Storeで管理します。以下のコマンドでパラメータを作成してください。

bash
# Backlog APIキー
aws ssm put-parameter \
  --name "/backlog-completion-notifier/BACKLOG_API_KEY" \
  --value "YOUR_BACKLOG_API_KEY" \
  --type String

# Slack Bot Token
aws ssm put-parameter \
  --name "/backlog-completion-notifier/SLACK_BOT_TOKEN" \
  --value "xoxb-xxxxx" \
  --type String

Amazon Bedrockのモデルアクセス有効化

Amazon Bedrockで呼び出すLLMのモデルアクセスを有効化します。

  1. AWSコンソール → Amazon Bedrock
  2. 「Model access」→「Manage model access」
  3. 「Claude Opus 4.5」にチェックを入れて有効化

4. セットアップ

リポジトリをクローンして、セットアップスクリプトを実行します。

git clone https://github.com/your-username/backlog-completion-notifier.git
cd backlog-completion-notifier

# 依存関係のインストール
pnpm install

# セットアップスクリプトを実行
pnpm setup

対話形式で設定値を入力します。

🔧 Backlog Completion Notifier セットアップ

Backlog スペースID: YOUR_BACKLOG_SPACE_ID
Slack チャンネルID: YOUR_SLACK_CHANNEL_ID

✅ 設定を保存しました: infra/config.ts

これによりinfra/config.tsが自動生成されます。手動で編集したい場合は、直接ファイルを編集してもOKです。

infra/config.ts
import type { Config } from "./config-type";

export const config: Config = {
  backlogSpaceId: "your-space-id", // BacklogのスペースID
  slackChannelId: "C0123456789",   // 通知先のSlackチャンネルID
};

5. デプロイ

pnpm run deploy

デプロイが完了すると、Lambdaの関数URLが出力されます。

Outputs:
BacklogCompletionNotifierStack.BacklogWebhookUrl = https://xxxxx.lambda-url.ap-northeast-1.on.aws/webhook/backlog
BacklogCompletionNotifierStack.SlackWebhookUrl = https://xxxxx.lambda-url.ap-northeast-1.on.aws/webhook/slack

6. Webhook URLの登録

出力されたURLを各サービスに登録します。

Backlog:

  1. プロジェクト設定 → Webhook
  2. 「BacklogWebhookUrl」を登録
  3. 「課題の更新」イベントを選択

Slack:

  1. Slack App設定 → Interactivity & Shortcuts
  2. 「SlackWebhookUrl」をRequest URLに設定

インフラ構成(CDK)

処理フローの詳細に入る前に、CDKで構築するインフラ構成を見てみましょう。このシステムは2つのLambda関数で構成されています。

https://github.com/takagakiryuheiCM/backlog-completion-notifier/blob/main/infra/lib/stack/server-stack.ts

infra/lib/stack/server-stack.ts
// ===========================================
// durable function Lambda
// ===========================================
const durableFunctionName = "backlog-completion-durable";

const durableFunction = new nodejs.NodejsFunction(this, "DurableFunction", {
  functionName: durableFunctionName,
  runtime: lambda.Runtime.NODEJS_22_X,
  entry: path.join(serverSrcPath, "handler/durable/handler.ts"),
  handler: "handler",
  timeout: cdk.Duration.seconds(30),
  memorySize: 1024,
  architecture: lambda.Architecture.ARM_64,
+  // durable functionsの設定
+  durableConfig: {
+    executionTimeout: cdk.Duration.days(1),  // 最大24時間待機可能
+    retentionPeriod: cdk.Duration.days(7),   // 実行履歴を7日間保持
+  },
});

// ===========================================
// API Lambda (Monolithic - Hono)
// Backlog/Slack Webhook受信を1つのLambdaで処理
// ===========================================
const apiFunctionName = "backlog-completion-api";

const apiFunction = new nodejs.NodejsFunction(this, "ApiFunction", {
  functionName: apiFunctionName,
  runtime: lambda.Runtime.NODEJS_22_X,
  entry: path.join(serverSrcPath, "handler/api/handler.ts"),
  handler: "handler",
  timeout: cdk.Duration.seconds(30),
  memorySize: 512,
  architecture: lambda.Architecture.ARM_64,
  environment: {
    DURABLE_FUNCTION_NAME: durableFunctionName,
  },
});

// Function URLでAPIを公開
const apiFunctionUrl = apiFunction.addFunctionUrl({
  authType: lambda.FunctionUrlAuthType.NONE,
});

ポイント:

  • API Lambda: BacklogとSlackからのWebhookを受信するエントリーポイント。Honoフレームワークでルーティングを行いモノリシックに保つようにしています
  • durable function Lambda: 長時間ワークフローを実行。durableConfigで待機時間と履歴保持期間を設定します

IAM権限の設定

durable functionsを使うには、適切なIAM権限の設定が必要です。

infra/lib/stack/server-stack.ts
// durable functionにBedrock呼び出し権限を付与
durableFunction.addToRolePolicy(
  new iam.PolicyStatement({
    actions: ["bedrock:InvokeModel"],
    resources: ["*"],
  })
);

// API Lambdaがdurable functionを呼び出す権限
durableFunction.grantInvoke(apiFunction);

+// API Lambdaがdurable functionのコールバックを送信する権限
+apiFunction.addToRolePolicy(
+  new iam.PolicyStatement({
+    actions: [
+      "lambda:SendDurableExecutionCallbackSuccess",
+      "lambda:SendDurableExecutionCallbackFailure",
+    ],
+    resources: [
+      durableFunction.functionArn,
+      `${durableFunction.functionArn}:*`,
+    ],
+  })
+);

特に重要なのがSendDurableExecutionCallbackSuccessSendDurableExecutionCallbackFailureの権限です。これらはdurable functions専用のアクションで、外部から待機中のワークフローに結果を送信するために必要です。

処理フローの詳細

ここからは、各処理フェーズを詳しく見ていきます。

Phase 1: 課題完了検知

Backlogで課題のステータスが変更されると、Webhookが送信されます。

API Lambdaのbacklog-webhook-handler.tsがリクエストを受信し、HandleBacklogWebhookUseCaseを実行します。UseCase内では以下の判定を行います。

https://github.com/takagakiryuheiCM/backlog-completion-notifier/blob/1e4a9bd7faf7c48509ee782e4f8eb80e851394e2/server/src/use-case/handle-backlog-webhook/handle-backlog-webhook-use-case.ts#L33-L108

server/src/use-case/handle-backlog-webhook/handle-backlog-webhook-use-case.ts
async execute(input: HandleBacklogWebhookInput): Promise<HandleBacklogWebhookOutput> {
  // 課題更新イベント以外は無視
  if (input.type !== BacklogEventType.ISSUE_UPDATED) {
    return {
      status: "ignored",
      reason: "Not an issue update event",
    };
  }

  // 課題が完了状態でなければ無視
  if (!isIssueCompleted(input)) {
    return {
      status: "ignored",
      reason: "Issue is not completed",
    };
  }

+  // durable functionを起動
+ await this.#durableFunctionClient.invoke({
+   issueKey,
+   projectKey: input.project.projectKey,
+   issueSummary: input.issue.summary,
+   issueDescription: input.issue.description || "",
+ });

  return { status: "invoked", issueKey };
}

課題が完了状態に変更された場合のみ、durable functionを起動します。

LambdaDurableFunctionClient.invoke() の実装
server/src/infrastructure/lambda-durable-function-client.ts
import {
  LambdaClient,
  InvokeCommand,
  SendDurableExecutionCallbackSuccessCommand,
  SendDurableExecutionCallbackFailureCommand,
} from "@aws-sdk/client-lambda";

export class LambdaDurableFunctionClient implements DurableFunctionClient {
  readonly #lambdaClient: LambdaClient;
  readonly #functionName: string;

  constructor(lambdaClient: LambdaClient, functionName: string) {
    this.#lambdaClient = lambdaClient;
    this.#functionName = functionName;
  }

  async invoke(params: DurableFunctionParams): Promise<void> {
    // durable functionはqualified ARN(バージョン指定)が必要
    const command = new InvokeCommand({
      FunctionName: this.#functionName,
      Qualifier: "$LATEST",
      InvocationType: "Event", // 非同期呼び出し
      Payload: JSON.stringify(params),
    });

    await this.#lambdaClient.send(command);
  }

  // ...sendCallbackSuccess, sendCallbackFailure は Phase 3 で紹介
}

Phase 2: サマリー生成・承認リクエスト

Phase 1でdurable functionが起動されると、durable/handler.tsが呼び出されます。このハンドラー内でProcessIssueCompletionUseCaseが実行されます。

https://github.com/takagakiryuheiCM/backlog-completion-notifier/blob/1e4a9bd7faf7c48509ee782e4f8eb80e851394e2/server/src/use-case/process-issue-completion/process-issue-completion-use-case.ts#L68-L225

context.step()でチェックポイントを設定しながら処理を進めます。

server/src/use-case/process-issue-completion/process-issue-completion-use-case.ts
async execute(
  input: ProcessIssueCompletionInput,
  context: DurableContext
): Promise<ProcessIssueCompletionOutput> {
  const { issueKey } = input;

+  // Step 1: 課題詳細とコメントを取得
  const issueDetails = await context.step("fetch-issue-details", async () => {
    const [issue, comments] = await Promise.all([
      this.#backlogRepository.getIssue(issueKey),
      this.#backlogRepository.getComments(issueKey),
    ]);
    return { /* 課題情報 */ };
  });

+  // Step 2: サマリーを生成
  const summary = await context.step("generate-summary", async () => {
    const prompt = buildCompletionSummaryPrompt({
      issueKey: issueDetails.issueKey,
      issueSummary: issueDetails.issueSummary,
      issueDescription: issueDetails.issueDescription,
      comments: issueDetails.comments,
    });
    return this.#summaryGenerator.generate(prompt);
  });

+  // Step 3: コールバックを作成
  const [callbackPromise, callbackId] = await context.createCallback("approval", {
    timeout: { hours: 24 },
  });

+  // Step 4: Slack承認リクエストを送信
  await context.step("send-approval-request", async () => {
    const message = buildApprovalMessage({
      channel: this.#slackChannelId,
      issueKey: issueDetails.issueKey,
      summary,
      callbackId,
    });
    await this.#slackNotifier.postMessage(message);
  });

+  // ここで待機(コンピュート料金なし)
  const result = await callbackPromise;
  // ...後続処理
}
BacklogClient(課題情報・コメント取得)の実装
server/src/infrastructure/backlog-client.ts
export class BacklogClient implements BacklogRepository {
  readonly #apiKey: string;
  readonly #spaceId: string;
  readonly #baseUrl: string;

  constructor(spaceId: string, apiKey: string) {
    this.#apiKey = apiKey;
    this.#spaceId = spaceId;
    this.#baseUrl = `https://${spaceId}.backlog.jp/api/v2`;
  }

  async getIssue(issueIdOrKey: string): Promise<BacklogIssue> {
    const url = `${this.#baseUrl}/issues/${issueIdOrKey}?apiKey=${this.#apiKey}`;
    const response = await fetch(url);

    if (!response.ok) {
      throw new Error(`Failed to get issue: ${response.statusText}`);
    }

    const apiResponse = (await response.json()) as BacklogApiIssueResponse;
    return toBacklogIssue(apiResponse);
  }

  async getComments(issueIdOrKey: string): Promise<BacklogComment[]> {
    const url = `${this.#baseUrl}/issues/${issueIdOrKey}/comments?apiKey=${this.#apiKey}&order=asc`;
    const response = await fetch(url);

    if (!response.ok) {
      throw new Error(`Failed to get comments: ${response.statusText}`);
    }

    const apiResponse = (await response.json()) as BacklogApiCommentResponse[];
    return apiResponse
      .filter((comment) => comment.content) // 空コメントを除外
      .map(toBacklogComment);
  }

  // ...addComment, getIssueUrl は Phase 4 で使用
}
BedrockClient(サマリー生成)の実装
server/src/infrastructure/bedrock-client.ts
import {
  BedrockRuntimeClient,
  InvokeModelCommand,
} from "@aws-sdk/client-bedrock-runtime";

export class BedrockClient implements SummaryGenerator {
  readonly #client: BedrockRuntimeClient;
  readonly #modelId: string;
  readonly #logger: Logger;

  constructor(logger: Logger) {
    this.#client = new BedrockRuntimeClient({ region: "ap-northeast-1" });
    this.#modelId = "global.anthropic.claude-opus-4-5-20251101-v1:0";
    this.#logger = logger;
  }

  async generate(prompt: string): Promise<string> {
    this.#logger.info(`[BedrockClient] Calling model: ${this.#modelId}`);

    const command = new InvokeModelCommand({
      modelId: this.#modelId,
      contentType: "application/json",
      accept: "application/json",
      body: JSON.stringify({
        anthropic_version: "bedrock-2023-05-31",
        max_tokens: 1024,
        messages: [{ role: "user", content: prompt }],
      }),
    });

    const response = await this.#client.send(command);
    const responseBody = JSON.parse(
      new TextDecoder().decode(response.body)
    ) as BedrockResponse;

    this.#logger.info(
      `[BedrockClient] Success, tokens: ${responseBody.usage.input_tokens}/${responseBody.usage.output_tokens}`
    );

    return responseBody.content[0].text;
  }
}
SlackClient(承認リクエスト送信)の実装
server/src/infrastructure/slack-client.ts
export class SlackClient implements SlackNotifier {
  readonly #botToken: string;
  readonly #baseUrl = "https://slack.com/api";

  constructor(botToken: string) {
    this.#botToken = botToken;
  }

  async postMessage(message: SlackMessage): Promise<void> {
    const response = await fetch(`${this.#baseUrl}/chat.postMessage`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${this.#botToken}`,
      },
      body: JSON.stringify(message),
    });

    if (!response.ok) {
      throw new Error(`Failed to post message: ${response.statusText}`);
    }

    const result = (await response.json()) as SlackPostMessageResponse;

    if (!result.ok) {
      throw new Error(`Slack API error: ${result.error}`);
    }
  }
}
プロンプトテンプレート(サマリー生成用)

AIにサマリーを生成させるためのプロンプトです。課題情報とコメント履歴を整形して渡し、構造化されたサマリーを出力させます。

server/src/domain/template/prompt/completion-summary-prompt.ts
export const buildCompletionSummaryPrompt = (
  params: CompletionSummaryPromptParams
): string => {
  const { issueKey, issueSummary, issueDescription, comments = [] } = params;
  const commentsSection = formatComments(comments);

  return `あなたはプロジェクト管理のアシスタントです。
以下のBacklog課題が完了しました。課題の説明とコメント履歴(やり取り)を読み、完了サマリーをMarkdown形式で作成してください。

## 課題情報
- 課題キー: ${issueKey}
- タイトル: ${issueSummary}
- 説明: ${issueDescription || "説明なし"}

## コメント履歴(やり取り)
${commentsSection}

## 出力形式
以下の構造で出力してください:

# 完了サマリー

## 課題の内容
[課題の内容を簡潔に記載]

## 対応内容・経緯
[コメント履歴から、どのような議論や対応が行われたかを要約]

## 課題の結論
[課題がどのように解決・完了されたかを記載]

## 備考・補足事項
[追加で記録すべき情報があれば記載、なければ「特になし」]`;
};

コメント履歴を含めることで、単なる課題の説明だけでなく、やり取りの経緯や決定事項もサマリーに反映されます。

ここで重要なのは、context.createCallback()で生成したコールバックIDをSlackメッセージのボタンに埋め込んでいることです。
これにより、複数のシステム間でcallbackIdを共有し、durable functionのコールバックを送信できます。

server/src/domain/template/slack/approval-message.ts
elements: [
  {
    type: "button",
    text: { type: "plain_text", text: "承認して投稿" },
    style: "primary",
    action_id: "approve",
    value: JSON.stringify({
+      callbackId,  // ここでコールバックIDを渡す
      approved: true,
    }),
  },
  {
    type: "button",
    text: { type: "plain_text", text: "却下" },
    style: "danger",
    action_id: "reject",
    value: JSON.stringify({
+      callbackId,
      approved: false,
    }),
  },
],

Phase 3: ユーザー承認

ユーザーがSlackでボタンをクリックすると、SlackからWebhookが送信されます。

API Lambdaのslack-webhook-handler.tsがリクエストを受信し、HandleSlackWebhookUseCaseを実行します。UseCase内では承認/却下に応じてdurable functionのコールバックを送信します。

https://github.com/takagakiryuheiCM/backlog-completion-notifier/blob/1e4a9bd7faf7c48509ee782e4f8eb80e851394e2/server/src/use-case/handle-slack-webhook/handle-slack-webhook-use-case.ts#L33-L98

server/src/use-case/handle-slack-webhook/handle-slack-webhook-use-case.ts
async execute(input: HandleSlackWebhookInput): Promise<HandleSlackWebhookOutput> {
  const { callbackId, approved, userName } = input;

  if (approved) {
+    await this.#durableFunctionClient.sendCallbackSuccess(callbackId, {
      approved: true,
      approvedBy: userName,
      approvedAt: this.#fetchNow().toISOString(),
+    });
  } else {
+    await this.#durableFunctionClient.sendCallbackFailure(callbackId, {
      rejectedBy: userName,
+    });
  }

  return buildApprovalResponse({ approved, userName });
}
LambdaDurableFunctionClient(コールバック送信)の実装
server/src/infrastructure/lambda-durable-function-client.ts
import {
  LambdaClient,
  InvokeCommand,
+  SendDurableExecutionCallbackSuccessCommand,
+  SendDurableExecutionCallbackFailureCommand,
} from "@aws-sdk/client-lambda";

export class LambdaDurableFunctionClient implements DurableFunctionClient {
  readonly #lambdaClient: LambdaClient;
  readonly #functionName: string;

  constructor(lambdaClient: LambdaClient, functionName: string) {
    this.#lambdaClient = lambdaClient;
    this.#functionName = functionName;
  }

  async sendCallbackSuccess(callbackId: string, result: ApprovalResult): Promise<void> {
    // AWS SDK v3 の新しいコマンド
+    const command = new SendDurableExecutionCallbackSuccessCommand({
+      CallbackId: callbackId,
+      Result: new TextEncoder().encode(JSON.stringify(result)),
+    });

    await this.#lambdaClient.send(command);
  }

  async sendCallbackFailure(callbackId: string, rejection: RejectionInfo): Promise<void> {
+    const command = new SendDurableExecutionCallbackFailureCommand({
+      CallbackId: callbackId,
+      Error: {
+        ErrorType: "REJECTED",
+        ErrorMessage: `Rejected by ${rejection.rejectedBy}`,
+      },
+    });

    await this.#lambdaClient.send(command);
  }

  // ...invoke() は Phase 1 で紹介済み
}

ポイント: SendDurableExecutionCallbackSuccessCommandSendDurableExecutionCallbackFailureCommandはAWS SDK v3の最新バージョンで追加された、durable functions専用のコマンドです。これらを使うことで、外部から待機中のdurable functionに結果を送信できます。

sendCallbackSuccess()を呼び出すと、待機中のdurable functionのcallbackPromiseが解決され、処理が再開されます。

Phase 4: 後処理(リプレイ)

コールバックが解決されると、durable/handler.tsが再び呼び出され、ProcessIssueCompletionUseCase最初からリプレイされます。

ただし、Step 1〜4は完了済みのチェックポイントがあるため、スキップされます。callbackPromiseが解決された後の後続処理から実行が再開されます。

https://github.com/takagakiryuheiCM/backlog-completion-notifier/blob/1e4a9bd7faf7c48509ee782e4f8eb80e851394e2/server/src/use-case/process-issue-completion/process-issue-completion-use-case.ts#L162-L225

server/src/use-case/process-issue-completion/process-issue-completion-use-case.ts
+// callbackPromiseが解決された後の処理
try {
  const result = await callbackPromise;
  approval = JSON.parse(result) as CallbackResult;
} catch {
+  // 却下またはタイムアウト
  await context.step("send-rejection-notification", async () => {
    await this.#slackNotifier.postMessage(/* 却下通知 */);
  });
  return { issueKey, status: "rejected", summary };
}

// 承認時の処理
if (approval.approved) {
+  // Step 5: Backlogにコメント投稿
  await context.step("post-backlog-comment", async () => {
    const comment = buildBacklogComment({ summary });
    await this.#backlogRepository.addComment(issueKey, comment);
  });

+  // Step 6: Slackに完了通知
  await context.step("send-completion-notification", async () => {
    await this.#slackNotifier.postMessage(/* 完了通知 */);
  });
}

return { issueKey, status: "completed", approved: approval.approved, summary };
BacklogClient.addComment(コメント投稿)の実装
server/src/infrastructure/backlog-client.ts
async addComment(issueIdOrKey: string, content: string): Promise<void> {
  const response = await fetch(
    `${this.#baseUrl}/issues/${issueIdOrKey}/comments?apiKey=${this.#apiKey}`,
    {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({ content }),
    }
  );

  if (!response.ok) {
    throw new Error(`Failed to add comment: ${response.statusText}`);
  }
}

getIssueUrl(issueKey: string): string {
  return `https://${this.#spaceId}.backlog.jp/view/${issueKey}`;
}
完了通知メッセージの生成
server/src/domain/template/slack/completion-message.ts
export const buildCompletionMessage = (
  params: CompletionMessageParams
): SlackMessage => {
  const { channel, issueKey, issueUrl, summary, approvedBy, approvedAt } = params;

  return {
    channel,
    text: `課題 ${issueKey} の完了サマリーを投稿しました`,
    blocks: [
      {
        type: "header",
        text: {
          type: "plain_text",
          text: "✅ 完了サマリー投稿完了",
        },
      },
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: `*課題:* <${issueUrl}|${issueKey}>`,
        },
      },
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: `*承認者:* ${approvedBy}\n*承認日時:* ${approvedAt}`,
        },
      },
      {
        type: "divider",
      },
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: `*投稿内容:*\n${summary}`,
        },
      },
    ],
  };
};

durable functionsの重要な概念

チェックポイント/リプレイ機構

durable functionsの核となるのがチェックポイント/リプレイ機構です。

[障害発生 or wait/callback完了]
        ↓
[関数を最初から再実行(リプレイ)]
        ↓
[完了済み step はスキップ、保存結果を使用]
        ↓
[未完了の処理から再開]

この仕組みにより、以下のメリットがあります。

  • 障害耐性: 処理中にエラーが発生しても、チェックポイントから再開
  • 長時間実行: 待機中はコンピュート料金が発生しない
  • 冪等性: リプレイ時に同じ処理が重複実行されない

プロジェクト構成

最後に、このプロジェクトのディレクトリ構成を紹介します。

backlog-completion-notifier/
├── infra/                              # CDKインフラコード
│   ├── bin/infra.ts
│   ├── lib/stack/server-stack.ts       # Lambda定義
│   ├── lib/util/ssm.ts                 # SSMパラメータ取得
│   ├── config.ts                       # 設定値
│   └── config-type.ts                  # 設定型定義
└── server/                             # サーバーコード
    └── src/
        ├── handler/
        │   ├── api/                    # API Lambda
        │   │   ├── handler.ts
        │   │   ├── app.ts              # Honoアプリ
        │   │   └── route/webhook/      # Webhookハンドラー
        │   └── durable/                # durable function
        │       └── handler.ts
        ├── use-case/                   # ユースケース層
        │   ├── handle-backlog-webhook/
        │   ├── handle-slack-webhook/
        │   └── process-issue-completion/
        ├── domain/                     # ドメイン層
        │   ├── model/backlog/          # Backlog関連型定義
        │   ├── support/                # インターフェース
        │   └── template/               # テンプレート(Slack、プロンプト)
        ├── infrastructure/             # インフラ層
        │   ├── backlog-client.ts
        │   ├── bedrock-client.ts
        │   ├── slack-client.ts
        │   └── lambda-durable-function-client.ts
        └── di-container/               # 依存性注入
            └── register-container.ts

レイヤードアーキテクチャに基づいた構成で、DIで依存関係を整理するようにして、各層の責務を明確に分離しています。

おわりに

今回は、AWS Lambda durable functionsを使って、Backlog課題完了時のサマリー生成・投稿を自動化するシステムを構築しました。

durable functionsの特徴であるチェックポイント/リプレイ機構により、Slackでの承認待ちのような長時間のワークフローを、コンピュート料金を抑えながら実現できました。

手動でやっていた作業が自動化されると、やはり快適ですね。

以上、どなたかの参考になれば幸いです!

参考

https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html

https://docs.aws.amazon.com/lambda/latest/dg/durable-execution-sdk.html

https://github.com/aws/aws-durable-execution-sdk-js

https://dev.classmethod.jp/articles/aws-lambda-durable-functions-awsreinvent/

https://dev.classmethod.jp/articles/aws-lambda-durable-functions-callback-awsreinvent/

この記事をシェアする

FacebookHatena blogX

関連記事