Lambda の耐久性のある関数におけるベストプラクティス - AWS Lambda

Lambda の耐久性のある関数におけるベストプラクティス

耐久性のある関数には、従来の Lambda 関数とは異なるパターンを必要とする再生ベースの実行モデルが使用されます。信頼性が高く、費用対効果の高いワークフローを構築するには、以下のベストプラクティスに従ってください。

決定的コードを記述する

再生中、関数は最初から実行され、元の実行と同じ実行パスに従う必要があります。耐久性のあるオペレーション以外のコードは決定的で、同じ入力で同じ結果が生成される必要があります。

非決定的なオペレーションをステップでラップします

  • ランダム数値生成と UUID

  • 現在の時刻またはタイムスタンプ

  • 外部 API コールとデータベースクエリ

  • ファイルシステムの操作

TypeScript
import { withDurableExecution, DurableContext } from '@aws/durable-execution-sdk-js'; import { randomUUID } from 'crypto'; export const handler = withDurableExecution( async (event: any, context: DurableContext) => { // Generate transaction ID inside a step const transactionId = await context.step('generate-transaction-id', async () => { return randomUUID(); }); // Use the same ID throughout execution, even during replay const payment = await context.step('process-payment', async () => { return processPayment(event.amount, transactionId); }); return { statusCode: 200, transactionId, payment }; } );
Python
from aws_durable_execution_sdk_python import durable_execution, DurableContext import uuid @durable_execution def handler(event, context: DurableContext): # Generate transaction ID inside a step transaction_id = context.step( lambda _: str(uuid.uuid4()), name='generate-transaction-id' ) # Use the same ID throughout execution, even during replay payment = context.step( lambda _: process_payment(event['amount'], transaction_id), name='process-payment' ) return {'statusCode': 200, 'transactionId': transaction_id, 'payment': payment}
重要

グローバル変数やクロージャを使用して、ステップ間の状態を共有しないでください。戻り値を通じてデータを渡します。ステップはキャッシュされた結果を返しますが、グローバル変数がリセットされるため、再生中にグローバル状態が中断されます。

クロージャミューテーションを避ける: クロージャでキャプチャされた変数は、再生中にミューテーションを失う可能性があります。ステップはキャッシュされた結果を返しますが、ステップ以外の変数更新は再生されません。

TypeScript
// ❌ WRONG: Mutations lost on replay export const handler = withDurableExecution(async (event, context) => { let total = 0; for (const item of items) { await context.step(async () => { total += item.price; // ⚠️ Mutation lost on replay! return saveItem(item); }); } return { total }; // Inconsistent value! }); // ✅ CORRECT: Accumulate with return values export const handler = withDurableExecution(async (event, context) => { let total = 0; for (const item of items) { total = await context.step(async () => { const newTotal = total + item.price; await saveItem(item); return newTotal; // Return updated value }); } return { total }; // Consistent! }); // ✅ EVEN BETTER: Use map for parallel processing export const handler = withDurableExecution(async (event, context) => { const results = await context.map( items, async (ctx, item) => { await ctx.step(async () => saveItem(item)); return item.price; } ); const total = results.getResults().reduce((sum, price) => sum + price, 0); return { total }; });
Python
# ❌ WRONG: Mutations lost on replay @durable_execution def handler(event, context: DurableContext): total = 0 for item in items: context.step( lambda _: save_item_and_mutate(item, total), # ⚠️ Mutation lost on replay! name=f'save-item-{item["id"]}' ) return {'total': total} # Inconsistent value! # ✅ CORRECT: Accumulate with return values @durable_execution def handler(event, context: DurableContext): total = 0 for item in items: total = context.step( lambda _: save_item_and_return_total(item, total), name=f'save-item-{item["id"]}' ) return {'total': total} # Consistent! # ✅ EVEN BETTER: Use map for parallel processing @durable_execution def handler(event, context: DurableContext): def process_item(ctx, item): ctx.step(lambda _: save_item(item)) return item['price'] results = context.map(items, process_item) total = sum(results.get_results()) return {'total': total}

べき等性用の設計

再試行や再生により、オペレーションが複数回実行される場合があります。非べき等性オペレーションでは副作用が重複します (お客様に 2 回請求したり、複数の E メールを送信したりするなど)。

べき等性トークンの使用: ステップ内でトークンを生成し、オペレーションの重複を防止するため、外部 API コールを使用して含めます。

TypeScript
import { withDurableExecution, DurableContext } from '@aws/durable-execution-sdk-js'; export const handler = withDurableExecution( async (event: any, context: DurableContext) => { // Generate idempotency token once const idempotencyToken = await context.step('generate-idempotency-token', async () => { return crypto.randomUUID(); }); // Use token to prevent duplicate charges const charge = await context.step('charge-payment', async () => { return paymentService.charge({ amount: event.amount, cardToken: event.cardToken, idempotencyKey: idempotencyToken }); }); return { statusCode: 200, charge }; } );
Python
from aws_durable_execution_sdk_python import durable_execution, DurableContext import uuid @durable_execution def handler(event, context: DurableContext): # Generate idempotency token once idempotency_token = context.step( lambda _: str(uuid.uuid4()), name='generate-idempotency-token' ) # Use token to prevent duplicate charges def charge_payment(_): return payment_service.charge( amount=event['amount'], card_token=event['cardToken'], idempotency_key=idempotency_token ) charge = context.step(charge_payment, name='charge-payment') return {'statusCode': 200, 'charge': charge}

「at-most-once」セマンティックの使用: 重複してはならない重要なオペレーション (金融トランザクション、インベントリ控除) には、「at-most-once」の実行モードを設定します。

TypeScript
// Critical operation that must not duplicate await context.step('deduct-inventory', async () => { return inventoryService.deduct(event.productId, event.quantity); }, { executionMode: 'AT_MOST_ONCE_PER_RETRY' });
Python
# Critical operation that must not duplicate context.step( lambda _: inventory_service.deduct(event['productId'], event['quantity']), name='deduct-inventory', config=StepConfig(execution_mode='AT_MOST_ONCE_PER_RETRY') )

データベースのべき等性: レコードの重複を防止するめ、「check-before-write」パターン、条件付き更新、アップサートオペレーションを使用してください。

状態を効率的に管理する

すべてのチェックポイントにより、状態を永続的ストレージに保存されます。大規模な状態オブジェクトはコストが増加し、チェックポイント処理が遅くなり、パフォーマンスに影響します。重要なワークフローの調整データのみを保存します。

状態を最小限に維持します

  • 完全なオブジェクトではなく、ID およびリファレンスを保存する

  • 必要に応じてステップ内で詳細データを取得する

  • 大規模なデータに Amazon S3 または DynamoDB を使用し、状態でリファレンスを渡す

  • ステップ間で大規模なペイロードを渡すことを避ける

TypeScript
import { withDurableExecution, DurableContext } from '@aws/durable-execution-sdk-js'; export const handler = withDurableExecution( async (event: any, context: DurableContext) => { // Store only the order ID, not the full order object const orderId = event.orderId; // Fetch data within each step as needed await context.step('validate-order', async () => { const order = await orderService.getOrder(orderId); return validateOrder(order); }); await context.step('process-payment', async () => { const order = await orderService.getOrder(orderId); return processPayment(order); }); return { statusCode: 200, orderId }; } );
Python
from aws_durable_execution_sdk_python import durable_execution, DurableContext @durable_execution def handler(event, context: DurableContext): # Store only the order ID, not the full order object order_id = event['orderId'] # Fetch data within each step as needed context.step( lambda _: validate_order(order_service.get_order(order_id)), name='validate-order' ) context.step( lambda _: process_payment(order_service.get_order(order_id)), name='process-payment' ) return {'statusCode': 200, 'orderId': order_id}

効果的なステップを設計する

ステップは、耐久性のある関数の基礎的な作業単位です。適切に設計されたステップによってワークフローの理解、デバッグ、維持が容易になります。

ステップ設計の原則

  • 記述的な名前の使用 – 「step1」よりも「validate-order」のような名前により、ログやエラーが理解しやすくなります。

  • 名前を静的に維持 – タイムスタンプやランダムな値を含む動的な名前を使用しないでください。再生には、ステップ名が決定的である必要があります

  • 詳細度のバランス – 複雑なオペレーションを重点的なステップに分割しますが、チェックポイントのオーバーヘッドが増加する小さなステップは避けてください。

  • グループ関連オペレーション – 同時に成功または失敗するオペレーションは同じステップに属します

待機オペレーションを効率的に使用

待機オペレーションはリソースを消費したり、コストを発生させたりすることなく、実行を一時停止します。Lambda の実行を維持せずに、使用してください。

時間ベースの待機: setTimeout または sleep ではなく、context.wait() を遅延に使用します。

外部コールバック: 外部システムを待つときに context.waitForCallback() を使用します。無期限の待機を防止するため、必ずタイムアウトを設定してください。

ポーリング: エクスポネンシャルバックオフを持つ context.waitForCondition() を使用して、外部サービスを圧迫せずにポーリングします。

TypeScript
// Wait 24 hours without cost await context.wait({ seconds: 86400 }); // Wait for external callback with timeout const result = await context.waitForCallback( 'external-job', async (callbackId) => { await externalService.submitJob({ data: event.data, webhookUrl: `https://api.example.com/callbacks/${callbackId}` }); }, { timeout: { seconds: 3600 } } );
Python
# Wait 24 hours without cost context.wait(86400) # Wait for external callback with timeout result = context.wait_for_callback( lambda callback_id: external_service.submit_job( data=event['data'], webhook_url=f'https://api.example.com/callbacks/{callback_id}' ), name='external-job', config=WaitForCallbackConfig(timeout_seconds=3600) )

その他の考慮事項

エラー処理: ネットワークタイムアウトやレート制限など、一過性の障害を再試行します。無効な入力エラーや認証エラーなど、永続的な障害を再試行しないでください。適切な最大試行回数およびバックオフ率で再試行戦略を設定します。詳細な例については、「エラー処理と再試行」を参照してください。

パフォーマンス: 完全なペイロードではなく、リファレンスを保存してチェックポイントのサイズを最小限に抑えます。context.parallel() および context.map() を使用して独立したオペレーションを同時に実行します。チェックポイントのオーバーヘッドを減らすためのバッチ関連オペレーション。

バージョニング: バージョン番号またはエイリアスを使用して関数を呼び出し、実行を特定のコードバージョンに固定します。新しいコードバージョンが古いバージョンの状態を処理できることを確認してください。再生を中断する原因となるステップの名前変更や動作の変更をしないでください。

シリアル化: オペレーションの入力および結果に JSON 互換タイプを使用します。耐久性のあるオペレーションに渡す前に、日付を ISO 文字列に変換し、カスタムオブジェクトをプレーンオブジェクトに変換します。

モニタリング: 実行 ID およびステップ名で構造化ログ記録を有効にします。エラー率および実行期間に CloudWatch アラームを設定します。トレースを使用してボトルネックを特定します。詳細なガイダンスについては、「モニタリングとデバッグ」を参照してください。

テスト: ハッピーパス、エラー処理、再生動作をテストします。コールバックおよび待機のタイムアウトシナリオをテストします。ローカルテストを使用して反復時間を短縮します。詳細なガイダンスについては、「耐久性のある関数のテスト」を参照してください。

回避すべき一般的なミス: context.step() の呼び出しをネストせず、代わりに子コンテキストを使用してください。非決定的なオペレーションはステップでラップします。コールバックのタイムアウトは必ず設定してください。ステップの詳細度はチェックポイントのオーバーヘッドでバランスを取ります。大規模なオブジェクトではなく、リファレンスを状態に保存します。

その他のリソース