サーバーレスアーキテクチャは、インフラ管理の負担を軽減しながら、スケーラブルなアプリケーションを構築できるモダンなアプローチです。本記事では、サーバーレスの設計パターンと実装のベストプラクティスを解説します。
サーバーレスの基本概念
アーキテクチャの比較
flowchart TB
subgraph Traditional["従来のサーバーアーキテクチャ"]
LB1["Load Balancer"]
LB1 --> S1["Server (EC2)<br/>24/7稼働"]
LB1 --> S2["Server (EC2)<br/>24/7稼働"]
LB1 --> S3["Server (EC2)<br/>24/7稼働"]
S1 & S2 & S3 --> DB1["DB"]
end
subgraph Serverless["サーバーレスアーキテクチャ"]
API["API Gateway"]
API --> L1["Lambda Function<br/>(オンデマンド)"]
API --> L2["Lambda Function<br/>(オンデマンド)"]
API --> L3["Lambda Function<br/>(オンデマンド)"]
L1 & L2 & L3 --> DB2["DynamoDB / Aurora<br/>(サーバーレス)"]
end
FaaS/BaaSの分類
| カテゴリ | サービス例 | 特徴 |
|---|---|---|
| FaaS | Lambda, Cloud Functions | イベント駆動の関数実行 |
| Edge Functions | Cloudflare Workers, Vercel Edge | CDNエッジでの実行 |
| Container-based | AWS Fargate, Cloud Run | コンテナベースの実行 |
| BaaS | Firebase, Supabase | バックエンド機能の提供 |
プラットフォーム比較
主要サービスの特性
| 項目 | Lambda | Workers | Vercel | Cloud Run |
|---|---|---|---|---|
| 実行環境 | Node/Py等 | V8 | Node/Edge | Container |
| コールドスタート | 100ms-1s | <5ms | <50ms | 1-5s |
| メモリ上限 | 10GB | 128MB | 1GB | 32GB |
| 実行時間上限 | 15分 | 30秒/無制限 | 10秒/300秒 | 60分 |
| ロケーション | リージョン | グローバル | グローバル | リージョン |
| プライシング | 実行時間 | リクエスト | 実行時間 | vCPU秒 |
AWS Lambda
基本的なLambda関数
// handler.ts - AWS Lambda Handler
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
// 基本的なハンドラー
export const hello = async (
event: APIGatewayProxyEvent,
context: Context
): Promise<APIGatewayProxyResult> => {
console.log('Event:', JSON.stringify(event, null, 2));
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
message: 'Hello from Lambda!',
requestId: context.awsRequestId,
}),
};
};
// RESTful APIハンドラー
export const users = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
const method = event.httpMethod;
const userId = event.pathParameters?.id;
try {
switch (method) {
case 'GET':
if (userId) {
return await getUser(userId);
}
return await listUsers(event.queryStringParameters);
case 'POST':
const body = JSON.parse(event.body || '{}');
return await createUser(body);
case 'PUT':
if (!userId) return badRequest('User ID required');
return await updateUser(userId, JSON.parse(event.body || '{}'));
case 'DELETE':
if (!userId) return badRequest('User ID required');
return await deleteUser(userId);
default:
return {
statusCode: 405,
body: JSON.stringify({ error: 'Method not allowed' }),
};
}
} catch (error) {
console.error('Error:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal server error' }),
};
}
};
// ヘルパー関数
function badRequest(message: string): APIGatewayProxyResult {
return {
statusCode: 400,
body: JSON.stringify({ error: message }),
};
}
イベントソース統合
// event-handlers.ts
// SQSイベントハンドラー
import { SQSEvent, SQSBatchResponse } from 'aws-lambda';
export const sqsHandler = async (event: SQSEvent): Promise<SQSBatchResponse> => {
const batchItemFailures: { itemIdentifier: string }[] = [];
for (const record of event.Records) {
try {
const body = JSON.parse(record.body);
await processMessage(body);
} catch (error) {
console.error(`Failed to process message: ${record.messageId}`, error);
batchItemFailures.push({ itemIdentifier: record.messageId });
}
}
return { batchItemFailures };
};
// DynamoDB Streamsハンドラー
import { DynamoDBStreamEvent } from 'aws-lambda';
export const dynamoHandler = async (event: DynamoDBStreamEvent): Promise<void> => {
for (const record of event.Records) {
console.log('Event Type:', record.eventName);
console.log('DynamoDB Record:', JSON.stringify(record.dynamodb, null, 2));
switch (record.eventName) {
case 'INSERT':
await handleInsert(record.dynamodb?.NewImage);
break;
case 'MODIFY':
await handleModify(record.dynamodb?.OldImage, record.dynamodb?.NewImage);
break;
case 'REMOVE':
await handleRemove(record.dynamodb?.OldImage);
break;
}
}
};
// S3イベントハンドラー
import { S3Event } from 'aws-lambda';
export const s3Handler = async (event: S3Event): Promise<void> => {
for (const record of event.Records) {
const bucket = record.s3.bucket.name;
const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' '));
console.log(`Processing: ${bucket}/${key}`);
// ファイル処理
await processS3Object(bucket, key);
}
};
// スケジュール実行(EventBridge)
import { ScheduledEvent } from 'aws-lambda';
export const scheduledHandler = async (event: ScheduledEvent): Promise<void> => {
console.log('Scheduled event:', event);
// 定期実行タスク
await runDailyReport();
await cleanupExpiredData();
};
SAM/CDKによるデプロイ
# template.yaml (AWS SAM)
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Timeout: 30
Runtime: nodejs20.x
MemorySize: 256
Environment:
Variables:
TABLE_NAME: !Ref UsersTable
NODE_OPTIONS: --enable-source-maps
Resources:
ApiGateway:
Type: AWS::Serverless::Api
Properties:
StageName: prod
Cors:
AllowOrigin: "'*'"
AllowMethods: "'GET,POST,PUT,DELETE,OPTIONS'"
AllowHeaders: "'Content-Type,Authorization'"
UsersFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: dist/
Handler: handler.users
Events:
GetUsers:
Type: Api
Properties:
RestApiId: !Ref ApiGateway
Path: /users
Method: GET
CreateUser:
Type: Api
Properties:
RestApiId: !Ref ApiGateway
Path: /users
Method: POST
GetUser:
Type: Api
Properties:
RestApiId: !Ref ApiGateway
Path: /users/{id}
Method: GET
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref UsersTable
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: users
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
Outputs:
ApiEndpoint:
Value: !Sub "https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/prod"
Cloudflare Workers
エッジでの実行
// worker.ts - Cloudflare Workers
export interface Env {
KV: KVNamespace;
DB: D1Database;
BUCKET: R2Bucket;
API_KEY: string;
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
// ルーティング
if (url.pathname === '/api/users') {
return handleUsers(request, env);
}
if (url.pathname.startsWith('/api/cache')) {
return handleCache(request, env);
}
if (url.pathname.startsWith('/api/storage')) {
return handleStorage(request, env);
}
return new Response('Not Found', { status: 404 });
},
// Scheduled Worker(Cron Triggers)
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
console.log(`Cron triggered at ${event.scheduledTime}`);
await cleanupOldData(env);
},
};
// KV Storageの使用
async function handleCache(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const key = url.searchParams.get('key');
if (!key) {
return new Response('Key required', { status: 400 });
}
if (request.method === 'GET') {
const value = await env.KV.get(key);
if (!value) {
return new Response('Not found', { status: 404 });
}
return new Response(value);
}
if (request.method === 'PUT') {
const value = await request.text();
await env.KV.put(key, value, {
expirationTtl: 3600, // 1時間
});
return new Response('OK');
}
return new Response('Method not allowed', { status: 405 });
}
// D1 Database(SQLite)
async function handleUsers(request: Request, env: Env): Promise<Response> {
if (request.method === 'GET') {
const { results } = await env.DB.prepare(
'SELECT * FROM users ORDER BY created_at DESC LIMIT 100'
).all();
return Response.json(results);
}
if (request.method === 'POST') {
const body = await request.json<{ name: string; email: string }>();
const result = await env.DB.prepare(
'INSERT INTO users (name, email) VALUES (?, ?) RETURNING *'
)
.bind(body.name, body.email)
.first();
return Response.json(result, { status: 201 });
}
return new Response('Method not allowed', { status: 405 });
}
// R2 Storage
async function handleStorage(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const key = url.pathname.replace('/api/storage/', '');
if (request.method === 'GET') {
const object = await env.BUCKET.get(key);
if (!object) {
return new Response('Not found', { status: 404 });
}
return new Response(object.body, {
headers: {
'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',
'Cache-Control': 'public, max-age=31536000',
},
});
}
if (request.method === 'PUT') {
await env.BUCKET.put(key, request.body, {
httpMetadata: {
contentType: request.headers.get('Content-Type') || 'application/octet-stream',
},
});
return new Response('OK');
}
return new Response('Method not allowed', { status: 405 });
}
wrangler.toml設定
# wrangler.toml
name = "my-worker"
main = "src/worker.ts"
compatibility_date = "2024-01-01"
[triggers]
crons = ["0 * * * *"] # 毎時実行
[[kv_namespaces]]
binding = "KV"
id = "abc123"
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "def456"
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "my-bucket"
[vars]
ENVIRONMENT = "production"
[env.staging]
name = "my-worker-staging"
vars = { ENVIRONMENT = "staging" }
Vercel Functions
Edge Functions
// app/api/hello/route.ts (Next.js App Router)
import { NextRequest, NextResponse } from 'next/server';
export const runtime = 'edge'; // Edge Functionとして実行
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const name = searchParams.get('name') || 'World';
return NextResponse.json({
message: `Hello, ${name}!`,
region: process.env.VERCEL_REGION,
timestamp: new Date().toISOString(),
});
}
export async function POST(request: NextRequest) {
const body = await request.json();
// Edge Functionでの処理
const result = processData(body);
return NextResponse.json(result);
}
Serverless Functions
// api/users/[id].ts (Pages Router)
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { id } = req.query;
switch (req.method) {
case 'GET':
const user = await getUser(id as string);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
return res.json(user);
case 'PUT':
const updated = await updateUser(id as string, req.body);
return res.json(updated);
case 'DELETE':
await deleteUser(id as string);
return res.status(204).end();
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
return res.status(405).end();
}
}
// Vercel Blob Storage
import { put, del } from '@vercel/blob';
export async function uploadFile(file: File) {
const blob = await put(file.name, file, {
access: 'public',
});
return blob.url;
}
設計パターン
イベント駆動アーキテクチャ
flowchart TB
EventSource["Event Source<br/>(API Gateway / S3 / DynamoDB Streams)"]
EventBus["Event Bus<br/>(EventBridge)"]
EventSource --> EventBus
EventBus --> OrderLambda["Lambda<br/>Order Process"]
EventBus --> EmailLambda["Lambda<br/>Email Notify"]
EventBus --> AnalyticsLambda["Lambda<br/>Analytics Process"]
OrderLambda --> DynamoDB["DynamoDB"]
EmailLambda --> SES["SES"]
AnalyticsLambda --> S3["S3"]
ファンアウトパターン
// SNS → SQS → Lambda (ファンアウト)
// Publisher Lambda
import { SNSClient, PublishCommand } from '@aws-sdk/client-sns';
const sns = new SNSClient({});
export async function publishOrderEvent(order: Order) {
await sns.send(new PublishCommand({
TopicArn: process.env.ORDER_TOPIC_ARN,
Message: JSON.stringify({
eventType: 'ORDER_CREATED',
order,
timestamp: new Date().toISOString(),
}),
MessageAttributes: {
eventType: {
DataType: 'String',
StringValue: 'ORDER_CREATED',
},
},
}));
}
// Consumer Lambda (SQSから受信)
import { SQSEvent } from 'aws-lambda';
export async function processOrderNotification(event: SQSEvent) {
for (const record of event.Records) {
const snsMessage = JSON.parse(record.body);
const orderEvent = JSON.parse(snsMessage.Message);
// メール送信
await sendOrderConfirmationEmail(orderEvent.order);
}
}
export async function processOrderAnalytics(event: SQSEvent) {
for (const record of event.Records) {
const snsMessage = JSON.parse(record.body);
const orderEvent = JSON.parse(snsMessage.Message);
// 分析データの保存
await saveAnalyticsData(orderEvent);
}
}
サーガパターン
// Step Functions Saga Pattern
// State Machine定義 (ASL)
const orderSagaDefinition = {
Comment: 'Order Processing Saga',
StartAt: 'ReserveInventory',
States: {
ReserveInventory: {
Type: 'Task',
Resource: '${ReserveInventoryFunctionArn}',
Next: 'ProcessPayment',
Catch: [{
ErrorEquals: ['States.ALL'],
Next: 'InventoryReservationFailed',
}],
},
ProcessPayment: {
Type: 'Task',
Resource: '${ProcessPaymentFunctionArn}',
Next: 'ShipOrder',
Catch: [{
ErrorEquals: ['States.ALL'],
Next: 'PaymentFailed',
}],
},
ShipOrder: {
Type: 'Task',
Resource: '${ShipOrderFunctionArn}',
End: true,
Catch: [{
ErrorEquals: ['States.ALL'],
Next: 'ShippingFailed',
}],
},
// 補償トランザクション
InventoryReservationFailed: {
Type: 'Fail',
Cause: 'Failed to reserve inventory',
},
PaymentFailed: {
Type: 'Task',
Resource: '${ReleaseInventoryFunctionArn}',
Next: 'PaymentFailedState',
},
PaymentFailedState: {
Type: 'Fail',
Cause: 'Payment processing failed',
},
ShippingFailed: {
Type: 'Task',
Resource: '${RefundPaymentFunctionArn}',
Next: 'ReleaseInventoryAfterShipFail',
},
ReleaseInventoryAfterShipFail: {
Type: 'Task',
Resource: '${ReleaseInventoryFunctionArn}',
Next: 'ShippingFailedState',
},
ShippingFailedState: {
Type: 'Fail',
Cause: 'Shipping failed',
},
},
};
コールドスタート対策
Provisioned Concurrency
# template.yaml
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
Handler: handler.main
ProvisionedConcurrencyConfig:
ProvisionedConcurrentExecutions: 5
AutoPublishAlias: live
コネクションプールの最適化
// db.ts - Lambda外で初期化
import { Pool } from 'pg';
// グローバルスコープでプール作成(再利用される)
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 1, // Lambdaでは1接続が推奨
idleTimeoutMillis: 120000,
connectionTimeoutMillis: 10000,
});
export async function query<T>(sql: string, params?: any[]): Promise<T[]> {
const client = await pool.connect();
try {
const result = await client.query(sql, params);
return result.rows;
} finally {
client.release();
}
}
// handler.ts
import { query } from './db';
export const handler = async (event: any) => {
// コネクションプールは再利用される
const users = await query<User>('SELECT * FROM users');
return {
statusCode: 200,
body: JSON.stringify(users),
};
};
まとめ
サーバーレスアーキテクチャは、適切に設計することで高いスケーラビリティとコスト効率を実現できます。
選択ガイドライン
| ユースケース | 推奨サービス |
|---|---|
| APIバックエンド | Lambda + API Gateway |
| グローバル低レイテンシ | Cloudflare Workers |
| Webアプリ | Vercel / Next.js |
| 長時間処理 | Fargate / Cloud Run |
| イベント処理 | Lambda + EventBridge |
ベストプラクティス
- 関数の責務分離: 単一責任原則
- コールドスタート対策: 軽量化、Provisioned Concurrency
- 非同期処理: キュー経由の疎結合
- 監視: CloudWatch、Datadog等の活用
- コスト管理: 実行時間とメモリの最適化
サーバーレスは万能ではありませんが、適切なユースケースで大きな効果を発揮します。
参考リンク
- AWS Lambda Documentation
- Cloudflare Workers Documentation
- Vercel Serverless Functions
- Serverless Framework