サーバーレスアーキテクチャパターンと実装戦略
サーバーレスアーキテクチャパターンと実装戦略
概要
サーバーレスアーキテクチャは、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年において現代的なアプリケーション開発の中核技術となっています。本記事で解説した実装パターン、コスト最適化戦略、監視手法を適切に組み合わせることで、スケーラブルで効率的なシステムを構築できます。
重要なポイント
- 適切なプラットフォーム選択: ワークロードの特性に応じたプラットフォーム選択が重要
- コスト最適化: 実行時間、メモリ使用量、リクエスト数の継続的な監視と最適化
- 監視・可観測性: 分散トレーシング、構造化ログ、メトリクス収集の実装
- セキュリティ: 認証・認可、入力値検証、レート制限の適切な実装
- テスト戦略: 単体テスト、統合テスト、パフォーマンステストの包括的な実施
最新技術の動向を把握し、継続的な改善を行うことで、サーバーレスアーキテクチャの利点を最大限に活用できるでしょう。