はじめに
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時間かからない。