LESSON 40分

「サーバーレスは銀の弾丸じゃない。『全てをLambdaで』という発想は危険だ。いつサーバーレスを使い、いつコンテナやVMを使うか。その判断ができてこそ、本物のクラウドアーキテクトだよ」と佐藤CTOが釘を刺した。

推定読了時間: 40分


サーバーレスが適さないケース

1. 長時間実行ワークロード

Lambda の制限:
- 最大実行時間: 15分
- 最大メモリ: 10,240 MB
- 最大パッケージサイズ: 250 MB (展開後)
- 同時実行数: アカウントあたり 1,000 (デフォルト)
ワークロードLambdaコンテナ (ECS/K8s)
API レスポンス (<30秒)最適
バッチ処理 (数分)最適
ML トレーニング (数時間)不可最適
WebSocket 常時接続制限あり最適
ストリーミング処理制限あり最適

2. 高スループット・低レイテンシ要件

graph TD
    subgraph chart["処理量とコストの損益分岐点"]
        direction LR
        LOW["低リクエスト数"]:::low --> MID["中リクエスト数"]:::mid --> HIGH["高リクエスト数"]:::high
    end

    L["Lambda<br/>低リクエストで最安<br/>高リクエストで最高"]:::lambda
    F["Fargate / ECS<br/>中規模で効率的"]:::fargate
    E["EC2<br/>大規模で最安<br/>固定コスト型"]:::ec2

    LOW --- L
    MID --- F
    HIGH --- E

    classDef low fill:#E8F5E9,stroke:#388E3C,color:#1B5E20
    classDef mid fill:#FFF3E0,stroke:#F57C00,color:#E65100
    classDef high fill:#FFEBEE,stroke:#D32F2F,color:#B71C1C
    classDef lambda fill:#FF9800,stroke:#E65100,color:#FFFFFF
    classDef fargate fill:#2196F3,stroke:#0D47A1,color:#FFFFFF
    classDef ec2 fill:#4CAF50,stroke:#1B5E20,color:#FFFFFF
// コスト試算例
const estimate = {
  scenario: "月間1000万リクエスト、平均100ms、512MB",
  lambda: {
    compute: 10_000_000 * 0.1 * 0.5 * 0.0000166667, // $8.33
    requests: 10_000_000 * 0.0000002, // $2.00
    total: "$10.33/月",
  },
  fargateSpot: {
    // 0.25 vCPU, 0.5GB, 2タスク常時起動
    compute: 2 * 0.25 * 0.01234567 * 730, // $4.51
    memory: 2 * 0.5 * 0.00135546 * 730,   // $0.99
    total: "$5.50/月(Spot割引後さらに低下)",
  },
};
// → 月間1000万リクエスト以上ではコンテナの方が安い場合がある

「サーバーレスは少量多品種のイベント処理に最適だ。大量のリクエストを常時処理するなら、コンテナの方がコスト効率が良いことがある」

3. ベンダーロックイン

要素ロックインの程度移行コスト
Lambda 関数コードハンドラーの書き換え程度
EventBridge ルールルーティングロジックの再実装
Step Functionsワークフロー全体の再実装
DynamoDBデータモデルの再設計
API GatewayOpenAPI で抽象化可能
// ベンダーロックインを軽減する設計
// ポートパターンでクラウドサービスを抽象化

// domain/ports/out/EventPublisher.ts
interface EventPublisher {
  publish(event: DomainEvent): Promise<void>;
}

// adapters/out/aws/EventBridgePublisher.ts
class EventBridgePublisher implements EventPublisher {
  async publish(event: DomainEvent): Promise<void> {
    await eventBridge.send(new PutEventsCommand({
      Entries: [{ /* EventBridge 固有の実装 */ }],
    }));
  }
}

// adapters/out/gcp/PubSubPublisher.ts
class PubSubPublisher implements EventPublisher {
  async publish(event: DomainEvent): Promise<void> {
    // Google Cloud Pub/Sub の実装
  }
}

テストの課題

ローカルテストの困難さ

// Lambda のローカルテスト戦略

// 1. ビジネスロジックを Lambda ハンドラーから分離
// ビジネスロジック(テスト容易)
export async function processOrder(order: OrderInput): Promise<OrderResult> {
  const validated = validateOrder(order);
  const priced = calculatePrice(validated);
  return priced;
}

// Lambda ハンドラー(薄いアダプター層)
export const handler = async (event: APIGatewayProxyEvent) => {
  const input = JSON.parse(event.body!);
  const result = await processOrder(input);
  return { statusCode: 200, body: JSON.stringify(result) };
};

// 2. テスト
describe("processOrder", () => {
  it("should calculate total correctly", async () => {
    const result = await processOrder({
      items: [{ productId: "P1", quantity: 2, price: 1000 }],
    });
    expect(result.total).toBe(2000);
  });
});

統合テストの複雑さ

// LocalStack を使った統合テスト
import { DynamoDBClient, CreateTableCommand } from "@aws-sdk/client-dynamodb";

const dynamodb = new DynamoDBClient({
  endpoint: "http://localhost:4566",  // LocalStack
  region: "ap-northeast-1",
  credentials: { accessKeyId: "test", secretAccessKey: "test" },
});

beforeAll(async () => {
  await dynamodb.send(new CreateTableCommand({
    TableName: "orders",
    KeySchema: [{ AttributeName: "orderId", KeyType: "HASH" }],
    AttributeDefinitions: [{ AttributeName: "orderId", AttributeType: "S" }],
    BillingMode: "PAY_PER_REQUEST",
  }));
});
# docker-compose.yml for local testing
services:
  localstack:
    image: localstack/localstack:3.0
    ports:
      - "4566:4566"
    environment:
      - SERVICES=dynamodb,sqs,sns,events,lambda,s3
      - DEBUG=0
    volumes:
      - "./localstack-init:/etc/localstack/init/ready.d"

コールドスタートの影響

ランタイムコールドスタート(平均)用途の適性
Node.js200-500msAPI バックエンド
Python200-500msデータ処理、ML
Go50-100ms低レイテンシAPI
Java (SnapStart無し)3,000-8,000msバッチ処理のみ
Java (SnapStart)200-500msAPI可能
.NET500-1,000msエンタープライズ
SLA 要件: p99 < 500ms の場合
→ Node.js でも Provisioned Concurrency が必要
→ コスト増のトレードオフ
→ コンテナ(Fargate)の方が安定する可能性

判断フレームワーク

サーバーレス vs コンテナの判断マトリクス

要件サーバーレス優位コンテナ優位
実行時間< 15分> 15分
リクエスト頻度不定期/バースト常時高負荷
レイテンシ要件p99 > 1秒 OKp99 < 100ms 必須
状態管理ステートレスステートフル
カスタムランタイム不要必要
デバッグ容易性許容可能重要
ベンダー独立性許容可能重要

ハイブリッドアーキテクチャ

graph TD
    subgraph hybrid["ハイブリッド構成"]
        APIGW(["API Gateway"]):::apigw
        APIGW --> LAMBDA["Lambda<br/>(CRUD)"]:::lambda
        APIGW --> ECS["ECS / Fargate<br/>(複雑な処理)"]:::ecs
        LAMBDA --> DYNAMO[("DynamoDB")]:::db
        ECS --> AURORA[("Aurora<br/>Serverless")]:::db
        EB{{"EventBridge"}}:::eventbridge --> EVLAMBDA["Lambda<br/>(イベント処理)"]:::lambda
        EB --> STEPFN(["Step Functions<br/>(ワークフロー)"]):::step
    end

    classDef apigw fill:#E74C3C,stroke:#A93226,color:#FFFFFF
    classDef lambda fill:#F39C12,stroke:#B7770D,color:#FFFFFF
    classDef ecs fill:#3498DB,stroke:#21618C,color:#FFFFFF
    classDef db fill:#1ABC9C,stroke:#148F77,color:#FFFFFF
    classDef eventbridge fill:#8E44AD,stroke:#6C3483,color:#FFFFFF
    classDef step fill:#9B59B6,stroke:#6C3483,color:#FFFFFF

「実際のプロダクションでは、サーバーレスとコンテナのハイブリッドが最も現実的だ。CRUD系のAPIはLambdaで、複雑なビジネスロジックはコンテナで。使い分けが重要だよ」

判断チェックリスト

以下の質問に「はい」が多いほどサーバーレスが適している:

  1. 実行時間は15分以内か?
  2. リクエストは不定期またはバースト型か?
  3. ゼロスケール(使わない時間がある)が必要か?
  4. チームがインフラ管理のオーバーヘッドを減らしたいか?
  5. イベント駆動で自然にモデリングできるか?
  6. コールドスタートのレイテンシは許容できるか?
  7. AWS への依存は許容できるか?
  8. 月間のリクエスト数は100万以下か?

まとめ

限界・課題影響対策
実行時間制限長時間処理が不可Step Functions で分割
コールドスタートレイテンシ増大Provisioned Concurrency
ベンダーロックイン移行コストポートパターンで抽象化
テストの困難さ開発効率低下LocalStack + ロジック分離
コスト高 (高負荷時)予算超過コンテナとのハイブリッド
デバッグの困難さ障害対応の長期化構造化ログ + X-Ray

チェックリスト

  • サーバーレスが適さないケースを列挙できる
  • コスト試算に基づくサーバーレス vs コンテナの判断ができる
  • ベンダーロックインを軽減する設計パターンを適用できる
  • サーバーレスのテスト戦略を立案できる
  • ハイブリッドアーキテクチャを設計できる

次のステップへ

Step 3 の演習とクイズで、サーバーレスアーキテクチャの理解度を確認しましょう。