Skip to content

Instantly share code, notes, and snippets.

@kobitoDevelopment
Created February 21, 2026 07:31
Show Gist options
  • Select an option

  • Save kobitoDevelopment/50edf0769aad649c7c8e02503b332be1 to your computer and use it in GitHub Desktop.

Select an option

Save kobitoDevelopment/50edf0769aad649c7c8e02503b332be1 to your computer and use it in GitHub Desktop.

Lambda Nano-functions アンチパターン

概要

1処理1Lambdaに細分化する設計パターン。通称 "Lambda per method" / "Nano-functions"。 通常のプログラミングにおける関数分割をそのままLambda分割に持ち上げる構成。

問題点

1. レイテンシの劣化

  • 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回直列に発生し、合計レイテンシが数秒に達する

2. コスト増

  • 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数分の端数切り上げが発生

3. 運用コスト

  • 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単位で管理が必要 → 数十〜数百のロール管理が発生

4. 分散システム固有の課題を不必要に負う

  • Lambda間の呼び出し失敗時のリトライ設計が必須(デフォルトで非同期呼び出しは2回リトライ)
  • べき等性(同じ操作を複数回実行しても結果が変わらない性質。例: 同じ注文IDで2回書き込んでも注文が重複しない)の担保が各Lambdaに求められる
  • 複数Lambdaにまたがるトランザクション管理にはSagaパターン(分散トランザクションを一連のローカルトランザクション+補償処理(ロールバック相当)で実現する設計パターン)やStep Functions(AWSのワークフローオーケストレーションサービス。Lambda等のステップを状態遷移図として定義・実行する)が必要 → 設計・実装コスト増
  • 部分障害(チェーン中の一部のLambdaのみが失敗し、残りは成功した状態。例: 注文DBへの書き込みは成功したがメール送信Lambdaが失敗)時の整合性担保が困難

具体例: 注文作成API

ユースケース: POST /orders を受け取り、以下を実行する。

  1. リクエストバリデーション
  2. 在庫チェック(DynamoDB読み取り)
  3. 注文レコード作成(DynamoDB書き込み)
  4. 確認メール送信(SES(Amazon Simple Email Service。AWSのメール送信サービス))

悪い設計: 1処理1Lambda(Nano-functions)

各ステップを個別の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間のネットワーク)

良い設計: ユースケース単位の1Lambda + コードレベル分割

ディレクトリ構造

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分割。デプロイ単位・課金単位・スケーリング単位が分かれる)とコード境界(=関数・モジュール分割。同一プロセス内で呼び出される)は別の関心事。

参考

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment