1処理1Lambdaに細分化する設計パターン。通称 "Lambda per method" / "Nano-functions"。 通常のプログラミングにおける関数分割をそのままLambda分割に持ち上げる構成。
- Lambda間呼び出しでは、コールドスタート(実行環境が未起動の状態からランタイムの初期化・コードのロードを行う時間。Java: 数百ms〜数s、Python/Node: 100〜500ms程度)が各Lambdaで発生しうる
- プロセス内関数呼び出しのオーバーヘッドはμs単位。Lambda間呼び出しはネットワークホップ(Lambda AがLambda Bを呼ぶ際に経由するネットワーク通信の1区間)を含むためms〜s単位
- 3段チェーン(Lambda A → Lambda B → Lambda Cのように3つのLambdaが直列に呼び出し合う構成)の場合、最悪ケースでコールドスタートが3回直列に発生し、合計レイテンシが数秒に達する
- Lambda課金単位: リクエスト数(100万リクエストあたり$0.20)+ 実行時間(GB-秒(割り当てメモリ1GBのLambdaが1秒間実行された場合の課金単位)あたり$0.0000166667)
- 1つのLambdaで完結する処理を3つに分割した場合、リクエスト課金が3倍になる
- 間にAPI Gateway(AWSのAPI管理サービス。HTTPリクエストをLambdaにルーティングする。$3.50/100万リクエスト)、SQS(Amazon Simple Queue Service。非同期メッセージキュー。$0.40/100万リクエスト)等を挟むとさらに加算
- 実行時間課金も、各Lambdaの最低課金単位(1ms)× Lambda数分の端数切り上げが発生
- Lambda数に比例してCloudFormation/SAM(AWSリソースをYAML/JSONテンプレートで宣言的に管理するIaCツール。SAMはサーバーレス特化のCloudFormation拡張)テンプレートのリソース定義が増加
- CloudWatch(AWSのモニタリング・ログ管理サービス)ロググループがLambda単位で生成される → 1トランザクションの追跡に複数ロググループの横断が必要
- X-Ray(AWSの分散トレーシングサービス。リクエストが複数サービスを通過する経路を可視化する)でトレース可能だが、スパン(X-Rayにおけるトレースの最小単位。1つのLambda実行やAPI呼び出しが1スパンに対応)数増加によりトレースの読解コストが上昇
- IAMロール/ポリシー(AWSリソースへのアクセス権限を定義する仕組み。各Lambdaに「何にアクセスできるか」を個別に設定する)もLambda単位で管理が必要 → 数十〜数百のロール管理が発生
- Lambda間の呼び出し失敗時のリトライ設計が必須(デフォルトで非同期呼び出しは2回リトライ)
- べき等性(同じ操作を複数回実行しても結果が変わらない性質。例: 同じ注文IDで2回書き込んでも注文が重複しない)の担保が各Lambdaに求められる
- 複数Lambdaにまたがるトランザクション管理にはSagaパターン(分散トランザクションを一連のローカルトランザクション+補償処理(ロールバック相当)で実現する設計パターン)やStep Functions(AWSのワークフローオーケストレーションサービス。Lambda等のステップを状態遷移図として定義・実行する)が必要 → 設計・実装コスト増
- 部分障害(チェーン中の一部のLambdaのみが失敗し、残りは成功した状態。例: 注文DBへの書き込みは成功したがメール送信Lambdaが失敗)時の整合性担保が困難
ユースケース: POST /orders を受け取り、以下を実行する。
- リクエストバリデーション
- 在庫チェック(DynamoDB読み取り)
- 注文レコード作成(DynamoDB書き込み)
- 確認メール送信(SES(Amazon Simple Email Service。AWSのメール送信サービス))
各ステップを個別のLambdaに分割し、Lambda同士をSDK(AWS SDK for JavaScript v3。@aws-sdk/*パッケージ群によるAWSサービス操作用ライブラリ)で直列呼び出しする構成。
order-service/
├── template.yaml # SAM: Lambda×4 + API Gateway + IAMロール×4 (BuildMethod: esbuild)
├── validate-order/
│ ├── handler.ts # バリデーションのみ → 次のLambdaをinvoke
│ └── package.json
├── check-inventory/
│ ├── handler.ts # 在庫チェックのみ → 次のLambdaをinvoke
│ └── package.json
├── create-order/
│ ├── handler.ts # DB書き込みのみ → 次のLambdaをinvoke
│ └── package.json
└── send-confirmation/
├── handler.ts # メール送信のみ
└── package.json
validate-order/handler.ts
import { LambdaClient, InvokeCommand } from "@aws-sdk/client-lambda";
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
const lambdaClient = new LambdaClient({});
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
const body = JSON.parse(event.body ?? "{}");
// バリデーションだけして次のLambdaに丸投げ
if (!body.user_id || !body.items) {
return { statusCode: 400, body: JSON.stringify({ error: "invalid request" }) };
}
// 同期呼び出し → コールドスタート発生の可能性
const response = await lambdaClient.send(
new InvokeCommand({
FunctionName: "check-inventory",
InvocationType: "RequestResponse",
Payload: Buffer.from(JSON.stringify(body)),
})
);
const payload = JSON.parse(Buffer.from(response.Payload!).toString());
return payload;
};check-inventory/handler.ts
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
import { LambdaClient, InvokeCommand } from "@aws-sdk/client-lambda";
const docClient = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const lambdaClient = new LambdaClient({});
interface OrderItem {
product_id: string;
quantity: number;
}
export const handler = async (event: { items: OrderItem[]; user_id: string }) => {
for (const item of event.items) {
const { Item: record } = await docClient.send(
new GetCommand({ TableName: "Inventory", Key: { product_id: item.product_id } })
);
if (!record || record.stock < item.quantity) {
return { statusCode: 409, body: JSON.stringify({ error: "out of stock" }) };
}
}
// また次のLambdaに丸投げ
const response = await lambdaClient.send(
new InvokeCommand({
FunctionName: "create-order",
InvocationType: "RequestResponse",
Payload: Buffer.from(JSON.stringify(event)),
})
);
return JSON.parse(Buffer.from(response.Payload!).toString());
};create-order/handler.ts
import { randomUUID } from "node:crypto";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
import { LambdaClient, InvokeCommand } from "@aws-sdk/client-lambda";
const docClient = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const lambdaClient = new LambdaClient({});
export const handler = async (event: { user_id: string; items: unknown[] }) => {
const orderId = randomUUID();
await docClient.send(
new PutCommand({
TableName: "Orders",
Item: { order_id: orderId, user_id: event.user_id, items: event.items },
})
);
// さらに次のLambdaへ
await lambdaClient.send(
new InvokeCommand({
FunctionName: "send-confirmation",
InvocationType: "Event", // 非同期
Payload: Buffer.from(JSON.stringify({ order_id: orderId, user_id: event.user_id })),
})
);
return { statusCode: 201, body: JSON.stringify({ order_id: orderId }) };
};| 指標 | 値 |
|---|---|
| Lambda関数数 | 4 |
| 最悪レイテンシ(コールドスタート3回直列) | 300ms〜1500ms 加算 |
| リクエスト課金 | 4倍(1注文あたり4 invoke) |
| CloudWatchロググループ数 | 4 |
| IAMロール数 | 4 |
| 障害点 | 4(各Lambda + 各Lambda間のネットワーク) |
order-service/
├── template.yaml # SAM: Lambda×1 + API Gateway + IAMロール×1 (BuildMethod: esbuild)
└── create-order/
├── handler.ts # エントリポイント
├── validator.ts # バリデーションロジック
├── inventory.ts # 在庫チェックロジック
├── repository.ts # DynamoDB操作
├── notifier.ts # メール送信ロジック
├── types.ts # 共通型定義
└── package.json
create-order/types.ts
export interface OrderItem {
product_id: string;
quantity: number;
}
export interface CreateOrderRequest {
user_id: string;
items: OrderItem[];
}create-order/validator.ts
import type { CreateOrderRequest } from "./types";
export class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "ValidationError";
}
}
export const validateOrderRequest = (body: unknown): CreateOrderRequest => {
const b = body as Record<string, unknown>;
if (!b.user_id || typeof b.user_id !== "string") {
throw new ValidationError("user_id is required");
}
if (!Array.isArray(b.items) || b.items.length === 0) {
throw new ValidationError("items must be a non-empty array");
}
for (const item of b.items) {
const i = item as Record<string, unknown>;
if (!i.product_id || typeof i.quantity !== "number" || !Number.isInteger(i.quantity)) {
throw new ValidationError("each item requires product_id and integer quantity");
}
}
return b as unknown as CreateOrderRequest;
};create-order/inventory.ts
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
import type { OrderItem } from "./types";
const docClient = DynamoDBDocumentClient.from(new DynamoDBClient({}));
export class OutOfStockError extends Error {
constructor(public readonly productId: string) {
super(`out of stock: ${productId}`);
this.name = "OutOfStockError";
}
}
export const checkInventory = async (items: OrderItem[]): Promise<void> => {
for (const item of items) {
const { Item: record } = await docClient.send(
new GetCommand({ TableName: "Inventory", Key: { product_id: item.product_id } })
);
if (!record || record.stock < item.quantity) {
throw new OutOfStockError(item.product_id);
}
}
};create-order/repository.ts
import { randomUUID } from "node:crypto";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
import type { OrderItem } from "./types";
const docClient = DynamoDBDocumentClient.from(new DynamoDBClient({}));
export const createOrder = async (userId: string, items: OrderItem[]): Promise<string> => {
const orderId = randomUUID();
await docClient.send(
new PutCommand({
TableName: "Orders",
Item: { order_id: orderId, user_id: userId, items },
})
);
return orderId;
};create-order/notifier.ts
import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses";
const ses = new SESClient({});
export const sendConfirmation = async (orderId: string, userId: string): Promise<void> => {
// 実装省略: SESでメール送信
// 失敗してもレスポンスには影響させない(非同期化する場合はSQS/EventBridgeへ)
};create-order/handler.ts
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { validateOrderRequest, ValidationError } from "./validator";
import { checkInventory, OutOfStockError } from "./inventory";
import { createOrder } from "./repository";
import { sendConfirmation } from "./notifier";
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
try {
const body = JSON.parse(event.body ?? "{}");
const request = validateOrderRequest(body);
await checkInventory(request.items);
const orderId = await createOrder(request.user_id, request.items);
await sendConfirmation(orderId, request.user_id);
return { statusCode: 201, body: JSON.stringify({ order_id: orderId }) };
} catch (e) {
if (e instanceof ValidationError) {
return { statusCode: 400, body: JSON.stringify({ error: e.message }) };
}
if (e instanceof OutOfStockError) {
return { statusCode: 409, body: JSON.stringify({ error: e.message }) };
}
return { statusCode: 500, body: JSON.stringify({ error: "internal server error" }) };
}
};| 指標 | 悪い設計 | 良い設計 |
|---|---|---|
| Lambda関数数 | 4 | 1 |
| コールドスタート発生回数(最悪) | 4 | 1 |
| リクエスト課金(1注文あたり) | 4 invoke | 1 invoke |
| CloudWatchロググループ数 | 4 | 1 |
| IAMロール数 | 4 | 1 |
| 1トランザクションのトレースに必要なログ横断数 | 4 | 0 |
| エラーハンドリング | 各Lambda + Lambda間通信の失敗処理 | try/catchのみ |
| テスト | 各Lambdaの結合テスト + モック必須 | 単体テスト + ローカル実行可 |
| 分割基準 | 説明 | 例 |
|---|---|---|
| ドメイン境界 / Bounded Context | DDDにおける、独立したドメインモデルが適用される範囲。サービス間の責務境界となる | 注文サービス、決済サービス、通知サービス |
| HTTPメソッド + リソース単位 | REST APIのエンドポイント1つに対して1Lambda | GET /users, POST /orders |
| イベントソース単位 | Lambdaをトリガーするイベントの発生元ごとに分割 | S3トリガー用、DynamoDB Streams用、SQSコンシューマー用 |
Lambda内部の処理分割は、通常のコードレベル(関数・クラス・モジュール)で行う。 インフラ境界(=Lambda分割。デプロイ単位・課金単位・スケーリング単位が分かれる)とコード境界(=関数・モジュール分割。同一プロセス内で呼び出される)は別の関心事。
- AWS Lambda Best Practices: https://docs.aws.amazon.com/lambda/latest/dg/best-practices.html
- "The Lambda Trilogy" (Yan Cui): Lambda関数の粒度に関する分類と考察
- AWS Well-Architected Framework - Serverless Lens