0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Lambda Durable Functions で Bedrock × 人間承認ワークフローを動かした話

0
Last updated at Posted at 2026-02-27

はじめに

AWS re:Invent 2025 の発表から気になっていた Lambda Durable Functions を、Bedrock を組み合わせた AI コンテンツ審査ワークフローで試した。Developerプレビューなのでドキュメントが薄く、いくつかの落とし穴で詰まったため、動くまでの経緯をそのまま残しておく。


Lambda Durable Functions とは

Lambda 関数が長期間にわたって実行を「中断・再開」できる仕組みを AWS がネイティブに提供したもの。Step Functions が別サービスとして状態を管理するのに対して、Durable Functions はコードを書く感覚がずっと素直だ。

public class OrderProcessor extends DurableHandler<Order, OrderResult> {
    @Override
    protected OrderResult handleRequest(Order order, DurableContext ctx) {
        var reservation = ctx.step("reserve-inventory", Reservation.class,
            () -> inventoryService.reserve(order.getItems()));

        ctx.wait(Duration.ofHours(2));  // ここで2時間待つ。その間は課金されない

        var shipment = ctx.step("confirm-shipment", Shipment.class,
            () -> shippingService.ship(reservation, order.getAddress()));

        return new OrderResult(order.getId(), shipment.getTrackingNumber());
    }
}

ctx.wait() の間、Lambda の実行は完全に停止する。コンピュートの課金も止まる。2時間後に自動的に再起動し、wait() の次の行から続きを実行する。チェックポイントの仕組みが SDK 内部で透過的に動いているので、コードを書く側は普通の逐次処理として書ける。

実行タイムアウトは最大1年に設定できる。承認フローや長期バッチ処理など、「処理自体は短いが合計の待機時間が長い」ユースケースによく合う。


今回作ったもの

ユーザーが投稿したコンテンツを AI が審査し、グレーゾーンの場合は人間に判断を委ねるワークフロー。

1. invoke → Bedrock (Claude) にコンテンツを送って審査
2a. APPROVE / REJECT の場合 → 即時結果を返す
2b. NEEDS_HUMAN の場合 →
    - コールバックID を発行して一時停止
    - SNS でレビュアーに通知 (コールバックID付き)
    - 人間の判断待ち(この間は完全にサスペンド・課金なし)
3. レビュアーが CLI か API でコールバックを送信
4. 処理が再開し、最終結果をまとめて終了

コード量はそれほど多くない。DurableHandler を継承して handleRequest を実装するだけで、SDK がチェックポイントと再実行を全部やってくれる。


環境セットアップ

SDK はまだ Maven Central にない

Developer Preview なので software.amazon.lambda.durable:aws-durable-execution-sdk-java は Maven Central に公開されていない。ソースから自分でビルドする必要がある。

git clone --branch v0.5.0 --depth 1 \
  https://github.com/aws/aws-durable-execution-sdk-java.git

cd aws-durable-execution-sdk-java
mvn install -DskipTests

これでローカルの ~/.m2 にインストールされるので、pom.xml で通常通り参照できる。

<dependency>
    <groupId>software.amazon.lambda.durable</groupId>
    <artifactId>aws-durable-execution-sdk-java</artifactId>
    <version>0.5.0-beta</version>
</dependency>

対応リージョンに注意

2026年2月時点で、Durable Functions が使えるのは一部のリージョンのみ。自分の場合は ap-northeast-3(大阪)で試みて以下のエラーに遭遇した。

Unrecognized field 'DurableConfig' in request. (Status Code: 400)

us-east-1 では通ったので、現時点では US リージョンが中心と見ていい。


ハンドラーの実装

public class ContentReviewHandler extends DurableHandler<ContentReviewRequest, ContentReviewResult> {

    @Override
    public ContentReviewResult handleRequest(ContentReviewRequest request, DurableContext ctx) {

        // Step 1: Bedrock で AI 審査
        AiReviewResult aiResult = ctx.step("ai-review", AiReviewResult.class,
                () -> callBedrock(request.content()));

        if (aiResult.decision() == Decision.APPROVE) {
            return ctx.step("finalize-ai-approve", ContentReviewResult.class, () ->
                    new ContentReviewResult(request.contentId(), "APPROVED", "AI (Claude)",
                            aiResult.reason(), Instant.now().toString()));
        }

        if (aiResult.decision() == Decision.REJECT) {
            return ctx.step("finalize-ai-reject", ContentReviewResult.class, () ->
                    new ContentReviewResult(request.contentId(), "REJECTED", "AI (Claude)",
                            aiResult.reason(), Instant.now().toString()));
        }

        // NEEDS_HUMAN: コールバックを作成して一時停止
        var callbackConfig = CallbackConfig.builder()
                .timeout(Duration.ofDays(7))
                .heartbeatTimeout(Duration.ofDays(1))
                .build();

        var callback = ctx.createCallback("human-approval", String.class, callbackConfig);

        // Step 2: SNS でレビュアーに通知(コールバックIDを本文に含める)
        ctx.step("notify-reviewer", String.class, () -> {
            sns.publish(PublishRequest.builder()
                    .topicArn(SNS_TOPIC_ARN)
                    .message(buildNotificationMessage(request, aiResult, callback.callbackId()))
                    .build());
            return "notified";
        });

        // ここで一時停止。callback.get() は人間の応答が来るまで戻らない
        String humanDecision = callback.get();

        // Step 3: 最終結果をまとめる
        return ctx.step("finalize-human", ContentReviewResult.class, () -> {
            String[] parts = humanDecision.split(":", 3);
            return new ContentReviewResult(
                    request.contentId(),
                    parts[0].equalsIgnoreCase("approved") ? "APPROVED" : "REJECTED",
                    "Human: " + (parts.length > 1 ? parts[1] : "unknown"),
                    parts.length > 2 ? parts[2] : "",
                    Instant.now().toString());
        });
    }
}

コードを読めばフローがそのまま追える。Step Functions の定義 JSON を別で管理する必要はなく、インフラとロジックが一箇所に収まっている。


インフラ定義 (CDK)

DurableConfig は CDK L2 コンストラクトでまだサポートされていないので、escape hatch で追加する。

const reviewFn = new lambda.DockerImageFunction(this, 'ContentReviewFunction', {
    functionName: 'poc-content-review',
    code: lambda.DockerImageCode.fromImageAsset(path.join(__dirname, '../../')),
    role: lambdaRole,
    timeout: cdk.Duration.seconds(900),
    memorySize: 512,
    architecture: lambda.Architecture.X86_64,
    environment: { SNS_TOPIC_ARN: reviewerTopic.topicArn },
});

// DurableConfig は escape hatch で追加
const cfnFn = reviewFn.node.defaultChild as lambda.CfnFunction;
cfnFn.addPropertyOverride('DurableConfig', {
    ExecutionTimeout: 604800,   // 7日間
    RetentionPeriodInDays: 14,
});

Durable Functions のデプロイは Docker コンテナが必須。zip デプロイは使えないので、CDK を使う環境では Docker が必要になる。


IAM の落とし穴

ここが一番詰まった部分で、同じ問題を踏まないよう詳しく残しておく。

アクション名は単数形

AWS ドキュメントや SAM の公式サンプルに lambda:CheckpointDurableExecutions(複数形)と書かれているが、実際に Lambda が要求するアクションは lambda:CheckpointDurableExecution(単数形)。複数形を設定しても権限エラーになる。

not authorized to perform: lambda:CheckpointDurableExecution

エラーメッセージを見ると単数形が要求されているのがわかる。

リソース ARN はワイルドカードが必要

関数 ARN を arn:aws:lambda:REGION:ACCOUNT:function:poc-content-review とそのまま書くと動かない。Durable Execution の実際のリソース ARN は以下の形式になる。

arn:aws:lambda:us-east-1:622202904226:function:poc-content-review:$LATEST/durable-execution/xxxx/yyyy

IAM のリソース指定には末尾に :* をつける必要がある。

lambdaRole.addToPolicy(new iam.PolicyStatement({
    actions: [
        'lambda:CheckpointDurableExecution',   // 単数形
        'lambda:GetDurableExecutionState',
    ],
    resources: [
        `arn:aws:lambda:${this.region}:${this.account}:function:poc-content-review:*`
        //                                                                         ^^ ここ重要
    ],
}));

テスト実行

起動

ExecutionTimeout が15分を超えると同期 invoke できない。非同期で起動する。

aws lambda invoke \
  --function-name 'poc-content-review:$LATEST' \
  --region us-east-1 \
  --invocation-type Event \
  --payload '{"contentId":"test-001","content":"今日の天気はとても良いです。散歩に行きましょう。","submitter":"test-user","reviewerEmail":"reviewer@example.com"}' \
  --cli-binary-format raw-in-base64-out \
  --output json \
  /tmp/response.json

レスポンスに DurableExecutionArn が含まれる。

{
    "StatusCode": 202,
    "DurableExecutionArn": "arn:aws:lambda:us-east-1:622202904226:function:poc-content-review:$LATEST/durable-execution/cc1225ed-.../8af994ed-..."
}

実行状態の確認

aws lambda get-durable-execution \
  --durable-execution-arn "ARN" \
  --region us-east-1

人間承認コールバックの送信

--result は base64 エンコードした JSON を渡す。素の文字列を渡すとデシリアライズエラーになる。

# 承認
aws lambda send-durable-execution-callback-success \
  --region us-east-1 \
  --callback-id <CALLBACK_ID> \
  --result $(echo -n '"approved:レビュアー名"' | base64)

# 拒否
aws lambda send-durable-execution-callback-success \
  --region us-east-1 \
  --callback-id <CALLBACK_ID> \
  --result $(echo -n '"rejected:レビュアー名:拒否理由"' | base64)

実行履歴を見ると何が起きたか一目でわかる

aws lambda get-durable-execution-history \
  --durable-execution-arn "ARN" \
  --region us-east-1

今回の実行では以下のイベントが記録された。

 1  ExecutionStarted
 2  StepStarted          ai-review
 3  StepSucceeded        ai-review         ← Bedrock 呼び出し完了
 4  CallbackStarted      human-approval    ← ここでサスペンド(課金ゼロ)
 5  StepStarted          notify-reviewer
 6  StepSucceeded        notify-reviewer   ← SNS 通知送信
 7  InvocationCompleted                    ← Lambda invocation #1 終了
 8  CallbackSucceeded    human-approval    ← 人間が承認
 9  StepStarted          finalize-human
10  StepSucceeded        finalize-human
11  InvocationCompleted                    ← Lambda invocation #2 終了
12  ExecutionSucceeded                     ← 完了

Lambda の invocation 自体は2回しか走っていない。1回目は Bedrock 呼び出しと SNS 通知まで(約7秒)、2回目は承認を受け取って最終処理(約0.2秒)。その間の「人間承認待ち」は完全に無課金だった。


Step Functions との比較

Step Functions でも同じワークフローは作れる。ただ、実装コストと管理コストが違う。

Step Functions は状態定義を ASL(Amazon States Language)という JSON/YAML で書く。Lambda のコードとは完全に分離するので、ロジックとフローを追いかけるのに2ファイルを行き来することになる。Durable Functions はフロー定義がコードの中にある。

コールバック(人間承認)の仕組みについては、Step Functions の waitForTaskToken パターンと本質的に同じ。ただ Durable Functions の場合、callback.get() の1行で完結する。

Step Functions の方が可視性は高い。コンソールからグラフィカルにフローを確認できるし、再実行の UI もある。Durable Functions は今のところ CLI での操作が基本で、コンソールの対応は限定的。Preview 段階なので今後改善されるはず。

大規模な本番用途や複数チームで共有するワークフローなら Step Functions を選ぶ価値はある。コードで完結させたい、Lambda 関数の延長として書きたいという場面では Durable Functions の方がシンプルにまとまる。


現時点での制約まとめ

  • 対応リージョン: us-east-1 / us-west-2 確認済み。アジアリージョンは現時点で対象外
  • 対応ランタイム: Java 17+ (Preview)。Python・Node.js は SDK ベータ版あり
  • デプロイ形式: Docker コンテナのみ。zip デプロイ不可
  • CDK サポート: L2 コンストラクト未対応。escape hatch で DurableConfig を追加する必要あり
  • Maven Central: 未公開。GitHub からソースビルドが必要

Developer Preview なのでこれらは順次解消されていくと思う。特にリージョン対応が広がれば、本番への適用がかなり現実的になる。


おわりに

実際に動かすまでにいくつか詰まりどころがあったが、動いた後のコードは想像より読みやすい。フロー定義と業務ロジックが同じファイルに収まっているのは、長期的なメンテナンスで効いてくると思う。

コールバックの --result を base64 で渡す、IAM のアクション名は単数形、リソース ARN は :* で終わらせる、この3点さえ最初から知っていれば、セットアップ自体は1時間かからない。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?