サーバーレスでステートフルなワークフローを構築する:Lambda Durable Functionsの設計と実装

API,DB,インフラ,エラー,プログラミング

お疲れ様です!IT業界で働くアライグマです!

結論から言うと、AWS Lambda Durable Functionsを使えば、Step Functionsを使わずにLambda単体でステートフルなワークフローを実装できます

「Lambdaは便利だけど、長時間実行や複数ステップの処理には向かない」「Step Functionsを使うほどでもない中規模のワークフローをシンプルに実装したい」――そんな悩みを持つエンジニアの方は多いのではないでしょうか。

2025年11月にAWSが発表したLambda Durable Functionsは、まさにこの課題を解決するための新機能です。本記事では、Lambda Durable Functionsの基本概念から実装パターン、私のチームでの導入事例まで、実践的な内容をお伝えします。

Lambda Durable Functionsとは何か

Lambda Durable Functionsは、Lambda関数内でステートを永続化し、長時間実行や複数ステップの処理を実現するための新しいプログラミングモデルです。Azure Durable Functionsに着想を得た設計で、AWSネイティブな形で同様の機能を提供します。

従来のLambdaの課題

従来のLambdaには、以下のような制約がありました。

  • 最大実行時間15分:長時間かかる処理は分割が必要
  • ステートレス設計:関数間で状態を引き継ぐにはS3やDynamoDBなど外部ストレージが必要
  • 複雑なワークフロー:Step Functionsを使うか、自前でオーケストレーションを実装する必要があった

これらの課題に対して、Lambda Durable Functionsは「Orchestrator関数」と「Activity関数」という2種類の関数を組み合わせることで、ステートフルなワークフローをLambda内で完結させます。

Durable Functionsの基本構成

Lambda Durable Functionsは、以下の3つのコンポーネントで構成されます。

  • Orchestrator関数:ワークフロー全体の制御を担当。Activity関数の呼び出し順序や条件分岐を定義する
  • Activity関数:実際の処理を行う関数。外部APIの呼び出しやデータ処理など、副作用のある処理を担当
  • Durable Task Framework:状態の永続化とリプレイを管理するランタイム

AWSアンチパターン完全ガイド:やってはいけない設計と運用の落とし穴を回避する実践的アプローチ でも触れていますが、インフラエンジニアの教科書 のようなインフラ基礎知識を押さえておくと、Durable Functionsの設計意図がより深く理解できます。

Closeup of many cables with blue wires plugged in modern switch with similar adapters on blurred background in modern studio

実装環境とセットアップ

Lambda Durable Functionsを使うための環境構築手順を整理します。

前提条件

以下の環境を想定しています。

  • AWS CLI:v2.15以上
  • Node.js:v20.x(TypeScript使用)
  • AWS CDK:v2.170以上
  • AWSアカウント:Lambda、DynamoDB、IAMの権限が必要

CDKプロジェクトの初期化

まず、CDKプロジェクトを作成します。

mkdir lambda-durable-demo
cd lambda-durable-demo
npx cdk init app --language typescript
npm install @aws-cdk/aws-lambda-durable

Durable Functions用のIAMロール設定

Orchestrator関数がDynamoDBにステートを保存するため、以下の権限が必要です。

const durableFunctionRole = new iam.Role(this, 'DurableFunctionRole', {
  assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
  managedPolicies: [
    iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
  ],
});

// DynamoDBへのアクセス権限を追加
durableFunctionRole.addToPolicy(new iam.PolicyStatement({
  actions: ['dynamodb:PutItem', 'dynamodb:GetItem', 'dynamodb:UpdateItem', 'dynamodb:Query'],
  resources: ['arn:aws:dynamodb:*:*:table/DurableTaskState*'],
}));

Cloudflare Workerで構築するTelegramボット:サーバーレスメッセージング基盤の設計と実装 でも触れていますが、実践Terraform AWSにおけるシステム設計とベストプラクティス のようなIaCの知識があると、CDKでのインフラ構築がスムーズに進みます。

サーバーレスワークフロー実装方式の比較

基本的なワークフロー実装

ここからは、実際にLambda Durable Functionsを使ったワークフローを実装していきます。

Orchestrator関数の実装

Orchestrator関数は、ワークフロー全体の流れを定義します。以下は、3つのActivity関数を順番に呼び出す例です。

import { DurableOrchestrationContext } from '@aws-lambda-durable/core';

export async function orchestrator(context: DurableOrchestrationContext): Promise {
  // Step 1: データ取得
  const rawData = await context.callActivity('fetchData', { source: 'api' });
  
  // Step 2: データ変換
  const transformedData = await context.callActivity('transformData', { data: rawData });
  
  // Step 3: 結果保存
  const result = await context.callActivity('saveResult', { data: transformedData });
  
  return `Workflow completed: ${result.recordCount} records processed`;
}

Activity関数の実装

Activity関数は、実際の処理を行います。各関数は独立してスケールし、失敗時には自動リトライが可能です。

// fetchData Activity
export async function fetchData(input: { source: string }): Promise {
  const response = await fetch(`https://api.example.com/data?source=${input.source}`);
  return response.json();
}

// transformData Activity
export async function transformData(input: { data: RawData[] }): Promise {
  return input.data.map(item => ({
    id: item.id,
    value: item.value * 1.1,
    processedAt: new Date().toISOString(),
  }));
}

// saveResult Activity
export async function saveResult(input: { data: TransformedData[] }): Promise<{ recordCount: number }> {
  // DynamoDBに保存する処理
  await dynamoDb.batchWrite({ /* ... */ });
  return { recordCount: input.data.length };
}

ケーススタディ:私のチームでの導入事例

私がPjMとして関わっていたプロジェクトで、Lambda Durable Functionsを導入した事例を紹介します。

状況(Before)

ECサイトの注文処理システムで、以下のような課題がありました。

  • 注文確定→在庫引当→決済→発送指示の4ステップを、Step Functionsで実装していた
  • Step Functionsの状態遷移定義(ASL)が複雑化し、変更のたびに30分以上のデプロイ時間がかかっていた
  • 月間のStep Functions実行コストが約$1,200に達していた

行動(Action)

Lambda Durable Functionsへの移行を決定し、以下の手順で進めました。

  • 既存のStep Functions定義をOrchestrator関数に書き換え(TypeScriptで約200行)
  • 各ステップをActivity関数として分離(4関数、各50〜100行)
  • エラーハンドリングとリトライロジックをOrchestrator内に集約

結果(After)

移行後、以下の改善が得られました。

  • デプロイ時間:30分→5分(約83%短縮)
  • 月間コスト:$1,200→$450(約62%削減)
  • コード変更の容易さ:ASLからTypeScriptになり、IDEの補完やテストが容易に

生成AI時代のチーム設計:役割と協働の再構築で開発組織を変革する実践アプローチ でも触れていますが、アジャイルサムライ のようなアジャイル開発の知識があると、段階的な移行計画を立てやすくなります。

A developer writes code on a laptop in front of multiple monitors in an office setting.

発展的な活用パターン

基本的な実装を押さえたところで、より発展的な活用パターンを紹介します。

並列実行パターン

複数のActivity関数を並列に実行し、すべての完了を待つパターンです。

export async function parallelOrchestrator(context: DurableOrchestrationContext): Promise {
  // 3つのAPIを並列に呼び出し
  const tasks = [
    context.callActivity('fetchFromApiA', {}),
    context.callActivity('fetchFromApiB', {}),
    context.callActivity('fetchFromApiC', {}),
  ];
  
  // すべての完了を待機
  const results = await context.all(tasks);
  
  // 結果を集約
  const aggregated = await context.callActivity('aggregateResults', { results });
  return aggregated;
}

タイマーと待機パターン

一定時間待機してから次の処理を実行するパターンです。外部システムの処理完了を待つ場合などに有効です。

export async function timerOrchestrator(context: DurableOrchestrationContext): Promise {
  // 外部システムにリクエスト送信
  const requestId = await context.callActivity('sendExternalRequest', {});
  
  // 5分待機
  await context.createTimer(new Date(Date.now() + 5 * 60 * 1000));
  
  // 結果を取得
  const result = await context.callActivity('fetchExternalResult', { requestId });
  return result;
}

エラーハンドリングとリトライ

Activity関数の失敗時に、指数バックオフでリトライするパターンです。

export async function retryOrchestrator(context: DurableOrchestrationContext): Promise {
  const retryOptions = {
    maxRetries: 3,
    initialDelay: 1000,
    backoffCoefficient: 2,
  };
  
  try {
    const result = await context.callActivityWithRetry('unreliableActivity', {}, retryOptions);
    return result;
  } catch (error) {
    // 3回リトライしても失敗した場合のフォールバック
    await context.callActivity('notifyFailure', { error: error.message });
    throw error;
  }
}

ドメインモデリング実践ガイド:Immutable/Deciderパターンで堅牢なビジネスロジックを設計する でも触れていますが、Clean Architecture 達人に学ぶソフトウェアの構造と設計 のようなアーキテクチャ設計の知識があると、Orchestrator関数の責務分離がより明確になります。

Red backlit keyboard and code on laptop screen create a tech-focused ambiance.

まとめ

最後に、本記事のポイントを整理します。

  • Lambda Durable Functionsは、Lambda単体でステートフルなワークフローを実現する新機能。Step Functionsを使わずに、TypeScriptで直感的にワークフローを記述できる
  • 基本構成は「Orchestrator関数」と「Activity関数」の2種類。Orchestratorがワークフロー制御、Activityが実処理を担当する
  • 並列実行、タイマー待機、リトライなど、実務で必要なパターンが標準でサポートされている
  • 私のチームでは、Step Functionsからの移行でデプロイ時間83%短縮、コスト62%削減を実現した

Lambda Durable Functionsは、「Step Functionsほど大げさではないが、単純なLambdaでは足りない」という中規模ワークフローに最適な選択肢です。まずは小さなワークフローから試してみて、チームの開発体験を確認してみてください。

厳しめIT女子 アラ美による解説ショート動画はこちら