LESSON 40分

「Lambda の設計で最も重要なのは、関数を小さく保つことと、コールドスタートを理解することだ。そして、Lambda が万能じゃないことも知っておく必要がある」と佐藤CTOが強調した。

推定読了時間: 40分


Lambda 関数の設計原則

Single Responsibility(単一責任)

// ❌ Bad: 1つの関数で全てを処理
export const handler = async (event: any) => {
  if (event.httpMethod === "GET") { /* ユーザー取得 */ }
  if (event.httpMethod === "POST") { /* ユーザー作成 */ }
  if (event.httpMethod === "PUT") { /* ユーザー更新 */ }
  if (event.httpMethod === "DELETE") { /* ユーザー削除 */ }
};

// ✅ Good: 関数を分離
// functions/getUser.ts
export const getUser = async (event: APIGatewayProxyEvent) => {
  const userId = event.pathParameters?.id;
  const user = await userRepository.findById(userId!);
  return { statusCode: 200, body: JSON.stringify(user) };
};

// functions/createUser.ts
export const createUser = async (event: APIGatewayProxyEvent) => {
  const data = JSON.parse(event.body ?? "{}");
  const user = await userRepository.create(data);
  return { statusCode: 201, body: JSON.stringify(user) };
};

ハンドラーパターン

// Hexagonal Architecture を Lambda に適用
// domain/ports/in/CreateOrderUseCase.ts
interface CreateOrderUseCase {
  execute(command: CreateOrderCommand): Promise<Order>;
}

// application/use-cases/CreateOrderUseCaseImpl.ts
class CreateOrderUseCaseImpl implements CreateOrderUseCase {
  constructor(
    private readonly orderRepo: OrderRepository,
    private readonly paymentGateway: PaymentGateway,
  ) {}

  async execute(command: CreateOrderCommand): Promise<Order> {
    const order = Order.create(command);
    await this.paymentGateway.charge(order.totalAmount);
    await this.orderRepo.save(order);
    return order;
  }
}

// adapters/in/lambda/createOrderHandler.ts
import { createOrderUseCase } from "../../../di/container";

export const handler = async (event: APIGatewayProxyEvent) => {
  try {
    const command = JSON.parse(event.body ?? "{}") as CreateOrderCommand;
    const order = await createOrderUseCase.execute(command);
    return {
      statusCode: 201,
      body: JSON.stringify(order),
    };
  } catch (error) {
    if (error instanceof ValidationError) {
      return { statusCode: 400, body: JSON.stringify({ error: error.message }) };
    }
    throw error;
  }
};

コールドスタート対策

コールドスタートの仕組み

graph LR
    subgraph Cold["Cold Start"]
        A["Download Code<br/>(~100ms)"] --> B["Start Runtime<br/>(~200ms)"]
        B --> C["Init Handler<br/>(~500ms)"]
        C --> D["Execute Handler<br/>(~50ms)"]
    end
    subgraph Warm["Warm Start"]
        E["Execute Handler<br/>(~50ms)"]
    end

    classDef cold fill:#e74c3c,stroke:#c0392b,color:#fff
    classDef warm fill:#27ae60,stroke:#1e8449,color:#fff
    classDef exec fill:#3498db,stroke:#2980b9,color:#fff
    class A,B,C cold
    class D,E exec

対策一覧

対策効果トレードオフ
パッケージサイズ削減起動時間短縮ビルド設定が必要
Provisioned Concurrencyコールドスタート排除コスト増
Lambda SnapStartJVM起動を高速化Java/Kotlin限定
初期化コードの最適化Init時間短縮コードの複雑化
Lambda Layers共有依存の分離バージョン管理

Provisioned Concurrency

# SAM Template
Resources:
  ApiFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: dist/handler.handler
      Runtime: nodejs20.x
      MemorySize: 512
      Timeout: 30
      AutoPublishAlias: live
      ProvisionedConcurrencyConfig:
        ProvisionedConcurrentExecutions: 10

  # Auto Scaling for Provisioned Concurrency
  ApiScalableTarget:
    Type: AWS::ApplicationAutoScaling::ScalableTarget
    Properties:
      MaxCapacity: 50
      MinCapacity: 5
      ResourceId: !Sub function:${ApiFunction}:live
      ScalableDimension: lambda:function:ProvisionedConcurrentExecutions
      ServiceNamespace: lambda

  ApiScalingPolicy:
    Type: AWS::ApplicationAutoScaling::ScalingPolicy
    Properties:
      PolicyName: utilization
      ScalingTargetId: !Ref ApiScalableTarget
      PolicyType: TargetTrackingScaling
      TargetTrackingScalingPolicyConfiguration:
        TargetValue: 0.7
        PredefinedMetricSpecification:
          PredefinedMetricType: LambdaProvisionedConcurrencyUtilization

パッケージサイズの最適化

// esbuild でバンドル(tree-shaking + minify)
// esbuild.config.ts
import { build } from "esbuild";

await build({
  entryPoints: ["src/handlers/*.ts"],
  bundle: true,
  minify: true,
  sourcemap: true,
  platform: "node",
  target: "node20",
  outdir: "dist",
  external: ["@aws-sdk/*"],  // Lambda ランタイムに含まれるSDKは除外
  treeShaking: true,
});

初期化の最適化

// ❌ Bad: ハンドラー内で毎回初期化
export const handler = async (event: any) => {
  const db = new Pool({ connectionString: process.env.DATABASE_URL });
  const result = await db.query("SELECT * FROM users WHERE id = $1", [event.id]);
  await db.end();
  return result.rows[0];
};

// ✅ Good: ハンドラー外で初期化(再利用される)
import { Pool } from "pg";

// コールドスタート時のみ実行
const db = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 1,  // Lambda では接続数を最小に
});

export const handler = async (event: any) => {
  // Warm Start ではDB接続が再利用される
  const result = await db.query("SELECT * FROM users WHERE id = $1", [event.id]);
  return result.rows[0];
};

Lambda Layers

# Lambda Layer の定義
Resources:
  SharedUtilsLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: shared-utils
      ContentUri: layers/shared-utils/
      CompatibleRuntimes:
        - nodejs20.x
      RetentionPolicy: Retain
    Metadata:
      BuildMethod: nodejs20.x

  ApiFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: dist/handler.handler
      Runtime: nodejs20.x
      Layers:
        - !Ref SharedUtilsLayer
        - !Sub arn:aws:lambda:${AWS::Region}:901920570463:layer:aws-otel-nodejs-amd64-ver-1-18-1:1

Lambda のベストプラクティス

べき等性(Idempotency)

import { IdempotencyConfig, makeIdempotent } from "@aws-lambda-powertools/idempotency";
import { DynamoDBPersistenceLayer } from "@aws-lambda-powertools/idempotency/dynamodb";

const persistenceLayer = new DynamoDBPersistenceLayer({
  tableName: process.env.IDEMPOTENCY_TABLE!,
});

const config = new IdempotencyConfig({
  eventKeyJmesPath: "body.orderId",
  expiresAfterSeconds: 3600,
});

export const handler = makeIdempotent(
  async (event: APIGatewayProxyEvent) => {
    const order = JSON.parse(event.body!);
    const result = await processOrder(order);
    return {
      statusCode: 200,
      body: JSON.stringify(result),
    };
  },
  { persistenceStore: persistenceLayer, config }
);

Powertools for AWS Lambda

import { Logger } from "@aws-lambda-powertools/logger";
import { Tracer } from "@aws-lambda-powertools/tracer";
import { Metrics, MetricUnit } from "@aws-lambda-powertools/metrics";

const logger = new Logger({ serviceName: "order-service" });
const tracer = new Tracer({ serviceName: "order-service" });
const metrics = new Metrics({ namespace: "OrderService", serviceName: "order-service" });

export const handler = async (event: APIGatewayProxyEvent) => {
  // 構造化ログ
  logger.info("Processing order", { orderId: event.pathParameters?.id });

  // カスタムメトリクス
  metrics.addMetric("OrderProcessed", MetricUnit.Count, 1);

  // トレーシング
  const segment = tracer.getSegment();
  const subsegment = segment?.addNewSubsegment("processOrder");

  try {
    const result = await processOrder(event);
    subsegment?.close();
    metrics.publishStoredMetrics();
    return { statusCode: 200, body: JSON.stringify(result) };
  } catch (error) {
    subsegment?.addError(error as Error);
    subsegment?.close();
    throw error;
  }
};

メモリとタイムアウトの設計

graph TD
    M1["128 MB / 0.08 vCPU
軽量な変換処理"] M2["512 MB / 0.33 vCPU
API バックエンド"] M3["1024 MB / 0.58 vCPU
データ処理"] M4["1769 MB / 1.00 vCPU
CPU集約型の処理"] M5["3008 MB / 1.75 vCPU
ML推論、画像処理"] M6["10240 MB / 6.00 vCPU
大規模データ処理"] M1 --> M2 --> M3 --> M4 --> M5 --> M6 style M1 fill:#d1fae5,stroke:#059669,color:#065f46 style M2 fill:#d1fae5,stroke:#059669,color:#065f46 style M3 fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af style M4 fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e style M5 fill:#fee2e2,stroke:#dc2626,color:#991b1b style M6 fill:#fee2e2,stroke:#dc2626,color:#991b1b
// AWS Lambda Power Tuning で最適なメモリを見つける
// https://github.com/alexcasalboni/aws-lambda-power-tuning
const powerTuningInput = {
  lambdaARN: "arn:aws:lambda:ap-northeast-1:123456789012:function:my-function",
  powerValues: [128, 256, 512, 1024, 1536, 2048, 3008],
  num: 50,
  payload: '{"test": "data"}',
  parallelInvocation: true,
  strategy: "cost",  // "cost" or "speed" or "balanced"
};

まとめ

プラクティス説明
単一責任1関数1責任
ハンドラー外初期化DB接続等はハンドラー外で
バンドルの最適化esbuild + tree-shaking
べき等性Powertools Idempotency
Provisioned Concurrencyコールドスタート排除
Layers共有依存の分離
Power Tuning最適なメモリ設定

チェックリスト

  • Lambda 関数を単一責任で設計できる
  • コールドスタートの仕組みと対策を理解している
  • ハンドラー外での初期化パターンを適用できる
  • べき等性を確保する設計ができる
  • Provisioned Concurrency の適用判断ができる
  • Lambda Powertools を活用できる

次のステップへ

次のレッスンでは、EventBridge/SQS/SNS を組み合わせたサーバーレスイベント駆動アーキテクチャを学びます。