サーバーレスアーキテクチャパターンと実装戦略

サーバーレスAWS LambdaAzure Functionsコスト最適化クラウドCloudflare WorkersVercel Functionsアーキテクチャ監視運用

サーバーレスアーキテクチャパターンと実装戦略

概要

サーバーレスアーキテクチャは、2024年において現代的なWebアプリケーション開発の基盤となっています。インフラ管理の削減、自動スケーリング、そして「使った分だけ支払う」コスト構造により、開発者は本来の価値創造に集中できるようになりました。この記事では、最新のサーバーレス技術(AWS Lambda、Azure Functions、Cloudflare Workers、Vercel Functions、Deno Deploy、Bun)を使った実装戦略、コスト最適化手法、監視パターンを詳解します。

サーバーレスアーキテクチャの基本概念

サーバーレスとは何か

サーバーレスコンピューティングは、クラウドプロバイダーがサーバーインフラの管理を担い、開発者がコードの実行に集中できるクラウドコンピューティングモデルです。「サーバーレス」という名称にも関わらず、実際にはサーバーが存在しますが、その管理責任がクラウドプロバイダーに移管されています。

主要な特徴

  • イベント駆動実行: リクエストやイベントに応じて自動的に関数が実行される
  • 自動スケーリング: トラフィック量に応じて自動的にスケールアップ・ダウン
  • 従量課金: 実際の使用量(実行時間・リクエスト数)に基づく課金
  • ステートレス: 各実行が独立しており、状態を保持しない
  • 短時間実行: 一般的に数秒から数分の短期実行に適している

主要プラットフォーム比較

AWS Lambda

特徴:

  • 最も成熟したサーバーレスプラットフォーム
  • 豊富なAWSサービスとの統合
  • 広範囲なランタイムサポート(Node.js、Python、Java、.NET、Go、Ruby、Rust)

料金構造(2024年):

- 無料利用枠: 月100万リクエスト、400,000 GB秒
- リクエスト料金: $0.20 / 100万リクエスト
- 実行時間料金: メモリ割り当てと実行時間に基づく
- メモリ: 128MB〜10,240MBまで設定可能

コード例 - 基本的なLambda関数:

// AWS Lambda Handler (Node.js)
export const handler = async (event, context) => {
    try {
        const { httpMethod, path, body } = event;
        
        // ビジネスロジック
        const result = await processRequest(httpMethod, path, body);
        
        return {
            statusCode: 200,
            headers: {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            body: JSON.stringify(result)
        };
    } catch (error) {
        console.error('Lambda execution error:', error);
        return {
            statusCode: 500,
            body: JSON.stringify({ error: 'Internal Server Error' })
        };
    }
};

async function processRequest(method, path, body) {
    // データベース接続
    const { Pool } = require('pg');
    const pool = new Pool({
        connectionString: process.env.DATABASE_URL,
        ssl: { rejectUnauthorized: false }
    });
    
    switch(method) {
        case 'GET':
            return await handleGet(pool, path);
        case 'POST':
            return await handlePost(pool, JSON.parse(body));
        default:
            throw new Error(`Unsupported method: ${method}`);
    }
}

Azure Functions

特徴:

  • Microsoft生態系との強力な統合
  • 複数のホスティングプラン(従量課金、Premium、App Service)
  • Durable Functionsによるステートフルなワークフロー

料金構造:

- 無料利用枠: 月100万リクエスト
- リクエスト料金: $0.20 / 100万リクエスト(Lambdaと同等)
- 実行時間料金: GB秒あたりの課金

コード例 - Azure Functions with Durable Functions:

// Azure Durable Functions(C#)
[FunctionName("OrderProcessingOrchestrator")]
public static async Task<string> RunOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var orderId = context.GetInput<string>();
    
    try
    {
        // ステップ1: 在庫確認
        var inventoryResult = await context.CallActivityAsync<bool>(
            "CheckInventory", orderId);
            
        if (!inventoryResult)
        {
            return "在庫不足";
        }
        
        // ステップ2: 決済処理
        var paymentResult = await context.CallActivityAsync<bool>(
            "ProcessPayment", orderId);
            
        if (!paymentResult)
        {
            return "決済失敗";
        }
        
        // ステップ3: 配送手配
        await context.CallActivityAsync("ArrangeShipping", orderId);
        
        return "注文処理完了";
    }
    catch (Exception ex)
    {
        // エラーハンドリングと補償処理
        await context.CallActivityAsync("HandleError", ex.Message);
        throw;
    }
}

[FunctionName("CheckInventory")]
public static async Task<bool> CheckInventory(
    [ActivityTrigger] string orderId,
    ILogger log)
{
    log.LogInformation($"在庫確認開始: {orderId}");
    
    // 在庫管理システムとの連携
    using var client = new HttpClient();
    var response = await client.GetAsync(
        $"{Environment.GetEnvironmentVariable("INVENTORY_API")}/check/{orderId}");
    
    return response.IsSuccessStatusCode;
}

Cloudflare Workers

特徴:

  • エッジコンピューティングでの超低レイテンシー
  • V8 JavaScriptエンジンによる高速起動(コールドスタートほぼゼロ)
  • CPU時間ベースの課金(待機時間は課金対象外)

料金構造:

- 無料利用枠: 日10万リクエスト
- 有料プラン: $5/月〜、月1,000万リクエスト込み
- 追加リクエスト: $0.30 / 100万リクエスト
- メモリ: 128MB固定

コード例 - Cloudflare Workers with KV Storage:

// Cloudflare Workers with KV
export default {
    async fetch(request, env, ctx) {
        const url = new URL(request.url);
        const cacheKey = `cache:${url.pathname}`;
        
        // KVストレージからキャッシュ取得
        let cached = await env.CACHE_KV.get(cacheKey, 'json');
        
        if (cached && Date.now() - cached.timestamp < 3600000) { // 1時間キャッシュ
            return new Response(JSON.stringify(cached.data), {
                headers: {
                    'Content-Type': 'application/json',
                    'Cache-Control': 'public, max-age=3600',
                    'X-Cache': 'HIT'
                }
            });
        }
        
        try {
            // 外部APIからデータ取得
            const apiResponse = await fetch(`${env.API_BASE_URL}${url.pathname}`, {
                headers: {
                    'Authorization': `Bearer ${env.API_TOKEN}`
                }
            });
            
            if (!apiResponse.ok) {
                throw new Error(`API error: ${apiResponse.status}`);
            }
            
            const data = await apiResponse.json();
            
            // KVに結果をキャッシュ(非同期、レスポンスをブロックしない)
            ctx.waitUntil(env.CACHE_KV.put(cacheKey, JSON.stringify({
                data,
                timestamp: Date.now()
            })));
            
            return new Response(JSON.stringify(data), {
                headers: {
                    'Content-Type': 'application/json',
                    'Cache-Control': 'public, max-age=3600',
                    'X-Cache': 'MISS'
                }
            });
            
        } catch (error) {
            console.error('Worker error:', error);
            
            // フォールバック処理
            if (cached) {
                return new Response(JSON.stringify(cached.data), {
                    headers: {
                        'Content-Type': 'application/json',
                        'X-Cache': 'STALE'
                    }
                });
            }
            
            return new Response(JSON.stringify({ error: 'Service unavailable' }), {
                status: 503,
                headers: { 'Content-Type': 'application/json' }
            });
        }
    }
};

Vercel Functions

特徴:

  • Next.jsとの完全統合
  • エッジランタイムサポート
  • 自動的なAPI Routes最適化

コード例 - Next.js API Routes with after():

// pages/api/analytics.ts または app/api/analytics/route.ts
import { after } from 'next/server';

export async function POST(request: Request) {
    const body = await request.json();
    
    // メインのレスポンス処理
    const result = await processMainRequest(body);
    
    // レスポンス後に実行される非同期処理
    after(async () => {
        // 分析データの送信(レスポンスをブロックしない)
        await sendAnalytics({
            userId: body.userId,
            action: body.action,
            timestamp: new Date(),
            metadata: result.metadata
        });
        
        // ログの記録
        await logActivity({
            type: 'api_call',
            endpoint: '/api/analytics',
            duration: Date.now() - startTime,
            success: true
        });
    });
    
    return Response.json(result);
}

async function sendAnalytics(data: AnalyticsData) {
    try {
        await fetch(process.env.ANALYTICS_ENDPOINT, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data)
        });
    } catch (error) {
        console.error('Analytics sending failed:', error);
    }
}

新興ランタイム

Deno Deploy

特徴:

  • TypeScript/JavaScriptのエッジランタイム
  • 標準Web API準拠
  • NPM互換性とセキュアなデフォルト

コード例:

// Deno Deploy Function
import { serve } from "https://deno.land/[email protected]/http/server.ts";

interface RequestData {
    message: string;
    timestamp: number;
}

serve(async (request: Request): Promise<Response> => {
    const url = new URL(request.url);
    
    // CORS対応
    if (request.method === "OPTIONS") {
        return new Response(null, {
            status: 200,
            headers: {
                "Access-Control-Allow-Origin": "*",
                "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
                "Access-Control-Allow-Headers": "Content-Type",
            },
        });
    }
    
    if (url.pathname === "/api/data" && request.method === "POST") {
        try {
            const data: RequestData = await request.json();
            
            // Deno KVを使用したデータストレージ
            const kv = await Deno.openKv();
            const key = ["messages", Date.now()];
            await kv.set(key, data);
            
            // レスポンス
            return Response.json({ 
                success: true, 
                id: key[1] 
            }, {
                headers: {
                    "Access-Control-Allow-Origin": "*",
                    "Content-Type": "application/json",
                },
            });
            
        } catch (error) {
            return Response.json({ 
                error: "Invalid request" 
            }, { 
                status: 400,
                headers: {
                    "Access-Control-Allow-Origin": "*",
                    "Content-Type": "application/json",
                },
            });
        }
    }
    
    return new Response("Not Found", { status: 404 });
});

Bun runtime

特徴:

  • JavaScriptランタイムの高速実装
  • 組み込みのバンドラー、パッケージマネージャー
  • Node.js互換性

コード例:

// Bun Server
import { serve } from "bun";

const server = serve({
    port: process.env.PORT || 3000,
    async fetch(request) {
        const url = new URL(request.url);
        
        if (url.pathname === "/api/health") {
            return new Response(JSON.stringify({
                status: "healthy",
                runtime: "bun",
                timestamp: Date.now(),
                memory: process.memoryUsage()
            }), {
                headers: { "Content-Type": "application/json" }
            });
        }
        
        if (url.pathname === "/api/file" && request.method === "POST") {
            const formData = await request.formData();
            const file = formData.get("file") as File;
            
            if (!file) {
                return new Response("No file uploaded", { status: 400 });
            }
            
            // Bunの高速ファイル処理
            const buffer = await file.arrayBuffer();
            const filename = `uploads/${Date.now()}-${file.name}`;
            
            await Bun.write(filename, buffer);
            
            return Response.json({
                filename,
                size: buffer.byteLength,
                type: file.type
            });
        }
        
        return new Response("Not Found", { status: 404 });
    }
});

console.log(`Bun server running on port ${server.port}`);

サーバーレスアーキテクチャパターン

1. マイクロサービスパターン

各機能を独立したサーバーレス関数として実装し、疎結合なアーキテクチャを実現します。

実装例 - API Gateway + Lambda:

# serverless.yml (Serverless Framework)
service: microservices-api

provider:
  name: aws
  runtime: nodejs18.x
  stage: ${opt:stage, 'dev'}
  region: ap-northeast-1
  
  environment:
    USERS_TABLE: ${self:service}-users-${self:provider.stage}
    ORDERS_TABLE: ${self:service}-orders-${self:provider.stage}
    
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource:
        - arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.USERS_TABLE}
        - arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.ORDERS_TABLE}

functions:
  # ユーザーサービス
  getUserById:
    handler: src/users/get.handler
    events:
      - http:
          path: users/{id}
          method: get
          cors: true
          
  createUser:
    handler: src/users/create.handler
    events:
      - http:
          path: users
          method: post
          cors: true
          
  # 注文サービス  
  getOrderById:
    handler: src/orders/get.handler
    events:
      - http:
          path: orders/{id}
          method: get
          cors: true
          
  createOrder:
    handler: src/orders/create.handler
    events:
      - http:
          path: orders
          method: post
          cors: true
          
  # 非同期処理
  processPayment:
    handler: src/payments/process.handler
    events:
      - sqs:
          arn: arn:aws:sqs:${self:provider.region}:*:payment-queue
          
resources:
  Resources:
    UsersTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:provider.environment.USERS_TABLE}
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
            
    OrdersTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:provider.environment.ORDERS_TABLE}
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
          - AttributeName: userId
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        GlobalSecondaryIndexes:
          - IndexName: UserIdIndex
            KeySchema:
              - AttributeName: userId
                KeyType: HASH
            Projection:
              ProjectionType: ALL

2. イベント駆動パターン

イベントをトリガーとして関数を実行し、リアクティブなシステムを構築します。

実装例 - EventBridge + Lambda:

// src/events/orderCreated.js
const { EventBridgeClient, PutEventsCommand } = require("@aws-sdk/client-eventbridge");

const eventBridge = new EventBridgeClient({ region: process.env.AWS_REGION });

exports.handler = async (event) => {
    console.log('Order created event:', JSON.stringify(event, null, 2));
    
    try {
        const order = JSON.parse(event.Records[0].body);
        
        // 複数のイベントを並行発行
        const events = [
            {
                Source: 'order.service',
                DetailType: 'Order Created',
                Detail: JSON.stringify({
                    orderId: order.id,
                    userId: order.userId,
                    amount: order.total,
                    timestamp: new Date().toISOString()
                })
            },
            {
                Source: 'inventory.service', 
                DetailType: 'Inventory Update Required',
                Detail: JSON.stringify({
                    orderId: order.id,
                    items: order.items,
                    action: 'reserve'
                })
            }
        ];
        
        const command = new PutEventsCommand({
            Entries: events
        });
        
        await eventBridge.send(command);
        
        console.log('Events published successfully');
        
    } catch (error) {
        console.error('Event processing failed:', error);
        throw error;
    }
};

// src/events/inventoryUpdater.js
exports.handler = async (event) => {
    console.log('Inventory update event:', JSON.stringify(event, null, 2));
    
    const { orderId, items, action } = event.detail;
    
    try {
        for (const item of items) {
            if (action === 'reserve') {
                await reserveInventory(item.productId, item.quantity);
            } else if (action === 'release') {
                await releaseInventory(item.productId, item.quantity);
            }
        }
        
        // 在庫更新完了イベントを発行
        await publishInventoryUpdatedEvent(orderId, items, action);
        
    } catch (error) {
        console.error('Inventory update failed:', error);
        // デッドレターキューへの送信またはリトライ処理
        throw error;
    }
};

async function reserveInventory(productId, quantity) {
    const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
    const { DynamoDBDocumentClient, UpdateCommand } = require("@aws-sdk/lib-dynamodb");
    
    const client = new DynamoDBClient({ region: process.env.AWS_REGION });
    const docClient = DynamoDBDocumentClient.from(client);
    
    const command = new UpdateCommand({
        TableName: process.env.INVENTORY_TABLE,
        Key: { productId },
        UpdateExpression: 'SET availableQuantity = availableQuantity - :qty, reservedQuantity = reservedQuantity + :qty',
        ConditionExpression: 'availableQuantity >= :qty',
        ExpressionAttributeValues: {
            ':qty': quantity
        },
        ReturnValues: 'UPDATED_NEW'
    });
    
    return await docClient.send(command);
}

3. CQRS (Command Query Responsibility Segregation) パターン

読み取りと書き込みを分離し、それぞれに最適化された処理を実装します。

実装例 - Lambda + DynamoDB + ElasticSearch:

// src/commands/createProduct.js
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
const { DynamoDBDocumentClient, PutCommand } = require("@aws-sdk/lib-dynamodb");
const { SQSClient, SendMessageCommand } = require("@aws-sdk/client-sqs");

const dynamoClient = new DynamoDBClient({ region: process.env.AWS_REGION });
const docClient = DynamoDBDocumentClient.from(dynamoClient);
const sqsClient = new SQSClient({ region: process.env.AWS_REGION });

exports.handler = async (event) => {
    try {
        const product = JSON.parse(event.body);
        const productId = generateId();
        
        const productData = {
            ...product,
            id: productId,
            createdAt: new Date().toISOString(),
            updatedAt: new Date().toISOString()
        };
        
        // Command側(書き込み最適化): DynamoDBに保存
        const putCommand = new PutCommand({
            TableName: process.env.PRODUCTS_TABLE,
            Item: productData
        });
        
        await docClient.send(putCommand);
        
        // Query側の更新をSQSで非同期実行
        const message = new SendMessageCommand({
            QueueUrl: process.env.PRODUCT_SYNC_QUEUE,
            MessageBody: JSON.stringify({
                action: 'CREATE',
                product: productData
            })
        });
        
        await sqsClient.send(message);
        
        return {
            statusCode: 201,
            headers: {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            body: JSON.stringify({
                id: productId,
                message: 'Product created successfully'
            })
        };
        
    } catch (error) {
        console.error('Product creation failed:', error);
        return {
            statusCode: 500,
            body: JSON.stringify({ error: 'Internal server error' })
        };
    }
};

// src/queries/searchProducts.js
const { Client } = require('@elastic/elasticsearch');

const esClient = new Client({
    node: process.env.ELASTICSEARCH_ENDPOINT,
    auth: {
        username: process.env.ES_USERNAME,
        password: process.env.ES_PASSWORD
    }
});

exports.handler = async (event) => {
    try {
        const { query, category, priceRange, sortBy } = event.queryStringParameters || {};
        
        const searchBody = {
            query: {
                bool: {
                    must: [],
                    filter: []
                }
            },
            sort: [],
            size: 20
        };
        
        // テキスト検索
        if (query) {
            searchBody.query.bool.must.push({
                multi_match: {
                    query,
                    fields: ['name^2', 'description', 'tags'],
                    type: 'best_fields',
                    fuzziness: 'AUTO'
                }
            });
        }
        
        // カテゴリフィルター
        if (category) {
            searchBody.query.bool.filter.push({
                term: { category }
            });
        }
        
        // 価格範囲フィルター
        if (priceRange) {
            const [min, max] = priceRange.split('-').map(Number);
            searchBody.query.bool.filter.push({
                range: {
                    price: { gte: min, lte: max }
                }
            });
        }
        
        // ソート
        if (sortBy === 'price_asc') {
            searchBody.sort.push({ price: 'asc' });
        } else if (sortBy === 'price_desc') {
            searchBody.sort.push({ price: 'desc' });
        } else {
            searchBody.sort.push({ _score: 'desc' });
        }
        
        const response = await esClient.search({
            index: 'products',
            body: searchBody
        });
        
        const products = response.body.hits.hits.map(hit => ({
            id: hit._id,
            ...hit._source,
            score: hit._score
        }));
        
        return {
            statusCode: 200,
            headers: {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*',
                'Cache-Control': 'public, max-age=300' // 5分キャッシュ
            },
            body: JSON.stringify({
                products,
                total: response.body.hits.total.value,
                took: response.body.took
            })
        };
        
    } catch (error) {
        console.error('Product search failed:', error);
        return {
            statusCode: 500,
            body: JSON.stringify({ error: 'Search failed' })
        };
    }
};

// src/sync/productSyncProcessor.js
const { Client } = require('@elastic/elasticsearch');

const esClient = new Client({
    node: process.env.ELASTICSEARCH_ENDPOINT,
    auth: {
        username: process.env.ES_USERNAME,
        password: process.env.ES_PASSWORD
    }
});

exports.handler = async (event) => {
    const promises = event.Records.map(async (record) => {
        try {
            const { action, product } = JSON.parse(record.body);
            
            switch (action) {
                case 'CREATE':
                case 'UPDATE':
                    await esClient.index({
                        index: 'products',
                        id: product.id,
                        body: {
                            name: product.name,
                            description: product.description,
                            price: product.price,
                            category: product.category,
                            tags: product.tags || [],
                            createdAt: product.createdAt,
                            updatedAt: product.updatedAt
                        }
                    });
                    break;
                    
                case 'DELETE':
                    await esClient.delete({
                        index: 'products',
                        id: product.id
                    });
                    break;
            }
            
            console.log(`Product ${action} synced to Elasticsearch:`, product.id);
            
        } catch (error) {
            console.error('Sync failed for record:', record, error);
            throw error; // SQSでリトライされる
        }
    });
    
    await Promise.all(promises);
};

コスト最適化戦略

1. プラットフォーム別コスト分析

実行時間とメモリの最適化

AWS Lambda最適化例:

// メモリとパフォーマンスの関係を測定
const AWS = require('aws-sdk');

// メモリ使用量の監視
function getMemoryUsage() {
    const used = process.memoryUsage();
    return {
        rss: Math.round(used.rss / 1024 / 1024 * 100) / 100,
        heapTotal: Math.round(used.heapTotal / 1024 / 1024 * 100) / 100,
        heapUsed: Math.round(used.heapUsed / 1024 / 1024 * 100) / 100,
        external: Math.round(used.external / 1024 / 1024 * 100) / 100
    };
}

exports.handler = async (event, context) => {
    const startTime = Date.now();
    const startMemory = getMemoryUsage();
    
    try {
        // 重い処理の実行
        const result = await processLargeDataset(event.data);
        
        const endTime = Date.now();
        const endMemory = getMemoryUsage();
        
        // パフォーマンスメトリクスをCloudWatchに送信
        await sendMetrics({
            executionTime: endTime - startTime,
            memoryUsed: endMemory.heapUsed,
            allocatedMemory: parseInt(context.memoryLimitInMB),
            requestSize: JSON.stringify(event).length
        });
        
        return {
            statusCode: 200,
            body: JSON.stringify(result)
        };
        
    } catch (error) {
        console.error('Processing error:', error);
        throw error;
    }
};

async function sendMetrics(metrics) {
    const cloudwatch = new AWS.CloudWatch();
    
    const params = {
        Namespace: 'Lambda/Performance',
        MetricData: [
            {
                MetricName: 'ExecutionTime',
                Value: metrics.executionTime,
                Unit: 'Milliseconds'
            },
            {
                MetricName: 'MemoryUtilization',
                Value: (metrics.memoryUsed / metrics.allocatedMemory) * 100,
                Unit: 'Percent'
            }
        ]
    };
    
    await cloudwatch.putMetricData(params).promise();
}

コスト監視とアラート

Terraform設定例:

# cost-monitoring.tf
resource "aws_cloudwatch_metric_alarm" "lambda_cost_alarm" {
  alarm_name          = "lambda-monthly-cost-alarm"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "1"
  metric_name         = "EstimatedCharges"
  namespace           = "AWS/Billing"
  period              = "86400" # 1日
  statistic           = "Maximum"
  threshold           = "100" # $100
  alarm_description   = "This metric monitors lambda monthly costs"
  alarm_actions       = [aws_sns_topic.cost_alerts.arn]

  dimensions = {
    Currency = "USD"
    ServiceName = "AWSLambda"
  }
}

resource "aws_sns_topic" "cost_alerts" {
  name = "lambda-cost-alerts"
}

resource "aws_sns_topic_subscription" "email_notification" {
  topic_arn = aws_sns_topic.cost_alerts.arn
  protocol  = "email"
  endpoint  = "[email protected]"
}

# Lambda関数の予約並行性設定(コスト制御)
resource "aws_lambda_function" "cost_optimized" {
  filename         = "function.zip"
  function_name    = "cost-optimized-function"
  role            = aws_iam_role.lambda_role.arn
  handler         = "index.handler"
  runtime         = "nodejs18.x"
  
  # メモリ最適化(パフォーマンステストに基づく)
  memory_size = 256
  timeout     = 30
  
  # 予約並行性でコスト制御
  reserved_concurrent_executions = 50
  
  environment {
    variables = {
      NODE_ENV = "production"
      # 接続プールサイズを調整
      DB_POOL_SIZE = "5"
    }
  }
}

# プロビジョニング済み並行性(コールドスタート対策、コスト要注意)
resource "aws_lambda_provisioned_concurrency_config" "example" {
  function_name                     = aws_lambda_function.cost_optimized.function_name
  provisioned_concurrent_executions = 2
  qualifier                        = aws_lambda_function.cost_optimized.version
}

2. Cloudflare Workersのコスト効率性

CPU時間ベース課金の活用:

// Cloudflare Workers - I/O待機時間は課金されない
export default {
    async fetch(request, env, ctx) {
        const startTime = Date.now();
        
        // 複数のAPIを並列呼び出し(I/O待機時間は課金対象外)
        const [userPromise, ordersPromise, inventoryPromise] = await Promise.allSettled([
            fetch(`${env.API_BASE}/users/${userId}`, {
                headers: { 'Authorization': `Bearer ${env.API_TOKEN}` }
            }),
            fetch(`${env.API_BASE}/orders/${userId}`, {
                headers: { 'Authorization': `Bearer ${env.API_TOKEN}` }
            }),
            fetch(`${env.API_BASE}/inventory/${productId}`, {
                headers: { 'Authorization': `Bearer ${env.API_TOKEN}` }
            })
        ]);
        
        // CPU集約的な処理(この部分のみ課金対象)
        const processStartTime = Date.now();
        
        const results = await Promise.all([
            userPromise.status === 'fulfilled' ? userPromise.value.json() : null,
            ordersPromise.status === 'fulfilled' ? ordersPromise.value.json() : null,
            inventoryPromise.status === 'fulfilled' ? inventoryPromise.value.json() : null
        ]);
        
        const aggregatedData = aggregateUserData(results);
        
        const cpuTime = Date.now() - processStartTime;
        const totalTime = Date.now() - startTime;
        
        // メトリクス記録(非同期、課金対象外)
        ctx.waitUntil(logMetrics({
            cpuTime,
            totalTime,
            ioRatio: (totalTime - cpuTime) / totalTime
        }));
        
        return Response.json(aggregatedData);
    }
};

function aggregateUserData([user, orders, inventory]) {
    // CPU集約的なデータ処理
    const aggregated = {
        user: user || {},
        orderSummary: orders ? {
            total: orders.reduce((sum, order) => sum + order.amount, 0),
            count: orders.length,
            recent: orders.slice(0, 5)
        } : {},
        inventoryStatus: inventory || {}
    };
    
    return aggregated;
}

3. コスト比較シミュレーター

使用量別コスト計算:

// cost-calculator.js
class ServerlessCostCalculator {
    constructor() {
        this.providers = {
            lambda: {
                freeRequests: 1000000, // 月間
                freeGBSeconds: 400000,
                requestPrice: 0.0000002, // $0.20 per 1M requests
                gbSecondPrice: 0.0000166667 // GB-second価格
            },
            cloudflareWorkers: {
                freeRequests: 100000, // 日間
                paidPlanBase: 5, // $5/月
                includedRequests: 10000000, // 有料プランに含まれる
                additionalRequestPrice: 0.0000003 // $0.30 per 1M requests
            },
            vercel: {
                freeRequests: 100000, // 月間
                paidRequestPrice: 0.0000004 // $0.40 per 1M requests
            }
        };
    }
    
    calculateLambdaCost(monthlyRequests, avgMemoryMB, avgDurationMs) {
        const { lambda } = this.providers;
        
        // リクエスト料金
        const billableRequests = Math.max(0, monthlyRequests - lambda.freeRequests);
        const requestCost = billableRequests * lambda.requestPrice;
        
        // 実行時間料金
        const gbSeconds = (avgMemoryMB / 1024) * (avgDurationMs / 1000) * monthlyRequests;
        const billableGBSeconds = Math.max(0, gbSeconds - lambda.freeGBSeconds);
        const computeCost = billableGBSeconds * lambda.gbSecondPrice;
        
        return {
            requestCost,
            computeCost,
            total: requestCost + computeCost,
            breakdown: {
                billableRequests,
                gbSeconds: billableGBSeconds
            }
        };
    }
    
    calculateWorkersCost(monthlyRequests) {
        const { cloudflareWorkers } = this.providers;
        const dailyRequests = monthlyRequests / 30;
        
        // 無料プランで収まるか確認
        if (dailyRequests <= cloudflareWorkers.freeRequests) {
            return { total: 0, plan: 'free' };
        }
        
        // 有料プランの計算
        const baseCost = cloudflareWorkers.paidPlanBase;
        const additionalRequests = Math.max(0, monthlyRequests - cloudflareWorkers.includedRequests);
        const additionalCost = additionalRequests * cloudflareWorkers.additionalRequestPrice;
        
        return {
            baseCost,
            additionalCost,
            total: baseCost + additionalCost,
            plan: 'paid'
        };
    }
    
    calculateVercelCost(monthlyRequests) {
        const { vercel } = this.providers;
        
        if (monthlyRequests <= vercel.freeRequests) {
            return { total: 0, plan: 'free' };
        }
        
        const billableRequests = monthlyRequests - vercel.freeRequests;
        const cost = billableRequests * vercel.paidRequestPrice;
        
        return {
            billableRequests,
            total: cost,
            plan: 'paid'
        };
    }
    
    compareAll(scenarios) {
        return scenarios.map(scenario => {
            const { name, monthlyRequests, avgMemoryMB = 256, avgDurationMs = 1000 } = scenario;
            
            return {
                scenario: name,
                lambda: this.calculateLambdaCost(monthlyRequests, avgMemoryMB, avgDurationMs),
                workers: this.calculateWorkersCost(monthlyRequests),
                vercel: this.calculateVercelCost(monthlyRequests)
            };
        });
    }
}

// 使用例
const calculator = new ServerlessCostCalculator();

const scenarios = [
    {
        name: 'Small API (10K requests/month)',
        monthlyRequests: 10000,
        avgMemoryMB: 128,
        avgDurationMs: 500
    },
    {
        name: 'Medium API (1M requests/month)', 
        monthlyRequests: 1000000,
        avgMemoryMB: 256,
        avgDurationMs: 1000
    },
    {
        name: 'Large API (10M requests/month)',
        monthlyRequests: 10000000,
        avgMemoryMB: 512,
        avgDurationMs: 2000
    }
];

const costComparison = calculator.compareAll(scenarios);
console.table(costComparison);

Infrastructure as Code (IaC) 実装

AWS CDK実装例

// infrastructure/serverless-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
import * as sqs from 'aws-cdk-lib/aws-sqs';
import * as lambdaEventSources from 'aws-cdk-lib/aws-lambda-event-sources';
import { Construct } from 'constructs';

export class ServerlessStack extends cdk.Stack {
    constructor(scope: Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);
        
        // DynamoDB テーブル
        const usersTable = new dynamodb.Table(this, 'UsersTable', {
            tableName: 'users',
            partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
            billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
            pointInTimeRecovery: true,
            stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES
        });
        
        const ordersTable = new dynamodb.Table(this, 'OrdersTable', {
            tableName: 'orders',
            partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
            sortKey: { name: 'createdAt', type: dynamodb.AttributeType.STRING },
            billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
            stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES
        });
        
        // SQS キュー(デッドレターキュー付き)
        const dlq = new sqs.Queue(this, 'ProcessingDLQ', {
            queueName: 'processing-dlq',
            retentionPeriod: cdk.Duration.days(14)
        });
        
        const processingQueue = new sqs.Queue(this, 'ProcessingQueue', {
            queueName: 'processing-queue',
            visibilityTimeout: cdk.Duration.minutes(5),
            deadLetterQueue: {
                queue: dlq,
                maxReceiveCount: 3
            }
        });
        
        // Lambda 関数の共通設定
        const commonLambdaProps = {
            runtime: lambda.Runtime.NODEJS_18_X,
            timeout: cdk.Duration.seconds(30),
            memorySize: 256,
            environment: {
                USERS_TABLE: usersTable.tableName,
                ORDERS_TABLE: ordersTable.tableName,
                PROCESSING_QUEUE_URL: processingQueue.queueUrl,
                NODE_ENV: 'production'
            },
            bundling: {
                externalModules: ['aws-sdk'], // AWS SDK v3は含めない
                minify: true,
                sourceMap: false
            }
        };
        
        // API Lambda 関数群
        const createUserFunction = new lambda.Function(this, 'CreateUserFunction', {
            ...commonLambdaProps,
            functionName: 'create-user',
            code: lambda.Code.fromAsset('src/users', { exclude: ['*.test.js'] }),
            handler: 'create.handler'
        });
        
        const getUserFunction = new lambda.Function(this, 'GetUserFunction', {
            ...commonLambdaProps,
            functionName: 'get-user',
            code: lambda.Code.fromAsset('src/users', { exclude: ['*.test.js'] }),
            handler: 'get.handler'
        });
        
        const createOrderFunction = new lambda.Function(this, 'CreateOrderFunction', {
            ...commonLambdaProps,
            functionName: 'create-order',
            code: lambda.Code.fromAsset('src/orders', { exclude: ['*.test.js'] }),
            handler: 'create.handler'
        });
        
        // 非同期処理関数
        const orderProcessorFunction = new lambda.Function(this, 'OrderProcessorFunction', {
            ...commonLambdaProps,
            functionName: 'order-processor',
            code: lambda.Code.fromAsset('src/processors', { exclude: ['*.test.js'] }),
            handler: 'orders.handler',
            timeout: cdk.Duration.minutes(5),
            reservedConcurrentExecutions: 10 // コスト制御
        });
        
        // イベント処理関数(DynamoDB Streams)
        const streamProcessorFunction = new lambda.Function(this, 'StreamProcessorFunction', {
            ...commonLambdaProps,
            functionName: 'stream-processor', 
            code: lambda.Code.fromAsset('src/processors', { exclude: ['*.test.js'] }),
            handler: 'streams.handler'
        });
        
        // 権限設定
        usersTable.grantReadWriteData(createUserFunction);
        usersTable.grantReadData(getUserFunction);
        ordersTable.grantReadWriteData(createOrderFunction);
        processingQueue.grantSendMessages(createOrderFunction);
        processingQueue.grantConsumeMessages(orderProcessorFunction);
        
        // イベントソース設定
        orderProcessorFunction.addEventSource(
            new lambdaEventSources.SqsEventSource(processingQueue, {
                batchSize: 10,
                maxBatchingWindow: cdk.Duration.seconds(5)
            })
        );
        
        streamProcessorFunction.addEventSource(
            new lambdaEventSources.DynamoEventSource(ordersTable, {
                startingPosition: lambda.StartingPosition.LATEST,
                batchSize: 100,
                maxBatchingWindow: cdk.Duration.seconds(5),
                retryAttempts: 3
            })
        );
        
        // API Gateway
        const api = new apigateway.RestApi(this, 'ServerlessApi', {
            restApiName: 'Serverless API',
            description: 'Serverless application API',
            defaultCorsPreflightOptions: {
                allowOrigins: apigateway.Cors.ALL_ORIGINS,
                allowMethods: apigateway.Cors.ALL_METHODS,
                allowHeaders: ['Content-Type', 'Authorization']
            }
        });
        
        // API エンドポイント設定
        const users = api.root.addResource('users');
        users.addMethod('POST', new apigateway.LambdaIntegration(createUserFunction));
        
        const user = users.addResource('{id}');
        user.addMethod('GET', new apigateway.LambdaIntegration(getUserFunction));
        
        const orders = api.root.addResource('orders');
        orders.addMethod('POST', new apigateway.LambdaIntegration(createOrderFunction));
        
        // EventBridge カスタムバス
        const eventBus = new events.EventBus(this, 'ServerlessEventBus', {
            eventBusName: 'serverless-events'
        });
        
        // イベントルール
        const orderCreatedRule = new events.Rule(this, 'OrderCreatedRule', {
            eventBus,
            eventPattern: {
                source: ['order.service'],
                detailType: ['Order Created']
            }
        });
        
        orderCreatedRule.addTarget(new targets.LambdaFunction(orderProcessorFunction));
        
        // CloudWatch アラーム
        createUserFunction.metricErrors().createAlarm(this, 'CreateUserErrorAlarm', {
            threshold: 5,
            evaluationPeriods: 2,
            alarmDescription: 'User creation function error rate too high'
        });
        
        createUserFunction.metricDuration().createAlarm(this, 'CreateUserDurationAlarm', {
            threshold: cdk.Duration.seconds(10).toMilliseconds(),
            evaluationPeriods: 3,
            alarmDescription: 'User creation function duration too high'
        });
        
        // 出力
        new cdk.CfnOutput(this, 'ApiEndpoint', {
            value: api.url,
            description: 'API Gateway endpoint URL'
        });
        
        new cdk.CfnOutput(this, 'EventBusArn', {
            value: eventBus.eventBusArn,
            description: 'EventBridge custom bus ARN'
        });
    }
}

// app.ts
import * as cdk from 'aws-cdk-lib';
import { ServerlessStack } from './serverless-stack';

const app = new cdk.App();

new ServerlessStack(app, 'ServerlessStack', {
    env: {
        account: process.env.CDK_DEFAULT_ACCOUNT,
        region: process.env.CDK_DEFAULT_REGION
    },
    tags: {
        Environment: 'production',
        Project: 'serverless-demo'
    }
});

Pulumi実装例(TypeScript)

// infrastructure/index.ts
import * as pulumi from '@pulumi/pulumi';
import * as aws from '@pulumi/aws';
import * as awsx from '@pulumi/awsx';

const config = new pulumi.Config();
const stage = config.get('stage') || 'dev';

// VPC設定(オプション)
const vpc = new awsx.ec2.Vpc('serverless-vpc', {
    cidrBlock: '10.0.0.0/16',
    numberOfAvailabilityZones: 2,
    tags: { Name: `serverless-vpc-${stage}` }
});

// DynamoDB テーブル
const usersTable = new aws.dynamodb.Table('users-table', {
    name: `users-${stage}`,
    hashKey: 'id',
    attributes: [{ name: 'id', type: 'S' }],
    billingMode: 'PAY_PER_REQUEST',
    pointInTimeRecovery: { enabled: true },
    streamEnabled: true,
    streamViewType: 'NEW_AND_OLD_IMAGES',
    tags: { Environment: stage }
});

// IAM ロール
const lambdaRole = new aws.iam.Role('lambda-role', {
    assumeRolePolicy: JSON.stringify({
        Version: '2012-10-17',
        Statement: [{
            Action: 'sts:AssumeRole',
            Effect: 'Allow',
            Principal: { Service: 'lambda.amazonaws.com' }
        }]
    })
});

new aws.iam.RolePolicyAttachment('lambda-basic-execution', {
    role: lambdaRole.name,
    policyArn: 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
});

new aws.iam.RolePolicyAttachment('lambda-vpc-execution', {
    role: lambdaRole.name,
    policyArn: 'arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole'
});

// Lambda 関数
const apiFunction = new aws.lambda.Function('api-function', {
    runtime: 'nodejs18.x',
    handler: 'index.handler',
    role: lambdaRole.arn,
    code: new pulumi.asset.AssetArchive({
        '.': new pulumi.asset.FileArchive('./dist')
    }),
    environment: {
        variables: {
            USERS_TABLE: usersTable.name,
            STAGE: stage,
            NODE_ENV: 'production'
        }
    },
    memorySize: 256,
    timeout: 30,
    vpcConfig: {
        subnetIds: vpc.privateSubnetIds,
        securityGroupIds: [vpc.vpc.defaultSecurityGroupId]
    },
    tags: { Environment: stage }
});

// DynamoDB 権限
new aws.iam.RolePolicy('lambda-dynamodb-policy', {
    role: lambdaRole.id,
    policy: pulumi.all([usersTable.arn]).apply(([tableArn]) => JSON.stringify({
        Version: '2012-10-17',
        Statement: [{
            Effect: 'Allow',
            Action: [
                'dynamodb:GetItem',
                'dynamodb:PutItem',
                'dynamodb:UpdateItem',
                'dynamodb:DeleteItem',
                'dynamodb:Query',
                'dynamodb:Scan'
            ],
            Resource: [tableArn, `${tableArn}/index/*`]
        }]
    }))
});

// API Gateway
const api = new awsx.apigateway.API('serverless-api', {
    routes: [
        {
            path: '/users',
            method: 'POST',
            eventHandler: apiFunction
        },
        {
            path: '/users/{id}',
            method: 'GET',
            eventHandler: apiFunction
        },
        {
            path: '/health',
            method: 'GET',
            eventHandler: async (event) => ({
                statusCode: 200,
                body: JSON.stringify({
                    status: 'healthy',
                    timestamp: new Date().toISOString()
                })
            })
        }
    ]
});

// CloudWatch Log Groups
const logGroup = new aws.cloudwatch.LogGroup('api-logs', {
    name: pulumi.interpolate`/aws/lambda/${apiFunction.name}`,
    retentionInDays: 14,
    tags: { Environment: stage }
});

// CloudWatch アラーム
const errorAlarm = new aws.cloudwatch.MetricAlarm('api-error-alarm', {
    name: `api-errors-${stage}`,
    comparisonOperator: 'GreaterThanThreshold',
    evaluationPeriods: 2,
    metricName: 'Errors',
    namespace: 'AWS/Lambda',
    period: 300,
    statistic: 'Sum',
    threshold: 5,
    alarmDescription: 'API function error rate is too high',
    dimensions: {
        FunctionName: apiFunction.name
    },
    tags: { Environment: stage }
});

// 出力
export const apiUrl = api.url;
export const functionName = apiFunction.name;
export const tableName = usersTable.name;
export const vpcId = vpc.vpcId;

監視・可観測性の実装

分散トレーシング

AWS X-Ray設定:

// src/middleware/tracing.js
const AWSXRay = require('aws-xray-sdk-core');
const AWS = AWSXRay.captureAWS(require('aws-sdk'));

// サブセグメント作成のヘルパー
function createSubsegment(name, callback) {
    const segment = AWSXRay.getSegment();
    const subsegment = segment.addNewSubsegment(name);
    
    return new Promise((resolve, reject) => {
        subsegment.addAnnotation('subsegment', name);
        
        callback(subsegment)
            .then(result => {
                subsegment.close();
                resolve(result);
            })
            .catch(error => {
                subsegment.addError(error);
                subsegment.close(error);
                reject(error);
            });
    });
}

// データベースアクセスのトレーシング
async function queryDatabase(params) {
    return createSubsegment('DynamoDB-Query', async (subsegment) => {
        subsegment.addMetadata('query', params);
        
        const dynamodb = new AWS.DynamoDB.DocumentClient();
        const result = await dynamodb.query(params).promise();
        
        subsegment.addMetadata('result', {
            itemCount: result.Items ? result.Items.length : 0,
            scannedCount: result.ScannedCount
        });
        
        return result;
    });
}

// 外部API呼び出しのトレーシング
async function callExternalAPI(url, options = {}) {
    return createSubsegment('External-API', async (subsegment) => {
        subsegment.addAnnotation('url', url);
        subsegment.addMetadata('request', options);
        
        const response = await fetch(url, options);
        
        subsegment.addAnnotation('statusCode', response.status);
        subsegment.addMetadata('response', {
            status: response.status,
            headers: Object.fromEntries(response.headers.entries())
        });
        
        if (!response.ok) {
            throw new Error(`API call failed: ${response.status}`);
        }
        
        return response.json();
    });
}

module.exports = {
    createSubsegment,
    queryDatabase,
    callExternalAPI
};

構造化ログとメトリクス

// src/utils/logger.js
class Logger {
    constructor(context) {
        this.requestId = context.awsRequestId;
        this.functionName = context.functionName;
        this.stage = process.env.STAGE || 'dev';
    }
    
    _formatLog(level, message, data = {}) {
        return JSON.stringify({
            timestamp: new Date().toISOString(),
            level,
            message,
            requestId: this.requestId,
            functionName: this.functionName,
            stage: this.stage,
            ...data
        });
    }
    
    info(message, data) {
        console.log(this._formatLog('INFO', message, data));
    }
    
    error(message, error, data = {}) {
        const errorData = {
            error: {
                name: error.name,
                message: error.message,
                stack: error.stack
            },
            ...data
        };
        console.error(this._formatLog('ERROR', message, errorData));
    }
    
    warn(message, data) {
        console.warn(this._formatLog('WARN', message, data));
    }
    
    debug(message, data) {
        if (process.env.LOG_LEVEL === 'debug') {
            console.debug(this._formatLog('DEBUG', message, data));
        }
    }
    
    // ビジネスメトリクス
    metric(metricName, value, unit = 'Count', dimensions = {}) {
        const metricData = {
            timestamp: new Date().toISOString(),
            type: 'METRIC',
            metricName,
            value,
            unit,
            dimensions: {
                functionName: this.functionName,
                stage: this.stage,
                ...dimensions
            }
        };
        
        console.log(JSON.stringify(metricData));
    }
}

// CloudWatch カスタムメトリクス送信
const AWS = require('aws-sdk');
const cloudwatch = new AWS.CloudWatch();

class MetricsCollector {
    constructor() {
        this.metrics = [];
    }
    
    addMetric(name, value, unit = 'Count', dimensions = {}) {
        this.metrics.push({
            MetricName: name,
            Value: value,
            Unit: unit,
            Dimensions: Object.entries(dimensions).map(([Name, Value]) => ({
                Name,
                Value: String(Value)
            }))
        });
    }
    
    async flush() {
        if (this.metrics.length === 0) return;
        
        const params = {
            Namespace: 'ServerlessApp/BusinessMetrics',
            MetricData: this.metrics
        };
        
        try {
            await cloudwatch.putMetricData(params).promise();
            this.metrics = [];
        } catch (error) {
            console.error('Failed to send metrics:', error);
        }
    }
}

module.exports = { Logger, MetricsCollector };

パフォーマンス監視

// src/middleware/performance.js
const { Logger, MetricsCollector } = require('../utils/logger');

class PerformanceMonitor {
    constructor(context) {
        this.logger = new Logger(context);
        this.metrics = new MetricsCollector();
        this.startTime = Date.now();
        this.checkpoints = [];
    }
    
    checkpoint(name) {
        const now = Date.now();
        const checkpoint = {
            name,
            timestamp: now,
            elapsed: now - this.startTime
        };
        
        this.checkpoints.push(checkpoint);
        this.logger.debug('Performance checkpoint', checkpoint);
        
        return checkpoint;
    }
    
    async measureAsync(name, asyncFunction) {
        const start = Date.now();
        
        try {
            const result = await asyncFunction();
            const duration = Date.now() - start;
            
            this.metrics.addMetric(`${name}.Duration`, duration, 'Milliseconds');
            this.metrics.addMetric(`${name}.Success`, 1);
            
            this.logger.info(`${name} completed`, { duration });
            
            return result;
        } catch (error) {
            const duration = Date.now() - start;
            
            this.metrics.addMetric(`${name}.Duration`, duration, 'Milliseconds');
            this.metrics.addMetric(`${name}.Error`, 1);
            
            this.logger.error(`${name} failed`, error, { duration });
            throw error;
        }
    }
    
    async finalize() {
        const totalDuration = Date.now() - this.startTime;
        
        this.metrics.addMetric('Function.Duration', totalDuration, 'Milliseconds');
        this.metrics.addMetric('Function.Invocation', 1);
        
        // メモリ使用量
        const memoryUsage = process.memoryUsage();
        this.metrics.addMetric('Function.MemoryUsed', 
            Math.round(memoryUsage.heapUsed / 1024 / 1024), 'Megabytes');
        
        this.logger.info('Function execution completed', {
            totalDuration,
            memoryUsage,
            checkpoints: this.checkpoints
        });
        
        await this.metrics.flush();
    }
}

// 使用例
exports.handler = async (event, context) => {
    const monitor = new PerformanceMonitor(context);
    
    try {
        monitor.checkpoint('start');
        
        // データベースアクセス
        const userData = await monitor.measureAsync('database.getUser', async () => {
            return await queryUser(event.userId);
        });
        
        monitor.checkpoint('user-data-loaded');
        
        // 外部API呼び出し
        const externalData = await monitor.measureAsync('api.getExternalData', async () => {
            return await fetchExternalData(userData.id);
        });
        
        monitor.checkpoint('external-data-loaded');
        
        // ビジネスロジック
        const result = await monitor.measureAsync('business.processData', async () => {
            return await processUserData(userData, externalData);
        });
        
        monitor.checkpoint('processing-completed');
        
        return {
            statusCode: 200,
            body: JSON.stringify(result)
        };
        
    } catch (error) {
        monitor.logger.error('Function execution failed', error);
        throw error;
    } finally {
        await monitor.finalize();
    }
};

セキュリティベストプラクティス

認証・認可

// src/middleware/auth.js
const jwt = require('jsonwebtoken');
const { Logger } = require('../utils/logger');

class AuthMiddleware {
    constructor(options = {}) {
        this.secretKey = options.secretKey || process.env.JWT_SECRET;
        this.issuer = options.issuer || process.env.JWT_ISSUER;
        this.audience = options.audience || process.env.JWT_AUDIENCE;
    }
    
    // JWT トークン検証
    async verifyToken(token) {
        try {
            const decoded = jwt.verify(token, this.secretKey, {
                issuer: this.issuer,
                audience: this.audience,
                algorithms: ['HS256']
            });
            
            return {
                valid: true,
                user: decoded
            };
        } catch (error) {
            return {
                valid: false,
                error: error.message
            };
        }
    }
    
    // APIキー検証
    async verifyApiKey(apiKey) {
        // DynamoDBからAPIキー情報を取得
        const { queryDatabase } = require('./tracing');
        
        const result = await queryDatabase({
            TableName: process.env.API_KEYS_TABLE,
            Key: { keyId: apiKey }
        });
        
        if (!result.Item) {
            return { valid: false, error: 'Invalid API key' };
        }
        
        const keyData = result.Item;
        
        // キーの有効期限チェック
        if (keyData.expiresAt && new Date(keyData.expiresAt) < new Date()) {
            return { valid: false, error: 'API key expired' };
        }
        
        // レート制限チェック
        const rateLimit = await this.checkRateLimit(apiKey, keyData.rateLimit);
        if (!rateLimit.allowed) {
            return { valid: false, error: 'Rate limit exceeded' };
        }
        
        return {
            valid: true,
            key: keyData,
            rateLimit
        };
    }
    
    async checkRateLimit(apiKey, limit) {
        const { DynamoDB } = require('aws-sdk');
        const dynamodb = new DynamoDB.DocumentClient();
        
        const now = Math.floor(Date.now() / 1000);
        const windowStart = now - (limit.windowSeconds || 3600);
        
        try {
            // 現在のウィンドウでの使用量を取得
            const result = await dynamodb.get({
                TableName: process.env.RATE_LIMIT_TABLE,
                Key: {
                    keyId: apiKey,
                    window: Math.floor(now / (limit.windowSeconds || 3600))
                }
            }).promise();
            
            const currentUsage = result.Item ? result.Item.count : 0;
            
            if (currentUsage >= limit.requests) {
                return {
                    allowed: false,
                    remaining: 0,
                    resetTime: (Math.floor(now / (limit.windowSeconds || 3600)) + 1) * (limit.windowSeconds || 3600)
                };
            }
            
            // 使用量を更新
            await dynamodb.update({
                TableName: process.env.RATE_LIMIT_TABLE,
                Key: {
                    keyId: apiKey,
                    window: Math.floor(now / (limit.windowSeconds || 3600))
                },
                UpdateExpression: 'ADD #count :inc SET #ttl = :ttl',
                ExpressionAttributeNames: {
                    '#count': 'count',
                    '#ttl': 'ttl'
                },
                ExpressionAttributeValues: {
                    ':inc': 1,
                    ':ttl': now + (limit.windowSeconds || 3600) + 86400 // 24時間後に削除
                }
            }).promise();
            
            return {
                allowed: true,
                remaining: limit.requests - currentUsage - 1,
                resetTime: (Math.floor(now / (limit.windowSeconds || 3600)) + 1) * (limit.windowSeconds || 3600)
            };
            
        } catch (error) {
            console.error('Rate limit check failed:', error);
            // エラー時はリクエストを許可(フェイルオープン)
            return { allowed: true, remaining: limit.requests };
        }
    }
}

// 認証ミドルウェア
function createAuthMiddleware(options = {}) {
    const auth = new AuthMiddleware(options);
    
    return async (event, context) => {
        const logger = new Logger(context);
        
        try {
            const authHeader = event.headers.Authorization || event.headers.authorization;
            const apiKeyHeader = event.headers['X-API-Key'] || event.headers['x-api-key'];
            
            let authResult = null;
            
            if (authHeader && authHeader.startsWith('Bearer ')) {
                const token = authHeader.substring(7);
                authResult = await auth.verifyToken(token);
                
                if (!authResult.valid) {
                    logger.warn('JWT token validation failed', { 
                        error: authResult.error,
                        ip: event.requestContext?.identity?.sourceIp 
                    });
                    
                    return {
                        statusCode: 401,
                        body: JSON.stringify({ error: 'Invalid token' })
                    };
                }
                
                logger.info('JWT authentication successful', { 
                    userId: authResult.user.sub 
                });
                
            } else if (apiKeyHeader) {
                authResult = await auth.verifyApiKey(apiKeyHeader);
                
                if (!authResult.valid) {
                    logger.warn('API key validation failed', { 
                        error: authResult.error,
                        ip: event.requestContext?.identity?.sourceIp 
                    });
                    
                    return {
                        statusCode: 401,
                        headers: {
                            'X-RateLimit-Remaining': '0',
                            'X-RateLimit-Reset': Math.floor(Date.now() / 1000) + 3600
                        },
                        body: JSON.stringify({ error: authResult.error })
                    };
                }
                
                logger.info('API key authentication successful', { 
                    keyId: authResult.key.keyId 
                });
                
            } else {
                logger.warn('No authentication provided', { 
                    ip: event.requestContext?.identity?.sourceIp 
                });
                
                return {
                    statusCode: 401,
                    body: JSON.stringify({ error: 'Authentication required' })
                };
            }
            
            // 認証情報をイベントに追加
            event.auth = authResult;
            
            return null; // 認証成功
            
        } catch (error) {
            logger.error('Authentication error', error);
            return {
                statusCode: 500,
                body: JSON.stringify({ error: 'Authentication service unavailable' })
            };
        }
    };
}

module.exports = { AuthMiddleware, createAuthMiddleware };

入力値検証とサニタイゼーション

// src/middleware/validation.js
const Joi = require('joi');
const DOMPurify = require('isomorphic-dompurify');

class ValidationMiddleware {
    static createValidator(schema) {
        return (event, context) => {
            const logger = new Logger(context);
            
            try {
                let data = {};
                
                // リクエストボディの解析
                if (event.body) {
                    try {
                        data = JSON.parse(event.body);
                    } catch (error) {
                        logger.warn('Invalid JSON in request body', { error: error.message });
                        return {
                            statusCode: 400,
                            body: JSON.stringify({ error: 'Invalid JSON format' })
                        };
                    }
                }
                
                // クエリパラメータの追加
                if (event.queryStringParameters) {
                    data.query = event.queryStringParameters;
                }
                
                // パスパラメータの追加
                if (event.pathParameters) {
                    data.path = event.pathParameters;
                }
                
                // バリデーション実行
                const { error, value } = schema.validate(data, {
                    abortEarly: false,
                    stripUnknown: true,
                    convert: true
                });
                
                if (error) {
                    const validationErrors = error.details.map(detail => ({
                        field: detail.path.join('.'),
                        message: detail.message,
                        type: detail.type
                    }));
                    
                    logger.warn('Validation failed', { errors: validationErrors });
                    
                    return {
                        statusCode: 400,
                        body: JSON.stringify({
                            error: 'Validation failed',
                            details: validationErrors
                        })
                    };
                }
                
                // サニタイズ処理
                const sanitizedValue = this.sanitizeData(value);
                
                // 検証済みデータをイベントに追加
                event.validatedData = sanitizedValue;
                
                return null; // バリデーション成功
                
            } catch (error) {
                logger.error('Validation middleware error', error);
                return {
                    statusCode: 500,
                    body: JSON.stringify({ error: 'Validation service error' })
                };
            }
        };
    }
    
    static sanitizeData(data) {
        if (typeof data === 'string') {
            // HTMLタグの除去
            return DOMPurify.sanitize(data, { ALLOWED_TAGS: [] });
        }
        
        if (Array.isArray(data)) {
            return data.map(item => this.sanitizeData(item));
        }
        
        if (data && typeof data === 'object') {
            const sanitized = {};
            for (const [key, value] of Object.entries(data)) {
                sanitized[key] = this.sanitizeData(value);
            }
            return sanitized;
        }
        
        return data;
    }
}

// バリデーションスキーマの例
const createUserSchema = Joi.object({
    name: Joi.string()
        .min(1)
        .max(100)
        .pattern(/^[a-zA-Z0-9\s\-_]+$/)
        .required()
        .messages({
            'string.pattern.base': 'Name contains invalid characters'
        }),
    
    email: Joi.string()
        .email()
        .required(),
    
    age: Joi.number()
        .integer()
        .min(13)
        .max(120)
        .optional(),
    
    bio: Joi.string()
        .max(500)
        .optional()
        .allow(''),
    
    preferences: Joi.object({
        newsletter: Joi.boolean().default(false),
        notifications: Joi.boolean().default(true)
    }).optional(),
    
    query: Joi.object({
        source: Joi.string().valid('web', 'mobile', 'api').optional()
    }).optional()
});

const getUserSchema = Joi.object({
    path: Joi.object({
        id: Joi.string()
            .pattern(/^[a-zA-Z0-9\-_]+$/)
            .required()
    }).required(),
    
    query: Joi.object({
        include: Joi.array()
            .items(Joi.string().valid('preferences', 'activity', 'stats'))
            .optional()
    }).optional()
});

module.exports = {
    ValidationMiddleware,
    schemas: {
        createUserSchema,
        getUserSchema
    }
};

テスト戦略

単体テスト

// tests/unit/users.test.js
const { handler } = require('../../src/users/create');
const { Logger } = require('../../src/utils/logger');

// AWS SDKのモック
jest.mock('aws-sdk', () => ({
    DynamoDB: {
        DocumentClient: jest.fn(() => ({
            put: jest.fn().mockReturnValue({
                promise: jest.fn()
            })
        }))
    }
}));

// ロガーのモック
jest.mock('../../src/utils/logger');

describe('Create User Function', () => {
    let mockContext;
    let mockDynamoDb;
    
    beforeEach(() => {
        mockContext = {
            awsRequestId: 'test-request-id',
            functionName: 'test-function',
            remainingTimeInMillis: () => 30000
        };
        
        mockDynamoDb = require('aws-sdk').DynamoDB.DocumentClient();
        Logger.mockClear();
        
        process.env.USERS_TABLE = 'test-users-table';
    });
    
    afterEach(() => {
        jest.clearAllMocks();
        delete process.env.USERS_TABLE;
    });
    
    describe('Valid Input', () => {
        test('should create user successfully', async () => {
            const event = {
                body: JSON.stringify({
                    name: 'John Doe',
                    email: '[email protected]',
                    age: 30
                })
            };
            
            mockDynamoDb.put().promise.mockResolvedValue({});
            
            const result = await handler(event, mockContext);
            
            expect(result.statusCode).toBe(201);
            expect(JSON.parse(result.body)).toHaveProperty('id');
            expect(JSON.parse(result.body)).toHaveProperty('message', 'User created successfully');
            
            // DynamoDB呼び出しの検証
            expect(mockDynamoDb.put).toHaveBeenCalledWith({
                TableName: 'test-users-table',
                Item: expect.objectContaining({
                    name: 'John Doe',
                    email: '[email protected]',
                    age: 30,
                    id: expect.any(String),
                    createdAt: expect.any(String)
                })
            });
        });
    });
    
    describe('Invalid Input', () => {
        test('should return 400 for invalid JSON', async () => {
            const event = {
                body: 'invalid json'
            };
            
            const result = await handler(event, mockContext);
            
            expect(result.statusCode).toBe(400);
            expect(JSON.parse(result.body)).toHaveProperty('error');
        });
        
        test('should return 400 for missing required fields', async () => {
            const event = {
                body: JSON.stringify({
                    name: 'John Doe'
                    // email is missing
                })
            };
            
            const result = await handler(event, mockContext);
            
            expect(result.statusCode).toBe(400);
            expect(JSON.parse(result.body)).toHaveProperty('error');
        });
    });
    
    describe('Database Errors', () => {
        test('should handle DynamoDB errors gracefully', async () => {
            const event = {
                body: JSON.stringify({
                    name: 'John Doe',
                    email: '[email protected]'
                })
            };
            
            const dbError = new Error('Database connection failed');
            mockDynamoDb.put().promise.mockRejectedValue(dbError);
            
            const result = await handler(event, mockContext);
            
            expect(result.statusCode).toBe(500);
            expect(JSON.parse(result.body)).toHaveProperty('error', 'Internal server error');
        });
    });
});

統合テスト

// tests/integration/api.test.js
const AWS = require('aws-sdk');
const fetch = require('node-fetch');

// テスト環境設定
const API_ENDPOINT = process.env.API_ENDPOINT || 'https://api.example.com';
const STAGE = process.env.STAGE || 'test';

describe('API Integration Tests', () => {
    let createdUserId;
    let authToken;
    
    beforeAll(async () => {
        // テスト用認証トークンの取得
        authToken = await getTestAuthToken();
    });
    
    afterAll(async () => {
        // テストデータのクリーンアップ
        if (createdUserId) {
            await cleanupTestUser(createdUserId);
        }
    });
    
    describe('User Management', () => {
        test('should create a new user', async () => {
            const userData = {
                name: 'Integration Test User',
                email: `test-${Date.now()}@example.com`,
                age: 25
            };
            
            const response = await fetch(`${API_ENDPOINT}/users`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${authToken}`
                },
                body: JSON.stringify(userData)
            });
            
            expect(response.status).toBe(201);
            
            const result = await response.json();
            expect(result).toHaveProperty('id');
            expect(result).toHaveProperty('message', 'User created successfully');
            
            createdUserId = result.id;
        });
        
        test('should retrieve created user', async () => {
            expect(createdUserId).toBeDefined();
            
            const response = await fetch(`${API_ENDPOINT}/users/${createdUserId}`, {
                headers: {
                    'Authorization': `Bearer ${authToken}`
                }
            });
            
            expect(response.status).toBe(200);
            
            const user = await response.json();
            expect(user).toHaveProperty('id', createdUserId);
            expect(user).toHaveProperty('name', 'Integration Test User');
        });
        
        test('should handle non-existent user', async () => {
            const response = await fetch(`${API_ENDPOINT}/users/non-existent-id`, {
                headers: {
                    'Authorization': `Bearer ${authToken}`
                }
            });
            
            expect(response.status).toBe(404);
        });
    });
    
    describe('Authentication', () => {
        test('should reject request without authentication', async () => {
            const response = await fetch(`${API_ENDPOINT}/users/any-id`);
            
            expect(response.status).toBe(401);
        });
        
        test('should reject request with invalid token', async () => {
            const response = await fetch(`${API_ENDPOINT}/users/any-id`, {
                headers: {
                    'Authorization': 'Bearer invalid-token'
                }
            });
            
            expect(response.status).toBe(401);
        });
    });
    
    describe('Rate Limiting', () => {
        test('should enforce rate limits', async () => {
            const requests = [];
            
            // 大量のリクエストを並行実行
            for (let i = 0; i < 10; i++) {
                requests.push(
                    fetch(`${API_ENDPOINT}/users/${createdUserId}`, {
                        headers: {
                            'Authorization': `Bearer ${authToken}`
                        }
                    })
                );
            }
            
            const responses = await Promise.all(requests);
            
            // 一部のリクエストはレート制限に引っかかることを確認
            const rateLimitedResponses = responses.filter(r => r.status === 429);
            expect(rateLimitedResponses.length).toBeGreaterThan(0);
        }, 10000);
    });
});

async function getTestAuthToken() {
    // テスト用トークンの生成または取得
    const jwt = require('jsonwebtoken');
    
    return jwt.sign(
        {
            sub: 'test-user-id',
            email: '[email protected]',
            iat: Math.floor(Date.now() / 1000),
            exp: Math.floor(Date.now() / 1000) + 3600
        },
        process.env.JWT_SECRET || 'test-secret',
        {
            issuer: process.env.JWT_ISSUER || 'test-issuer',
            audience: process.env.JWT_AUDIENCE || 'test-audience'
        }
    );
}

async function cleanupTestUser(userId) {
    const dynamodb = new AWS.DynamoDB.DocumentClient({
        region: process.env.AWS_REGION || 'ap-northeast-1'
    });
    
    try {
        await dynamodb.delete({
            TableName: process.env.USERS_TABLE || `users-${STAGE}`,
            Key: { id: userId }
        }).promise();
    } catch (error) {
        console.warn('Failed to cleanup test user:', error);
    }
}

パフォーマンステスト

// tests/performance/load.test.js
const autocannon = require('autocannon');

describe('Performance Tests', () => {
    const API_ENDPOINT = process.env.API_ENDPOINT || 'https://api.example.com';
    
    test('should handle moderate load', async () => {
        const result = await autocannon({
            url: `${API_ENDPOINT}/health`,
            connections: 10,
            pipelining: 1,
            duration: 10, // 10秒間
            headers: {
                'Content-Type': 'application/json'
            }
        });
        
        console.log('Load test results:', {
            requests: result.requests,
            latency: result.latency,
            throughput: result.throughput,
            errors: result.errors
        });
        
        // パフォーマンス要件の検証
        expect(result.latency.mean).toBeLessThan(1000); // 平均レスポンス時間 < 1秒
        expect(result.latency.p99).toBeLessThan(3000);  // 99%ile < 3秒
        expect(result.errors).toBe(0); // エラー率 0%
        expect(result.requests.average).toBeGreaterThan(50); // 最低50 RPS
    }, 30000);
    
    test('should maintain performance under authenticated load', async () => {
        const authToken = await getTestAuthToken();
        
        const result = await autocannon({
            url: `${API_ENDPOINT}/users/test-user-id`,
            connections: 5,
            pipelining: 1,
            duration: 10,
            headers: {
                'Authorization': `Bearer ${authToken}`,
                'Content-Type': 'application/json'
            }
        });
        
        // 認証ありでもパフォーマンスを維持
        expect(result.latency.mean).toBeLessThan(1500);
        expect(result.latency.p99).toBeLessThan(5000);
        expect(result.errors).toBe(0);
    }, 30000);
});

まとめ

サーバーレスアーキテクチャは、2024年において現代的なアプリケーション開発の中核技術となっています。本記事で解説した実装パターン、コスト最適化戦略、監視手法を適切に組み合わせることで、スケーラブルで効率的なシステムを構築できます。

重要なポイント

  1. 適切なプラットフォーム選択: ワークロードの特性に応じたプラットフォーム選択が重要
  2. コスト最適化: 実行時間、メモリ使用量、リクエスト数の継続的な監視と最適化
  3. 監視・可観測性: 分散トレーシング、構造化ログ、メトリクス収集の実装
  4. セキュリティ: 認証・認可、入力値検証、レート制限の適切な実装
  5. テスト戦略: 単体テスト、統合テスト、パフォーマンステストの包括的な実施

最新技術の動向を把握し、継続的な改善を行うことで、サーバーレスアーキテクチャの利点を最大限に活用できるでしょう。