はじめに

こんにちは、サイトーです。

AWS re:Invent 2025で発表された「AWS Lambda Durable Functions(以下Lambda Durable Functions)」を触ってみましたこれまでワークフローを実装する際はStep Functionsを使うのが定石でしたが、ASL(Amazon States Language)の学習コストや、既存のLambdaとの統合の難しさに課題を感じることもありました。Lambda Durable Functionsは、普段使っている言語でワークフローが書けるという新しいアプローチです。本記事では、実際に注文処理ワークフローを実装し、Step Functionsとの比較や使い分けのポイントを整理していきます。

Lambda Durable Functionsとは

主な機能

・最長1年間の実行が可能
・Wait中は課金されない(コスト効率的な待機)
・Callback外部イベント(人間の承認など)を待つ
・自動的なチェックポイント失敗時の自動リトライ
parallelで並列実行、mapで並列処理が可能

また通常のLambdaと異なり、context.stepを使用することで各ステップがチェックポイントとして記録され、障害発生時はそこから再開できます。
# 通常のLambda
def lambda_handler(event, context):
    result = process_order(event)
    return result
# Lambda Durable Functions
@durable_execution
def lambda_handler(event, context: DurableContext):
    # ステップごとにチェックポイント作成
    result = context.step(process_order(event))
    return result

Step Functionsとの比較

Lambda Durable Functions Step Functions
記述方式 Python/JS/TS JSON(ASL)
学習コスト ✅低(普段の言語) △中〜高
動的フロー ✅実行時に分岐決定可能 △事前定義が必要
可視化 △実行履歴のみ ✅ビジュアルエディタ
AWS統合 Lambda経由 ✅200以上のサービスと直接統合
最大実行時間 1年 Standard1年、Express5分
成熟度 △2025/12〜(新規) ✅2016〜(実績多数)
リージョン △US-East-2のみ(2025/12時点) ✅全リージョン
強み
通常のif文で分岐できる
コードレビューしやすい、IDEの補完が効く
ビジュアルで全体像の把握が容易
非エンジニアにも説明しやすい
実績が豊富

Lambda Durable Functionsを選ぶべきケース

・既存Lambdaを活かしたい
・ASLの学習コストを避けたい
・実行時データで動的に分岐したい
・コードレビューを容易にしたい
・単一Lambda内で完結する処理

Step Functionsを選ぶべきケース

・複雑な並列、分岐処理
・ビジュアル化、監視が必要
・AWS サービスと直接統合したい
・非エンジニアへの説明が必要
・実績を重視

実装比較

簡単な承認フローを両方で実装すると、以下のような違いがあります。

Lambda Durable Functions(Python)
@durable_execution
def lambda_handler(event, context: DurableContext):
    # バリデーション
    validated = context.step(validate(event))
   
    # 金額による動的分岐(実行時に決定!)
    if validated['amount'] > 100000:
        # 高額注文は承認フロー
        callback = context.create_callback("approval")
        context.step(send_approval(callback.callback_id))
        approval = callback.result()  # 待機
       
        if not approval['approved']:
            return {"status": "rejected"}
   
    # 処理実行
    result = context.step(process(validated))
    return result
Step Functions(Json(ASL))
{
  "States": {
    "Validate": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:...:validate",
      "Next": "CheckAmount"
    },
    "CheckAmount": {
      "Type": "Choice",
      "Choices": [{
        "Variable": "$.amount",
        "NumericGreaterThan": 100000,
        "Next": "SendApproval"
      }],
      "Default": "Process"
    },
    "SendApproval": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke.waitForTaskToken",
      "Next": "Process"
    },
    "Process": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:...:process",
      "End": true
    }
  }
}

実装してみた 注文処理ワークフロー

以下のような注文処理ワークフローを実装します。

1. 注文確認
2. 在庫確認
3. 承認待ち(100,000円超の場合のみ)←Wait機能
4. 決済処理
5. 配送手配
import json
import logging
import random
from datetime import datetime, timezone
from typing import Dict, Any

from aws_durable_execution_sdk_python import (
    DurableContext,
    StepContext,
    durable_execution,
    durable_step,
)
from aws_durable_execution_sdk_python.config import (
    Duration,
    StepConfig,
    CallbackConfig,
)

logger = logging.getLogger()
logger.setLevel(logging.INFO)

# 各処理を@durable_stepデコレータで修飾します
@durable_step
def validate_order(step_context: StepContext, order_data: Dict[str, Any]) -> Dict[str, Any]:
    """注文確認"""
    step_context.logger.info(f"Validating order: {order_data.get('order_id')}")
   
    required_fields = ['order_id', 'customer_id', 'items', 'total_amount']
    for field in required_fields:
        if field not in order_data:
            raise ValueError(f"Missing required field: {field}")
   
    if order_data['total_amount'] <= 0:
        raise ValueError("Invalid total amount")
   
    step_context.logger.info("Order validation passed")
   
    return {
        "order_id": order_data['order_id'],
        "customer_id": order_data['customer_id'],
        "total_amount": order_data['total_amount'],
        "status": "validated",
        "validated_at": datetime.now(timezone.utc).isoformat()
    }

@durable_step
def check_inventory(step_context: StepContext, order_data: Dict[str, Any]) -> Dict[str, Any]:
    """在庫確認"""
    step_context.logger.info(f"Checking inventory for order: {order_data['order_id']}")
   
    items = order_data.get('items', [])
    reservation_id = f"RSV-{order_data['order_id']}-{datetime.now(timezone.utc).timestamp()}"
   
    step_context.logger.info(f"Inventory available. Reservation ID: {reservation_id}")
   
    return {
        "order_id": order_data['order_id'],
        "reservation_id": reservation_id,
        "items_reserved": len(items),
        "status": "reserved"
    }

@durable_step
def send_approval_request(
    step_context: StepContext,
    callback_id: str,
    order_data: Dict[str, Any]
) -> Dict[str, Any]:
    """承認リクエストの送信"""
    step_context.logger.info(f"Sending approval request for order: {order_data['order_id']}")
    step_context.logger.info(f"Callback ID: {callback_id}")
   
    step_context.logger.info("Approval request sent successfully")
   
    return {
        "order_id": order_data['order_id'],
        "callback_id": callback_id,
        "status": "awaiting_approval",
        "requested_at": datetime.now(timezone.utc).isoformat()
    }

@durable_step
def process_payment(step_context: StepContext, order_data: Dict[str, Any]) -> Dict[str, Any]:
    """決済処理"""
    step_context.logger.info(f"Processing payment for order: {order_data['order_id']}")
   
    # 不安定なAPI呼び出しを想定
    if random.random() > 0.95:
        step_context.logger.warning("Payment processing failed (simulated), will retry")
        raise Exception("Payment gateway timeout")
   
    transaction_id = f"TXN-{order_data['order_id']}-{datetime.now(timezone.utc).timestamp()}"
   
    step_context.logger.info(f"Payment processed. Transaction ID: {transaction_id}")
   
    return {
        "order_id": order_data['order_id'],
        "transaction_id": transaction_id,
        "amount": order_data['total_amount'],
        "status": "paid",
        "paid_at": datetime.now(timezone.utc).isoformat()
    }
@durable_step
def arrange_shipment(step_context: StepContext, order_data: Dict[str, Any]) -> Dict[str, Any]:
    """配送手配"""
    step_context.logger.info(f"Arranging shipment for order: {order_data['order_id']}")
   
    tracking_number = f"TRACK-{order_data['order_id']}-{datetime.now(timezone.utc).timestamp()}"
   
    step_context.logger.info(f"Shipment arranged. Tracking: {tracking_number}")
   
    return {
        "order_id": order_data['order_id'],
        "tracking_number": tracking_number,
        "status": "shipped",
        "shipped_at": datetime.now(timezone.utc).isoformat()
    }

# メインワークフローを@durable_executionデコレータで修飾。長時間実行とコールバック機能が可能に
@durable_execution
def lambda_handler(event: Dict[str, Any], context: DurableContext) -> Dict[str, Any]:
    """注文処理のメインワークフロー"""
    try:
        context.logger.info("=== Order Processing Workflow Started ===")
        context.logger.info(f"Event: {json.dumps(event)}")
       
        order_data = event.get('order', {})
        if not order_data:
            raise ValueError("Missing 'order' in event")
       
        # Step 1: 注文確認
        validated_order = context.step(validate_order(order_data))
       
        if validated_order["status"] != "validated":
            raise Exception("Order validation failed")
       
        context.logger.info(f"Order validated: {validated_order}")
       
        # Step 2: 在庫確認
        inventory_result = context.step(check_inventory(validated_order))
        context.logger.info(f"Inventory reserved: {inventory_result}")

        # Step 3: 高額注文の場合は承認待ち
        # Lambda Durable Functionsの核心機能!外部イベント(承認)を待ちます
        if validated_order["total_amount"] > 100000:
            context.logger.info("High-value order detected, requesting approval...")


            # コールバックトークンを作成
            callback = context.create_callback(
                name="order-approval",
                config=CallbackConfig(timeout=Duration.from_hours(48))
            )
            context.logger.info(f"Callback created with ID: {callback.callback_id}")

            # 承認依頼を送信
            approval_request = context.step(
                send_approval_request(callback.callback_id, validated_order)
            )
            context.logger.info(f"Approval request sent: {approval_request}")

            # 最重要!この処理で実行が一時停止し、承認を待つ
            # Lambdaは終了するが状態は保持され、外部から承認が来るまで最大48時間待機する
            context.logger.info("Waiting for approval callback...")
            approval_result = callback.result()

            # 承認後、処理が再開
            context.logger.info(f"Approval received: {approval_result}")
           
            if not approval_result.get("approved", False):
                context.logger.warning("Order was rejected")
                return {
                    "status": "rejected",
                    "order_id": validated_order["order_id"],
                    "reason": approval_result.get("reason", "Not approved")
                }
        else:
            context.logger.info("Standard order, no approval needed")
       
        # Step 4: 決済処理
        context.logger.info("Processing payment...")

        payment_result = context.step(process_payment(validated_order))

        if payment_result["status"] != "paid":
            raise Exception("Payment processing failed after retries")

        context.logger.info(f"Payment successful: {payment_result}")
       
        # Step 5: 配送手配
        shipment_result = context.step(arrange_shipment(validated_order))
        context.logger.info(f"Shipment arranged: {shipment_result}")
       
        # 完了
        final_result = {
            "status": "completed",
            "order_id": validated_order["order_id"],
            "transaction_id": payment_result["transaction_id"],
            "tracking_number": shipment_result["tracking_number"],
            "completed_at": datetime.now(timezone.utc).isoformat()
        }
       
        context.logger.info("=== Order Processing Workflow Completed ===")
        context.logger.info(f"Final result: {json.dumps(final_result)}")
       
        return final_result
       
    except Exception as error:
        context.logger.error(f"Workflow failed: {str(error)}")
       
        return {
            "status": "failed",
            "error": str(error),
            "order_id": event.get('order', {}).get('order_id', 'unknown'),
            "failed_at": datetime.now(timezone.utc).isoformat()
        }
Lambda Durable Functionsは現時点で、US-East-2のみ使用できますのでオハイオリージョンでLambda関数を作成します。コンソールを確認するとDurable executionを有効化できます。
少額注文と高額注文、2つのテストを作成します。少額注文テストでは、Lambda Durable Functionsの基本的な動作を確認します。100,000円以下の注文は承認プロセスをスキップし、注文確認、在庫確認、決済処理、配送手配の全ステップが自動的に実行されます。各処理は@durable_stepデコレータによって自動的にチェックポイントとして保存され、障害時には途中から再実行できます。
# 少額注文
{
  "order": {
    "order_id": "ORD-2025-004",
    "customer_id": "CUST-12345",
    "items": [
      {
        "product_id": "PROD-A",
        "quantity": 2,
        "price": 5000
      }
    ],
    "total_amount": 25000,
    "currency": "JPY"
  }
}
テスト実行後、永続実行のタブを確認します。少額のため承認は必要なく、瞬時に実行ステータスが成功になりました。
CloudWatchでログを確認します。
CloudWatchのログを見やすく転記したものを下記に表示します(重要でない処理のログは省略しています)。
「ワークフロー開始→ 注文確認→承認(少額注文なのでスキップ)→完了」がLambdaの初期化時間を含め、約2.4秒で処理されているのが確認できます。
# Lambda初期化
{
    "time": "2025-12-12T09:47:40.432Z",
    "type": "platform.initStart",
    "record": {
        "initializationType": "on-demand",
        "phase": "init",
        "runtimeVersion": "python:3.13.DurableFunction.v6",
        "runtimeVersionArn": "arn:aws:lambda:us-east-2::...",
        "functionName": "durable-order-processing",
        "functionVersion": "$LATEST",
        "instanceId": "2025/12/12/[$LATEST]1c19f9987529483484899cdb3e559f85",
        "instanceMaxMemory": 536870912
    }
}
# ワークフロー開始
{
"timestamp": "2025-12-12T09:47:41Z",
"level": "INFO",
"message": "=== Order Processing Workflow Started ===",
"logger": "root",
"requestId": "142df71e-3238-4cf7-bb88-ec6ff5eb0f63",
"executionArn": "arn:aws:lambda:us-east-2:************:function:durable-order-processing:..."
}
# 注文確認
{
"timestamp": "2025-12-12T09:47:41Z",
"level": "INFO",
"message": "Validating order: ORD-2025-004",
"logger": "root",
"requestId": "142df71e-3238-4cf7-bb88-ec6ff5eb0f63",
"executionArn": "arn:aws:lambda:us-east-2:************:function:durable-order-processing:..."
"operationName": "validate_order",
"attempt": 1,
"operationId": "1ced8f5be2db23a6513eba4d819c73806424748a7bc6fa0d792cc1c7d1775a97"
}
# 承認(少額注文なので承認がスキップされています)
{
"timestamp": "2025-12-12T09:47:42Z",
"level": "INFO",
"message": "Standard order, no approval needed",
"logger": "root",
"requestId": "142df71e-3238-4cf7-bb88-ec6ff5eb0f63",
"executionArn": "arn:aws:lambda:us-east-2:************:function:durable-order-processing:..."
}
# 完了
{
"timestamp": "2025-12-12T09:47:42Z",
"level": "INFO",
"message": "=== Order Processing Workflow Completed ===",
"logger": "root",
"requestId": "142df71e-3238-4cf7-bb88-ec6ff5eb0f63",
"executionArn": "arn:aws:lambda:us-east-2:************:function:durable-order-processing:..."
}
# レポート(課金対象の実行時間が2.338秒であることが確認できる)
{
  "time": "2025-12-12T09:47:42.774Z",
  "type": "platform.report",
  "record": {
      "requestId": "142df71e-3238-4cf7-bb88-ec6ff5eb0f63",
      "metrics": {
          "durationMs": 1447.982,
          "billedDurationMs": 2338,
          "memorySizeMB": 512,
          "maxMemoryUsedMB": 89,
          "initDurationMs": 889.95
      },
      "status": "success"
    }
}
次に高額注文テストを行います。Lambda Durable Functionsの真価を発揮する場面で、150,000円の注文では承認が必要となり、ワークフローは最大48時間停止します(今回はデフォルトの48時間にしましたがMAX1年いけるようです)。この間、従来のLambda関数であれば15分のタイムアウト制限により実現不可能でしたが、Lambda Durable Functionsでは外部システムからの承認を待機し続けることができます。
# 高額注文
{
  "order": {
    "order_id": "ORD-2025-006",
    "customer_id": "CUST-67890",
    "items": [
      {
        "product_id": "PROD-PREMIUM",
        "quantity": 1,
        "price": 150000
      }
    ],
    "total_amount": 150000,
    "currency": "JPY"
  }
}
テスト実行後、高額注文のため承認が必要となります。そのためステータスが成功にはならず実行中のままとなっています。最大48時間CALLBACK_STARTED(コールバック待機中)となりますが、このLambda実行料金は発生しません(データ保持料金は除く)。
詳細なログを確認します(重要でない処理のログは省略しています)。
$ Lambda実行開始
{
"timestamp": "2025-12-12T09:53:58Z",
"level": "INFO",
"message": "=== Order Processing Workflow Started ===",
"logger": "root",
"requestId": "e8dac2cd-7b94-45ac-8e73-2882e3243387",
"executionArn": "arn:aws:lambda:us-east-2:************:function:durable-order-processing:..."
}
# 高額注文検出!注文金額が¥100,000を超えたため、承認待ちへ
{
"timestamp": "2025-12-12T09:53:58Z",
"level": "INFO",
"message": "High-value order detected, requesting approval...",
"logger": "root",
"requestId": "e8dac2cd-7b94-45ac-8e73-2882e3243387",
"executionArn": "arn:aws:lambda:us-east-2:************:function:durable-order-processing:..."
}
# コールバックID作成
{
"timestamp": "2025-12-12T09:53:58Z",
"level": "INFO",
"message": "Callback created with ID: Ab9h...kM/8",
"logger": "root",
"requestId": "e8dac2cd-7b94-45ac-8e73-2882e3243387",
"executionArn": "arn:aws:lambda:us-east-2:************:function:durable-order-processing:..."
}
コールバックID作成後、承認依頼→コールバックIDの確認→送信成否がありますが省略します。
 
そしてLambda Durable Functionsの核心でもあるWait機能のログです。ワークフローは CALLBACK_STARTED状態で保存され、承認を待ちます。
# Wait状態へ
{
"timestamp": "2025-12-12T09:53:59Z",
"level": "INFO",
"message": "Waiting for approval callback...",
"logger": "root",
"requestId": "e8dac2cd-7b94-45ac-8e73-2882e3243387",
"executionArn": "arn:aws:lambda:us-east-2:************:function:durable-order-processing:..."
}
# Lambdaの実行はここで一度終了されます。
{
    "time": "2025-12-12T09:53:59.164Z",
    "type": "platform.report",
    "record": {
        "requestId": "e8dac2cd-7b94-45ac-8e73-2882e3243387",
        "metrics": {
            "durationMs": 1003.52,
            "billedDurationMs": 1004,
            "memorySizeMB": 512,
            "maxMemoryUsedMB": 91
        },
        "status": "success"
    }
}
別途承認を行うとステータスが成功に変わったのがコンソールで確認できます。

ログを確認すると、Lambdaが再び実行されていたのが確認できます。Wait状態から再開時、初期化及び新しいrequestIdが割り当てられ、別の呼び出しとして処理されます。重要なのは、Wait中にLambda実行が終了するため、個々のLambda実行は15分制限内に収まるが、ワークフロー全体としては15分を大幅に超える実行期間が実現されています。また、instanceIdが変わっていることから仮想的な初期化ではなくLambda実行環境も変わっていることが確認できました。

# # Lambda初期化開始
{
    "time": "2025-12-12T10:08:44.725Z",
    "type": "platform.initStart",
    "record": {
        "initializationType": "on-demand",
        "phase": "init",
        "runtimeVersion": "python:3.13.DurableFunction.v6",
        "runtimeVersionArn": "arn:aws:lambda:us-east-2::runtime:d4fbf...",
        "functionName": "durable-order-processing",
        "functionVersion": "$LATEST",
        "instanceId": "2025/12/12/[$LATEST]1c6a3a1dc9f949ef82e680da103e9eea",
        "instanceMaxMemory": 536870912
    }
}
# requestIdが変わっているのを確認できます
{
    "time": "2025-12-12T10:08:45.580Z",
    "type": "platform.start",
    "record": {
        "requestId": "823e5765-e3dc-42e8-b1d5-ff2a4b2ce5da",
        "functionArn": "arn:aws:lambda:us-east-2:************:...
        "version": "$LATEST"
    }
}

承認を受信したことでcallback.resultが値を返し、Approval receivedが出力され、Waitが終了したことが確認できました。

# 承認データ受信
{
"timestamp": "2025-12-12T10:08:45Z",
"level": "INFO",
"message": "Approval received: {\"approved\": true}",
"logger": "root",
"requestId": "823e5765-e3dc-42e8-b1d5-ff2a4b2ce5da",
"executionArn": "arn:aws:lambda:us-east-2:************:function:durable-order-processing:..."
}
# 実行レポート
{
    "time": "2025-12-12T10:08:46.098Z",
    "type": "platform.report",
    "record": {
        "requestId": "823e5765-e3dc-42e8-b1d5-ff2a4b2ce5da",
        "metrics": {
            "durationMs": 518.101,
            "billedDurationMs": 1370,
            "memorySizeMB": 512,
            "maxMemoryUsedMB": 87,
            "initDurationMs": 851.443
        },
        "status": "success"
    }
}

料金について(2025年12月 時点)

料金に関して詳細な説明がされているサイトがあまりありませんでしたので、詳しく調べてみました。情報は現時点のものであり、ご利用の際にはAWS公式サイト*1の確認をお願いします。また下記はx86_64 アーキテクチャを使用した場合であり、arm64アーキテクチャを使用した場合、料金が異なります。

・Lambda基本料金(US-East-2)

料金項目 単価 無料枠
リクエスト料金(最初の60億GB秒/月) $0.20/100万リクエスト 100万リクエスト/月 *2
実行時間料金(最初の60億GB秒/月) $0.0000166667/GB秒 40万GB秒/月

また別途実行時間料金がかかります。実行時間料金は、Lambda関数に割り当てるメモリ量によって異なり、デフォルトの128MBだと最初の60億GB秒/月までは1ミリ秒あたり0.0000000021ドルとなっています。

・Lambda Durable Functions 追加料金(US-East-2)

料金項目 単価
Durable Operations(ステップ、待機、コールバックなど) $8/100万オペレーション
データ書き込み $0.25/GB
データ保持 1か月あたり$0.15/GB

ここで注意したいのがWait中、Lambda実行料金はかからないがデータ保持料金はかかるという点です。 個人で使用する分には正直無視してよい金額ですが大規模システムだと、ある程度かかってくるようです。 AWS公式サイト*1に利用ケース例が載っていますので、詳細はそちらをチャックしてみてください。

*1 英語表示にする必要あり。日本語表示だとLambda Durable Functionsの項目が表示されない場合があります。
*2 無料利用枠には、リクエストあたり最初の6MBを超える分のHTTPレスポンスストリーミング(無料)が月間100GiBまで含まれています。

まとめ

Lambda Durable Functionsを実際に触ってみました。PythonやTypeScriptでワークフローを書けるという点がStep Functionsにはなく新しい選択肢になり得ると感じました。現時点(2025年12月)ではGA直後で実績が少なく使用できるリージョンにも制限があり、本番導入は慎重にならざるを得ません。しかし、慣れ親しんだ言語で書けるという開発体験の良さは魅力的で、今後のリージョン拡大が待ち遠しいです。まずは開発環境で試してみて、自分のユースケースに合うか確認してみることをおすすめします。

参考

AWS公式ドキュメント
AWS公式サイト
AWS公式ブログ
SDK (Python)
SDK (JavaScript)