serverless-development について
API、イベント処理、スケジュール実行を構築する開発者向けの、サーバーレスアプリケーション設計・運用スキルです。各プラットフォームのパターンに加え、レイテンシー、データベース接続、デプロイ、監視、コストを扱います。
主な機能
- 01AWS Lambda と SAM のパターン
- 02Vercel と Cloudflare の関数パターン
- 03コールドスタートの低減
- 04データベース接続プーリング
- 05デプロイとコストの指針
活用シーン
- 01サーバーレス API と Webhook の構築
- 02バックグラウンド処理と定期タスク
- 03サーバーレスエンドポイントの遅延低減
- 04高い同時実行時のデータベースアクセス管理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ travisjneuman/ .claude --skill serverless-developmentインストール後、Claude Code で/serverless-developmentを入力して読み込まれたか確認します。
詳細
サーバーレス開発
概要
このスキルでは、インフラストラクチャ管理が抽象化されたサーバーレスコンピューティングプラットフォームでアプリケーションを構築する方法を扱います。AWS Lambda(Node.js、Python、Go のランタイム)、Vercel Edge Functions と Serverless Functions、Cloudflare Workers、コールドスタートの最適化、サーバーレス設計パターン(ファンアウト、ステップ関数、イベント駆動)、データベース接続管理、SAM/SST/Serverless Framework を使ったデプロイ、モニタリング、コスト最適化を取り上げます。
API、Webhook、バックグラウンド処理、スケジュールタスク、または自動スケーリング、従量課金、インフラストラクチャ管理の不要さがメリットとなるあらゆるワークロードを構築する際に、このスキルを使用してください。
基本原則
- ステートレス設計にする - 関数の呼び出しはそれぞれ独立しています。呼び出し間でグローバル変数に状態を保存しないでください(ただし、接続は再利用できます)。永続化には外部の状態ストア(データベース、キャッシュ、キュー)を使用してください。
- コールドスタートを最小限に抑える - コールドスタートにより、初回呼び出し時に100ms~10sの遅延が発生します。バンドルを小さくし、軽量なランタイムを使用してください。また、レイテンシーが重要な経路ではプロビジョンドコンカレンシーを検討してください。
- 接続管理は重要です - サーバーレス関数は数千の同時実行インスタンスを起動する場合があります。それぞれが独自のデータベース接続を開くと、接続プールが枯渇します。接続プーリング(RDS Proxy、Prisma Data Proxy、PgBouncer)を使用してください。
- 呼び出しごとの課金を意識する - 実行時間とメモリに対して課金されます。頻繁に実行される処理を最適化し、適切なメモリサイズを選択してください(メモリを増やすとCPUも増え、処理が速くなり、場合によってはコストも下がります)。可能であれば処理をバッチ化してください。
- 制約を活用する - 関数のタイムアウト、ペイロードの上限、同時実行数の上限はバグではなく、仕様です。それらを考慮して設計してください。長時間のタスクにはキュー、大きなペイロードにはS3、複雑なオーケストレーションにはステップ関数を使用してください。
主なパターン
パターン1:TypeScript を使った AWS Lambda
使用場面: AWS 上のバックエンド API、イベント処理、スケジュールタスク。
実装:
// handler.ts - Lambda handler with proper typing
import {
APIGatewayProxyEventV2,
APIGatewayProxyResultV2,
SQSEvent,
ScheduledEvent,
Context,
} from "aws-lambda";
// Reusable across invocations (connection reuse)
let dbConnection: PrismaClient | null = null;
function getDb(): PrismaClient {
if (!dbConnection) {
dbConnection = new PrismaClient({
datasources: {
db: { url: process.env.DATABASE_URL },
},
});
}
return dbConnection;
}
// API Gateway handler
export async function apiHandler(
event: APIGatewayProxyEventV2,
context: Context
): Promise<APIGatewayProxyResultV2> {
// Don't wait for event loop to drain (for connection reuse)
context.callbackWaitsForEmptyEventLoop = false;
try {
const method = event.requestContext.http.method;
const path = event.rawPath;
if (method === "GET" && path === "/api/users") {
const db = getDb();
const users = await db.user.findMany({ take: 50 });
return response(200, { users });
}
if (method === "POST" && path === "/api/users") {
const body = JSON.parse(event.body ?? "{}");
const db = getDb();
const user = await db.user.create({ data: body });
return response(201, { user });
}
return response(404, { error: "Not found" });
} catch (error) {
console.error("Handler error:", error);
return response(500, { error: "Internal server error" });
}
}
// SQS event handler (batch processing)
export async function sqsHandler(event: SQSEvent): Promise<{ batchItemFailures: Array<{ itemIdentifier: string }> }> {
const failures: 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);
failures.push(record.messageId);
}
}
// Partial batch failure reporting (only retry failed messages)
return {
batchItemFailures: failures.map((id) => ({ itemIdentifier: id })),
};
}
// Scheduled event handler (cron)
export async function cronHandler(event: ScheduledEvent): Promise<void> {
console.log("Running scheduled task at:", event.time);
const db = getDb();
// Clean up expired sessions
const deleted = await db.session.deleteMany({
where: { expiresAt: { lt: new Date() } },
});
console.log(`Cleaned up ${deleted.count} expired sessions`);
}
function response(statusCode: number, body: Record<string, unknown>): APIGatewayProxyResultV2 {
return {
statusCode,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": process.env.ALLOWED_ORIGIN ?? "*",
},
body: JSON.stringify(body),
};
}
# template.yaml - AWS SAM template
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Runtime: nodejs20.x
Timeout: 30
MemorySize: 256
Environment:
Variables:
DATABASE_URL: !Ref DatabaseUrl
NODE_OPTIONS: "--enable-source-maps"
Resources:
ApiFunction:
Type: AWS::Serverless::Function
Properties:
Handler: dist/handler.apiHandler
Events:
Api:
Type: HttpApi
Properties:
Path: /api/{proxy+}
Method: ANY
Metadata:
BuildMethod: esbuild
BuildProperties:
Minify: true
Target: es2022
Sourcemap: true
EntryPoints:
- src/handler.ts
QueueProcessor:
Type: AWS::Serverless::Function
Properties:
Handler: dist/handler.sqsHandler
Events:
Queue:
Type: SQS
Properties:
Queue: !GetAtt ProcessingQueue.Arn
BatchSize: 10
FunctionResponseTypes:
- ReportBatchItemFailures
ScheduledCleanup:
Type: AWS::Serverless::Function
Properties:
Handler: dist/handler.cronHandler
Events:
Schedule:
Type: Schedule
Properties:
Schedule: rate(1 hour)
ProcessingQueue:
Type: AWS::SQS::Queue
Properties:
VisibilityTimeout: 180
RedrivePolicy:
deadLetterTargetArn: !GetAtt DLQ.Arn
maxReceiveCount: 3
DLQ:
Type: AWS::SQS::Queue
理由: AWS Lambda と SAM を組み合わせると、Infrastructure as Code、ローカルテスト(sam local invoke)、自動デプロイを利用できます。モジュールレベルの変数を使って接続を再利用すれば、呼び出しごとに再接続するコールドスタートのペナルティを回避できます。SQS の部分バッチ失敗レポートにより、失敗したメッセージのみが再試行されます。
パターン2:Vercel Edge Functions
使用場面: 世界中のユーザーの近くで実行する必要がある、低レイテンシーの API ルートやミドルウェア。
実装:
// app/api/geo/route.ts - Edge Function
export const runtime = "edge"; // Run on Vercel Edge Network
export async function GET(request: Request) {
// Access geo information (available at the edge)
const country = request.headers.get("x-vercel-ip-country") ?? "US";
const city = request.headers.get("x-vercel-ip-city") ?? "Unknown";
// Edge-compatible KV store
const cachedData = await kv.get(`content:${country}`);
if (cachedData) {
return Response.json(cachedData);
}
// Lightweight processing at the edge
const content = await getLocalizedContent(country);
await kv.set(`content:${country}`, content, { ex: 3600 });
return Response.json(content, {
headers: {
"Cache-Control": "public, s-maxage=3600, stale-while-revalidate=86400",
},
});
}
// middleware.ts - Edge Middleware for auth, redirects, A/B testing
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export const config = {
matcher: ["/dashboard/:path*", "/api/:path*"],
};
export function middleware(request: NextRequest) {
// Auth check at the edge (fast, runs before serverless function)
const token = request.cookies.get("session_token");
if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
// A/B test assignment at the edge
const abCookie = request.cookies.get("ab-pricing");
if (!abCookie && request.nextUrl.pathname === "/pricing") {
const variant = Math.random() < 0.5 ? "control" : "new-layout";
const response = NextResponse.next();
response.cookies.set("ab-pricing", variant, { maxAge: 60 * 60 * 24 * 30 });
// Rewrite to variant-specific page
if (variant === "new-layout") {
return NextResponse.rewrite(new URL("/pricing-v2", request.url));
}
return response;
}
return NextResponse.next();
}
理由: Edge Functions は、グローバルに30以上の拠点で、サブミリ秒のコールドスタートで実行されます(コンテナではなくV8アイソレートを使用)。認証チェック、地域に応じたルーティング、A/Bテスト、ユーザーに近い場所からの応答がメリットとなる API に最適です。一方で、ランタイムには制限があります(Node.js API とネイティブモジュールは使用できず、メモリも少なめです)。
パターン3:コールドスタートの最適化
使用場面: レイテンシーが重要なエンドポイントで、コールドスタート時間が許容できない場合。
実装:
// 1. Minimize bundle size with esbuild tree-shaking
// esbuild.config.mjs
import { build } from "esbuild";
await build({
entryPoints: ["src/handler.ts"],
bundle: true,
minify: true,
platform: "node",
target: "node20",
outdir: "dist",
external: [
// Don't bundle the AWS SDK (already available in Lambda)
"@aws-sdk/*",
],
// Tree-shake unused code
treeShaking: true,
// Source maps for debugging
sourcemap: true,
});
// 2. Lazy-load heavy dependencies
let _sharp: typeof import("sharp") | null = null;
async function getSharp() {
if (!_sharp) {
_sharp = await import("sharp");
}
return _sharp;
}
// Only load sharp when actually processing images
export async function imageHandler(event: APIGatewayProxyEventV2) {
if (event.rawPath.includes("/resize")) {
const sharp = await getSharp();
// Use sharp...
}
}
// 3. Provisioned concurrency for critical paths
// In SAM template:
// Properties:
// ProvisionedConcurrencyConfig:
// ProvisionedConcurrentExecutions: 5
// 4. Keep-alive with scheduled warming (cheaper than provisioned concurrency)
// CloudWatch rule: rate(5 minutes) -> Lambda with warming event
export async function handler(event: unknown) {
// Check if this is a warming invocation
if ((event as Record<string, unknown>).source === "serverless-warming") {
console.log("Warming invocation, returning early");
return { statusCode: 200, body: "warm" };
}
// Normal handler logic...
}
理由: サーバーレスにおける主なレイテンシーの懸念はコールドスタートです。バンドルが小さいほど初期化が速くなります。遅延読み込みにより、重いインポートのコストを実際に必要になるまで先送りできます。プロビジョンドコンカレンシーを使うと、重要な経路のコールドスタートを完全に排除できます(ただしコストがかかります)。ウォームアップ呼び出しを行うと、プロビジョンドコンカレンシーの料金をかけずに関数インスタンスをウォーム状態に保てます。
パターン4:データベース接続プーリング
使用場面: リレーショナルデータベースに接続するサーバーレス関数全般。
実装:
// prisma/schema.prisma - Use Prisma Accelerate or Data Proxy for serverless
datasource db {
provider = "postgresql"
// Direct connection for migrations
url = env("DATABASE_URL")
// Pooled connection for serverless runtime
directUrl = env("DIRECT_DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
// For AWS Lambda: Use RDS Proxy
// DATABASE_URL = "postgresql://user:[email protected]:5432/mydb"
// For Vercel/Cloudflare: Use Neon serverless driver or Prisma Accelerate
import { Pool, neonConfig } from "@neondatabase/serverless";
import { PrismaNeon } from "@prisma/adapter-neon";
import { PrismaClient } from "@prisma/client";
import ws from "ws";
// Required for Node.js environments
neonConfig.webSocketConstructor = ws;
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const adapter = new PrismaNeon(pool);
const prisma = new PrismaClient({ adapter });
// Connection reuse pattern
let client: PrismaClient | null = null;
export function getClient(): PrismaClient {
if (!client) {
client = new PrismaClient({
log: process.env.NODE_ENV === "development" ? ["query"] : [],
});
}
return client;
}
理由: Lambda の呼び出しごとに、新しいデータベース接続が作成される場合があります。数千の呼び出しが同時実行されると、データベースの接続上限(マネージドデータベースでは通常100~500)に達します。接続プーリング(RDS Proxy、PgBouncer、Neon serverless、Prisma Accelerate)を使うと、多数のサーバーレス接続を、より少数のデータベース接続経由で多重化できます。
コスト最適化のリファレンス
| 戦略 | 削減効果 | トレードオフ |
|---|---|---|
| メモリを適切なサイズにする | 20-40% | ベンチマークが必要 |
| ARM64(Graviton) | 20% | 軽微な互換性の懸念 |
| プロビジョニング済み同時実行数(過剰にプロビジョニングされたサーバーとの比較) | 常時稼働と比べて50-70% | スケールアップ時のレイテンシが増加 |
| 予約済み同時実行数 | コストの予期せぬ増大を防止 | スループットを制限 |
| バッチ処理(SQS のバッチサイズ) | 50-80% | レイテンシが増加 |
| 階層型ストレージ(S3 ライフサイクル) | 30-60% | 低頻度アクセス層でアクセスレイテンシが発生 |
アンチパターン
| アンチパターン | 問題点 | より良い方法 |
|---|---|---|
| 呼び出しごとに DB 接続を開く | 接続プールを使い果たす | モジュールレベルで接続を再利用し、RDS Proxy を使用する |
node_modules 全体をバンドルする | コールドスタートが大幅に遅くなる(5-10s) | esbuild でツリーシェイキングし、AWS SDK を外部化する |
| グローバル変数に状態を保存する | 呼び出し間で失われる(信頼性が低い) | 状態の保存に DynamoDB、Redis、または S3 を使用する |
| API ハンドラーに15分のタイムアウトを設定する | ユーザーを15分も待たせるのでしょうか? | API は30sに設定し、長時間のタスクには Step Functions を使用する |
| 同時実行数に上限を設けない | コストが予期せず増大し、下流サービスに過負荷がかかる | 関数ごとに予約済み同時実行数を設定する |
| 同期的なファンアウト | 遅く、タイムアウトのリスクがある | ファンアウトには SQS/SNS を使用し、非同期で処理する |
構造化せずに console.log を使う | CloudWatch で検索できない | 相関 ID を含む構造化 JSON ロギングを使用する |
チェックリスト
- バンドルサイズを最小化している(esbuild のツリーシェイキング、AWS SDK の外部化)
- データベース接続をプールしている(RDS Proxy、Neon、Prisma Accelerate)
- コールドスタートを測定し、ユースケースに対して許容できることを確認している
- レイテンシに敏感な処理経路にプロビジョニング済み同時実行数を設定している
- メモリサイズをベンチマークし、適切なサイズにしている
- SQS の部分的なバッチ失敗の報告を有効にしている
- 非同期イベントソースにデッドレターキューを設定している
- コストの予期せぬ増大を防ぐため、予約済み同時実行数を設定している
- リクエスト ID/相関 ID を含む構造化ロギングを行っている
- タイムアウトを適切に設定している(API は30s、バッチ処理はそれより長く)
- すべての設定に環境変数を使用している(値をハードコードしていない)
- コスト削減のため ARM64/Graviton ランタイムを選択している
関連リソース
- スキル:
event-driven-architecture(SQS/SNS のパターン)、monitoring-observability(Lambda のトレース) - スキル:
performance-engineering(レイテンシに対するコールドスタートの影響) - ルール:
docs/reference/stacks/python.md(Python Lambda のパターン)