LESSON 40分

「サーバーレスは『サーバーがない』のではなく、『サーバーを意識しない』ということだ。イベント駆動で考え、適材適所でサーバーレスを使えるエンジニアが、クラウドネイティブの真価を引き出せる」と佐藤CTOが語った。

推定読了時間: 40分


サーバーレスの基本原則

原則説明
No Server Managementインフラの管理が不要
Pay-per-Use使った分だけ課金
Auto-Scaling自動的にスケール(ゼロまで)
Event-Drivenイベントをトリガーに実行
Stateless関数はステートレス

イベント駆動アーキテクチャ

基本パターン

graph LR
    A(["Producer<br/>(API GW)"]):::producer -->|Event| B(["Broker<br/>(EventBridge)"]):::broker
    B -->|Event| C(["Consumer<br/>(Lambda)"]):::consumer

    classDef producer fill:#4A90D9,stroke:#2C5F8A,color:#FFFFFF
    classDef broker fill:#F5A623,stroke:#C07D12,color:#FFFFFF
    classDef consumer fill:#7ED321,stroke:#4A8C14,color:#FFFFFF

イベントソースの種類

// 1. 同期呼び出し(API Gateway → Lambda)
// クライアントがレスポンスを待つ
export const apiHandler = async (event: APIGatewayProxyEvent) => {
  const body = JSON.parse(event.body ?? "{}");
  const result = await processOrder(body);
  return {
    statusCode: 200,
    body: JSON.stringify(result),
  };
};

// 2. 非同期呼び出し(S3 → Lambda)
// イベント発火後、結果を待たない
export const s3Handler = async (event: S3Event) => {
  for (const record of event.Records) {
    const bucket = record.s3.bucket.name;
    const key = record.s3.object.key;
    await processUploadedFile(bucket, key);
  }
};

// 3. ストリーム処理(DynamoDB Streams → Lambda)
// 順序保証あり、バッチ処理
export const streamHandler = async (event: DynamoDBStreamEvent) => {
  for (const record of event.Records) {
    if (record.eventName === "INSERT") {
      const newItem = record.dynamodb?.NewImage;
      await publishOrderEvent(newItem);
    }
  }
};

Fan-out / Fan-in パターン

Fan-out: 1つのイベントから複数の処理を並列実行

graph LR
    SNS(["SNS Topic<br/>(注文確定)"]):::sns --> A["Lambda A<br/>(メール送信)"]:::lambda
    SNS --> B["Lambda B<br/>(在庫更新)"]:::lambda
    SNS --> C["Lambda C<br/>(分析データ送信)"]:::lambda

    classDef sns fill:#E74C3C,stroke:#A93226,color:#FFFFFF
    classDef lambda fill:#F39C12,stroke:#B7770D,color:#FFFFFF
// SNS Topic に注文イベントを発行
import { SNSClient, PublishCommand } from "@aws-sdk/client-sns";

const sns = new SNSClient({ region: "ap-northeast-1" });

export const publishOrderEvent = async (order: Order) => {
  await sns.send(new PublishCommand({
    TopicArn: process.env.ORDER_TOPIC_ARN,
    Message: JSON.stringify({
      orderId: order.id,
      customerId: order.customerId,
      totalAmount: order.totalAmount,
      items: order.items,
    }),
    MessageAttributes: {
      eventType: {
        DataType: "String",
        StringValue: "ORDER_CONFIRMED",
      },
    },
  }));
};

Fan-in: 複数の結果を集約

graph LR
    A["Lambda A<br/>(画像リサイズ S)"]:::lambda --> SF(["Step Functions"]):::step
    B["Lambda B<br/>(画像リサイズ M)"]:::lambda --> SF
    C["Lambda C<br/>(画像リサイズ L)"]:::lambda --> SF
    SF --> N["完了通知"]:::notify

    classDef lambda fill:#F39C12,stroke:#B7770D,color:#FFFFFF
    classDef step fill:#9B59B6,stroke:#6C3483,color:#FFFFFF
    classDef notify fill:#2ECC71,stroke:#1E8449,color:#FFFFFF

Choreography vs Orchestration

Choreography(コレオグラフィー)

各サービスがイベントに反応して自律的に動作します。

graph TD
    OS["Order Service"]:::order -->|event| EB{{"EventBridge"}}:::eventbridge
    EB --> PS["Payment<br/>Service"]:::service
    EB --> IS["Inventory<br/>Service"]:::service
    EB --> NS["Notification<br/>Service"]:::service
    PS -->|event| SH["Shipping<br/>Service"]:::downstream
    IS -->|event| AN["Analytics<br/>Service"]:::downstream
    NS -->|event| NS_END[ ]:::hidden

    classDef order fill:#3498DB,stroke:#21618C,color:#FFFFFF
    classDef eventbridge fill:#E74C3C,stroke:#A93226,color:#FFFFFF
    classDef service fill:#F39C12,stroke:#B7770D,color:#FFFFFF
    classDef downstream fill:#2ECC71,stroke:#1E8449,color:#FFFFFF
    classDef hidden fill:none,stroke:none
# EventBridge Rule
Resources:
  OrderConfirmedRule:
    Type: AWS::Events::Rule
    Properties:
      EventBusName: !Ref OrderEventBus
      EventPattern:
        source:
          - "order-service"
        detail-type:
          - "OrderConfirmed"
      Targets:
        - Arn: !GetAtt PaymentFunction.Arn
          Id: payment
        - Arn: !GetAtt InventoryFunction.Arn
          Id: inventory
        - Arn: !GetAtt NotificationFunction.Arn
          Id: notification

Orchestration(オーケストレーション)

中央のオーケストレーター(Step Functions)がフローを制御します。

graph TD
    SF(["Step Functions"]):::step --> INV["1. 在庫確認<br/>(Lambda)"]:::lambda
    INV -->|成功| PAY["2. 決済処理<br/>(Lambda)"]:::lambda
    INV -->|失敗| INV_ERR["在庫不足通知"]:::error
    PAY -->|成功| SHIP["3. 出荷指示<br/>(Lambda)"]:::lambda
    PAY -->|失敗| PAY_ERR["在庫ロールバック"]:::error
    SHIP --> DONE["4. 完了通知<br/>(Lambda)"]:::lambda

    classDef step fill:#9B59B6,stroke:#6C3483,color:#FFFFFF
    classDef lambda fill:#F39C12,stroke:#B7770D,color:#FFFFFF
    classDef error fill:#E74C3C,stroke:#A93226,color:#FFFFFF

使い分け

観点ChoreographyOrchestration
複雑さサービス数が少ない時複雑なワークフロー
結合度疎結合オーケストレーターに依存
可視性分散して追跡困難フロー全体が可視化
エラー処理各サービスで個別に一元管理
適用例通知、ログ、分析注文処理、承認フロー

非同期パターン

Request-Response with Callback

// 1. クライアントがリクエスト(即座にジョブIDを返す)
export const submitJob = async (event: APIGatewayProxyEvent) => {
  const jobId = ulid();
  const body = JSON.parse(event.body ?? "{}");

  // SQS にジョブを投入
  await sqs.send(new SendMessageCommand({
    QueueUrl: process.env.JOB_QUEUE_URL,
    MessageBody: JSON.stringify({ jobId, ...body }),
  }));

  return {
    statusCode: 202,  // Accepted
    body: JSON.stringify({ jobId, status: "PROCESSING" }),
    headers: {
      "Location": `/jobs/${jobId}`,  // ポーリング用
    },
  };
};

// 2. Worker が非同期で処理
export const processJob = async (event: SQSEvent) => {
  for (const record of event.Records) {
    const job = JSON.parse(record.body);
    const result = await heavyProcessing(job);

    // 結果を保存
    await dynamodb.send(new PutCommand({
      TableName: process.env.JOBS_TABLE,
      Item: { jobId: job.jobId, status: "COMPLETED", result },
    }));

    // Webhook でコールバック(オプション)
    if (job.callbackUrl) {
      await fetch(job.callbackUrl, {
        method: "POST",
        body: JSON.stringify({ jobId: job.jobId, result }),
      });
    }
  }
};

// 3. クライアントが結果をポーリング
export const getJobStatus = async (event: APIGatewayProxyEvent) => {
  const jobId = event.pathParameters?.jobId;
  const result = await dynamodb.send(new GetCommand({
    TableName: process.env.JOBS_TABLE,
    Key: { jobId },
  }));

  return {
    statusCode: result.Item ? 200 : 404,
    body: JSON.stringify(result.Item ?? { error: "Not found" }),
  };
};

Dead Letter Queue (DLQ) パターン

# SAM Template
Resources:
  OrderQueue:
    Type: AWS::SQS::Queue
    Properties:
      VisibilityTimeout: 300
      RedrivePolicy:
        deadLetterTargetArn: !GetAtt OrderDLQ.Arn
        maxReceiveCount: 3  # 3回失敗したらDLQへ

  OrderDLQ:
    Type: AWS::SQS::Queue
    Properties:
      MessageRetentionPeriod: 1209600  # 14日

  DLQAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: OrderDLQMessages
      MetricName: ApproximateNumberOfMessagesVisible
      Namespace: AWS/SQS
      Dimensions:
        - Name: QueueName
          Value: !GetAtt OrderDLQ.QueueName
      Statistic: Sum
      Period: 300
      EvaluationPeriods: 1
      Threshold: 1
      ComparisonOperator: GreaterThanOrEqualToThreshold
      AlarmActions:
        - !Ref AlertSNSTopic

まとめ

パターン用途AWS サービス
イベント駆動リアルタイム処理EventBridge, SNS, SQS
Fan-out並列処理SNS → 複数 Lambda
Choreography疎結合なサービス連携EventBridge Rules
Orchestration複雑なワークフローStep Functions
非同期 Request重い処理のオフロードSQS + DynamoDB
DLQ失敗メッセージの隔離SQS DLQ

チェックリスト

  • イベント駆動アーキテクチャの基本パターンを理解している
  • Fan-out / Fan-in パターンを適用できる
  • Choreography と Orchestration の使い分けができる
  • 非同期パターン(Callback, Polling)を実装できる
  • DLQ によるエラーハンドリングを設計できる

次のステップへ

次のレッスンでは、Lambda/Cloud Functions の設計パターンとコールドスタート対策を学びます。