今更ですが、前々から興味はあったものの触れたことがなかったので、ハンズオンを通してLambda durable functionsについての理解を深めていきたいと思います。
Lambda durable functionsは2025年12月にGAし、「Lambdaは15分まで」という常識を変えた新機能です。
Lambda durable functionsは日本語表記だと「Lambda永続関数」となります。
強そうですね!!
checkpoint & replay という仕組みで、最長 1 年間動き続けるワークフローを普通の Lambda 関数のコードとして書けます。
このハンズオンでは注文処理を題材に、その仕組みについて実際に手を動かして体感していきたいと思います。
ハンズオン教材は2026年7月時点の最新情報を元にClaude Codeと共同で作成したものとなります。
間違った認識等ありましたらご指摘をお願いいたします。
durable functionsは何を解決するのか?
従来の Lambda関数は1回の呼び出しで最初から最後まで走り切る前提でした。
途中で失敗すれば全部やり直し。処理の途中状態を残したければDynamoDBやS3に自分で保存し、長い待ち時間(人間の承認待ちなど)を挟むならStep Functionsのような外部オーケストレーターが必要でした。
durable functionsでは、ハンドラの中の処理をstep(チェックポイント+自動リトライ付きの処理単位)とwait(コンピュート課金なしの一時停止)で区切るだけで、Lambdaサービス側が進捗の保存・失敗からの復旧・再開をすべて引き受けてくれます。
この一連のライフサイクルを永続実行(durable execution)と呼びます。
replayについて
durable functionsはwait状態から再開する際、Lambda関数は先頭からもう一度処理を実行します。ただし、完了済みのstepは再実行せず、チェックポイントログに保存された結果をその場で返します。これをreplayと呼びます。
裏返すと、コードはreplayしても同じ結果になる必要があります。乱数の生成・現在時刻の取得・API呼び出しのような「実行のたびに結果が変わる処理」は、必ずstepの中に入れる必要があります。stepの中なら初回の結果が保存され、replay時はその値が使われます。
効果的なユースケース
durable functions が特に威力を発揮するのは、以下のどれかを含む処理です。
- 複数ステップ
- 失敗しうる
- 待ち時間がある
- 多段トランザクション処理
- 注文 → 決済 → 在庫引当 → 通知のような処理で、決済まで済んだのに在庫引当で落ちても、決済をやり直さず途中から再開できる。
- 失敗時に補償処理を入れるパターンも組める。
- 人間の承認を待つワークフロー
- callbackでexecutionを一時停止し、承認者の応答が来たら再開する。
- 公式の料金例では保険金請求の処理で「7日間の人間レビュー待ち」を課金ゼロのsuspendで実現している。
- 承認待ちのためだけのポーリングやキューが不要になる。
- 外部ジョブの完了待ち
- wait_for_conditionでバックオフ付きポーリングを宣言的に書ける。
- 動画のエンコード完了待ち、外部APIのジョブステータス監視など、「終わるまで待つ」処理が数行になる。
ハンズオンによる概算コストについて
Lambdaの実行は無料枠(月100万リクエスト+ 40万GB/秒)に収まります。
永続オペレーションの課金も数十回の操作では1円未満です。
DynamoDBはオンデマンドで数回の読み書きのみ。
合計で実質0円(1円未満)ですが、クリーンアップを忘れるとcheckpointデータの保持課金(GB/月、ごく少額)が保持期間(既定14日)まで続きます。
ハンズオンの事前準備
土台となる以下リソースについてはCDKで作成します。
※GitHub上に公開しておりますのでご自由にご利用ください。
- DynamoDBテーブル
- 注文レコードの書き込み先。
- 「完了済みstepがreplayで再実行されない」ことを、実データの変化で確認するために使います。
- IAMロール
- durable executionに必要なcheckpoint + DynamoDB読み書き + ログ出力権限をLambdaに付与します。
CDKで土台をデプロイする
cd cdk
npm install
npx cdk deploy AwsHandsonLambdaDurableFunctionsBaseStack
確認を求められたら「y」を入力します。
完了するとOutputsに2つの値が表示されるので控えてください。
Outputs:
AwsHandsonLambdaDurableFunctionsBaseStack.OrdersTableName = AwsHandsonDurableOrders
AwsHandsonLambdaDurableFunctionsBaseStack.ExecutionRoleArn = arn:aws:iam::123456789012:role/AwsHandsonDurableFunctionRole
確認ポイント
cdk deployがCREATE_COMPLETEで終わり、OutputsにOrdersTableNameとExecutionRoleArnが表示されていること。
STEP1:durable functionを作成する
Lambdaコンソールでdurable executionを有効にした関数を作成します。
リージョンが東京 (ap-northeast-1) であることを最初に確認してください。
1. Lambdaコンソールの関数ページで「関数の作成」を選択
2. 一から作成を選択
3. 関数名に「myDurableOrderFunction」と入力
4. ランタイムで「Python3.14」を選択
5. カスタム設定の永続実行をONにする
6. カスタム実行ロールをONにし、既存のロール選択でAwsHandsonDurableFunctionRoleを選択
7. 「関数を作成」をクリック
checkpointの保存基盤が関数の実行基盤そのものに組み込まれるため、durable executionの設定は関数の作成時にしか指定できず、既存関数への後付け・無効化はできない。
既存のワークロードを移行する場合は、関数の作り直しが必要になる。
確認ポイント
関数の詳細画面にて「永続実行」タブが表示されていることを確認。
通常の関数にはこのタブは表示されない。
補足:2つのタイムアウトの違いについて
実行タイムアウト(既定86,400秒 = 24時間、最大366日)と保持期間(既定14日、最大90日)があります。
これは「execution全体」の設定です。一方、通常の一般設定にある関数タイムアウト(最大15分)は「1回のinvocation」の上限です。executionは複数のinvocationで構成されるため、両者は別物です。
STEP2:注文処理コードを書き、バージョンを発行して実行する
step/waitで注文処理を実装する
「コード」タブに以下プログラムを上書きする。
このプログラムは3step+1wait1の構成で「注文の検証 → 決済(DynamoDBへ書き込み)→ 60秒のwait(外部確認を待つ想定)→ 注文確定」という流れになってます。
import boto3
# Lambdaランタイムに同梱されたDurable Execution SDKから必要な部品を取り込む
from aws_durable_execution_sdk_python import (
DurableContext, # handlerに渡るcontextの型(step/waitが使える)
durable_execution, # handlerを中断・再開できる関数にするデコレータ
durable_step, # 処理を「やり直しても1回だけ実行するstep」にする
)
# waitの待ち時間
from aws_durable_execution_sdk_python.config import Duration
# 書き込み先のDynamoDBテーブル(土台はCDKが作成済み)
table = boto3.resource('dynamodb').Table('AwsHandsonDurableOrders')
# @durable_stepを付けた関数は「step」になる。一度成功すると戻り値が
# checkpointに保存され、replay(handlerの再実行)ではスキップされる
@durable_step
def validate_order(step_context, order_id):
# step_context.loggerはreplayを認識する。再実行では二重に出力されない
step_context.logger.info(f'注文 {order_id} を検証しました')
return {'orderId': order_id, 'status': 'validated'}
@durable_step
def process_payment(step_context, order_id):
step_context.logger.info(f'注文 {order_id} の決済を実行しました')
# DynamoDBへの書き込みをstepの中に置くのがポイント。
# replayでは再実行されないので、二重決済が起きない
table.put_item(Item={'orderId': order_id, 'status': 'paid', 'amount': 9999})
return {'orderId': order_id, 'status': 'paid'}
@durable_step
def confirm_order(step_context, order_id):
step_context.logger.info(f'注文 {order_id} を確定しました')
# 注文のステータスをconfirmedに更新する
table.update_item(
Key={'orderId': order_id},
UpdateExpression='SET #s = :s',
ExpressionAttributeNames={'#s': 'status'},
ExpressionAttributeValues={':s': 'confirmed'},
)
return {'orderId': order_id, 'status': 'confirmed'}
# @durable_executionを付けるとhandlerがdurableになり、
# contextがDurableContextになってstep()/wait()が使えるようになる
@durable_execution
def lambda_handler(event, context: DurableContext):
# テストイベントのJSONから注文IDを取得
order_id = event['orderId']
# context.step(...)でstepを実行。成功すると結果がcheckpointに保存され、
# 次のreplayではこの行を実行せず保存済みの結果を返す
validation = context.step(validate_order(order_id))
payment = context.step(process_payment(order_id))
# 外部の入金確認を待つ想定で60秒suspend(この間コンピュート課金なし)
context.wait(Duration.from_seconds(60))
confirmation = context.step(confirm_order(order_id))
# 3stepの結果をまとめて呼び出し元へ返す
return {
'orderId': order_id,
'status': "completed",
'steps': [validation, payment, confirmation],
}
押さえるべきポイント
@durable_executionがハンドラを包むことで、contextがDurableContextになりstep/waitが使える。
DynamoDBへの書き込みはstepの中にあるため、replay時に再実行されない。(二重決済が起きない)
バージョンを発行してテスト実行する
バージョン発行をする理由
durable functionの呼び出しにはバージョンかエイリアスの付いた修飾ARNの使用が推奨されます。
replayは「executionを開始したときと同じコード」で行われる必要があるため、$LATESTのまま運用してコードを更新すると、実行中のexecutionのreplayが別のコードで走って壊れる恐れがあるからです。
1. 「バージョン」タブで「新しいバージョンを発行」をクリックし、そのまま「発行」
2. 「テスト」タブで「新しいイベントを作成」を選択し、イベント名に「order1」と入力
3. 「イベントJSON」を以下に置き換える
{
"orderId": "order-001"
}
4. 「保存」後、「実行」をクリック
60秒のwaitが入っているため、最初のinvocationはすぐ終了し、関数はsuspend状態に入ります。裏で何が起きているかは次のステップで観察します。
確認ポイント
実行がエラーにならず開始される。
wait中のため、すぐに最終結果が返らないのは正常。
STEP3:「永続実行」タブとCloudWatch Logsでreplayを観察する
3つのパターンでdurable executionの動きを確認します。
「永続実行」タブ
1. 「永続実行」タブを開く
2. 実行中のexecutionが「実行中」で表示される。60秒経過後に再読み込みすると「成功しました」に変わる
3. execution IDを選び、詳細を確認する
- 各stepの完了タイミングを示すタイムライン
- checkpointの履歴
- waitの期間
- stepの戻り値
CloudWatch Logs
「モニタリング」タブにて「CloudWatchログを表示」をクリックして、最新のログストリームを確認する
DynamoDB
確認ポイント
1. ログにinvocationが2回記録されている(waitの前後で別のinvocation)。
2. 2回目のinvocationで「検証しました」「決済を実行しました」のログが再出力されていない(stepはreplayでスキップされ、step_context.loggerはreplay-awareなため二重に出ない)。
3. DynamoDBのitemが1件でstatusがconfirmedとなっている(決済のput_itemが2回走っていない=再実行されていない)。
STEP4:わざと失敗させて自動リトライをする
stepの例外が自動リトライされる様子を見る
stepは失敗するとSDKのリトライポリシーに従って自動で再試行されます。
(従来ならtry/exceptとバックオフを自前で書いていた部分です。)
決済stepの直後に、必ず失敗する在庫引当stepを追加して観察してみます。
「コード」タブで、confirm_orderの定義の後に次のstepを追加します。
@durable_step
def reserve_stock(step_context, order_id):
step_context.logger.info(f'注文 {order_id} の在庫引当を試行します')
raise RuntimeError('在庫サービスに一時的に接続できません')
そしてハンドラ内のcontext.wait(...)の行を、このstepの呼び出しに置き換えます。
(waitを待つ時間を省くため)
# context.wait(Duration.from_seconds(60))を置き換え
stock = context.step(reserve_stock(order_id))
Deployして、以下の新しいテストイベントで実行し、
「永続実行」タブで該当executionの詳細を確認します。
{
"orderId": "order-002"
}
確認ポイント
1. reserve_stock stepが失敗と再試行を繰り返している(試行回数が増えていく)のが永続オペレーションの詳細で見える。
2. その間もvalidate_orderとprocess_paymentは再実行されていない。リトライのたびにハンドラはreplayされるが、完了済みstepはスキップされる。
3. リトライ上限に達するとexecutionは「失敗しました」となる。
STEP5:callbackで人間の承認を待つ
wait_for_callback + CLI承認でexecutionを再開する
最後はdurable functionsの代表的ユースケース、human-in-the-loopです。
wait_for_callbackはcallback IDを発行してexecutionをsuspendし、外部からSendDurableExecutionCallbackSuccess(承認)またはSendDurableExecutionCallbackFailure(却下)APIが呼ばれるまで待ちます。
suspend中はコンピュート課金ゼロなので、承認待ちが7日間でも費用は発生しません。
ハンドラを次のコードに置き換えてDeployします。
(validate_orderなど既存のstep定義はそのまま残す)
from aws_durable_execution_sdk_python.types import WaitForCallbackContext
@durable_execution
def lambda_handler(event, context: DurableContext):
order_id = event['orderId']
validation = context.step(validate_order(order_id))
payment = context.step(process_payment(order_id))
def submit(callback_id: str, ctx: WaitForCallbackContext) -> None:
#実運用ではメールやSlackへcallback_idを送る。
#今回はログに出して、自身が承認者としてCLIから応答する
print(f'承認待ちです。callback_id = {callback_id}')
approval = context.wait_for_callback(submitter=submit, name='wait-for-approval')
confirmation = context.step(confirm_order(order_id))
return {
'orderId': order_id,
'approval': approval,
'status': 'completed',
}
以下のテストイベントで実行すると、executionは承認待ちでsuspendします。
callback IDを取得して承認を送ります。
{
"orderId": "order-003"
}
1. 「永続実行」タブで実行中(Running)のexecutionを開く。
2. 永続オペレーションの一覧にwait-for-approvalのcallbackが見える。
callback IDをコピーする。(CloudWatch Logsの「承認待ちです」の行からも取得できる)
3. ターミナルから承認を送信する。
aws lambda send-durable-execution-callback-success \
--callback-id "コピーしたcallback ID" \
--cli-binary-format raw-in-base64-out \
--result '{"approvedBy": "you", "decision": "approved"}' \
--region ap-northeast-1
確認ポイント
コマンド送信の数秒後、executionが再開して「成功しました」になる。
executionの結果にapprovalとしてCLIで送ったJSONが入っており、DynamoDBのorder-003がconfirmedになっている。
クリーンアップ
手動で作ったリソースはCDKでは消えません。
必ず「手動で作ったもの → CDK」の順で削除します。
Lambda関数の削除(手動作成分)
Lambdaコンソールの関数ページから「myDurableOrderFunction」を選択し、アクションから削除を選択。確認が求められるので「確認」を入力し削除をする。
ロググループの削除
CloudWatchコンソールのログ管理で「/aws/lambda/myDurableOrderFunction」をフィルタリングおよび選択する。その後、アクションからロググループの削除を選択。
確認が求められるので、「削除」をクリックする。
CDK土台の削除
cd cdk
npx cdk destroy AwsHandsonLambdaDurableFunctionsBaseStack
確認ポイント
Lambdaの関数一覧・CloudWatchのロググループ・CloudFormationのスタック一覧のいずれからも、このハンズオンのリソースが消えていることを確認。
今回のハンズオンを通しての感想
今回は人間の承認を待つワークフローを題材にdurable functionsを操作してみました。
durable functions特有の用語や基本的な使い方を学ぶことができました。
他のユースケースの実践や今回出てこなかったparallel()やmap()などを使った並列実行なども試してみてdurable functionsの理解を深めていければと思います。




















