サーバーレスアーキテクチャは、Lambda関数・API Gateway・DynamoDB・EventBridgeなど多数のAWSリソースを組み合わせて構築します。この「構成の複雑さ」こそ、Claude Codeが力を発揮する領域です。インフラ定義・Lambda関数実装・DynamoDBテーブル設計・CI/CDパイプラインをすべてTypeScriptで記述し、Claude Codeに一気通貫で生成させられます。
この記事では、SST v3(TypeScriptネイティブのサーバーレスIaCフレームワーク)とClaude Codeを組み合わせた実践的なサーバーレス開発ワークフローを解説します。CLAUDE.mdへの設計ルール記述から、Lambda関数の自動生成、DynamoDB single-table design、Live Lambda Devによる高速イテレーション、GitHub Actionsデプロイまでカバーします。
SST v3とは ── なぜClaude Codeと相性が良いか
SST(Serverless Stack)v3はPulumiベースのTypeScriptネイティブIaCフレームワークで、AWSサーバーレスアプリケーションの構築に特化しています。
| 特徴 | 内容 |
|---|---|
| インフラ定義 | TypeScript(sst.config.ts 1ファイル) |
| Live Lambda Dev | sst devでLambdaをローカル実行しつつAWSリソースに接続 |
| Resource Linking | linkプロパティでLambda→DynamoDB等の権限を自動設定 |
| Secret管理 | sst secret setでステージ別シークレット管理 |
| デプロイ | Pulumiエンジンで直接デプロイ(CloudFormation不要) |
プロジェクトセットアップ
# 新規プロジェクト npx create-sst@latest my-serverless-app cd my-serverless-app npm install # 既存プロジェクトにSSTを追加 npx sst@latest init # 開発サーバー起動(Live Lambda Dev) npx sst dev
my-serverless-app/ ├── sst.config.ts # インフラ + アプリ定義(IaC) ├── packages/ │ ├── functions/ # Lambda ハンドラー │ │ └── src/ │ │ ├── api/ # API Gateway ハンドラー │ │ ├── events/ # EventBridge ハンドラー │ │ └── lib/ # 共通ミドルウェア │ └── core/ # ビジネスロジック(Lambda非依存) │ └── src/ │ ├── entities/ # エンティティ定義 │ └── services/ # ビジネスサービス ├── CLAUDE.md └── package.json
CLAUDE.mdにサーバーレス設計ルールを記述する
## サーバーレスアーキテクチャ
### 技術スタック
- IaC: SST v3(TypeScript、sst.config.ts)
- ランタイム: Node.js 20.x(AWS Lambda)
- DB: DynamoDB(single-table design)
- API: API Gateway V2(HTTP API)
- バンドラー: esbuild(SST組み込み)
### Lambda ハンドラー規約
- 1ファイル1ハンドラー(packages/functions/src/api/ 配下)
- ハンドラーは薄く保つ(ビジネスロジックはpackages/core/に分離)
- レスポンス形式: { statusCode, headers, body } を統一
- DynamoDB client はハンドラー外(モジュールスコープ)で初期化
- 重いライブラリは動的import(コールドスタート対策)
### DynamoDB設計
- Single-table design を採用
- pk/sk のパターン: ENTITY#<id>, METADATA / RELATION#<id>
- GSI は最大2つまで(GSI1PK/GSI1SK, GSI2PK/GSI2SK)
- アクセスパターンをCLAUDE.mdに必ず定義してから実装
### IAM権限
- SST の link プロパティで自動権限付与を活用
- ワイルドカード (*) は原則禁止
- 追加権限は permissions で明示的に指定
### テスト
- aws-sdk-client-mock でDynamoDB/S3をモック
- sst の Resource モックは vi.mock("sst") で実装
- E2Eテストは sst dev のエンドポイントに対して実行
sst.config.ts ── インフラとアプリを1ファイルで定義する
sst.config.ts を生成してください。 構成: - DynamoDB テーブル(pk: string, sk: string) - API Gateway V2 + Lambda(CRUD: GET/POST/PUT/DELETE /items) - S3 バケット(画像アップロード用) - EventBridge(注文作成イベント) - Cronジョブ(日次レポート) 要件: - リージョン: ap-northeast-1 - 本番はremoval: "retain"、開発はremoval: "remove" - Lambda関数はlink でリソース権限を自動設定 - arm64アーキテクチャ(コールドスタート改善)
/// <reference path="./.sst/platform/config.d.ts" />
export default $config({
app(input) {
return {
name: "my-serverless-app",
removal: input?.stage === "production" ? "retain" : "remove",
home: "aws",
providers: {
aws: { region: "ap-northeast-1" },
},
};
},
async run() {
// DynamoDB
const table = new sst.aws.Dynamo("Items", {
fields: { pk: "string", sk: "string" },
primaryIndex: { hashKey: "pk", rangeKey: "sk" },
});
// S3
const bucket = new sst.aws.Bucket("Uploads");
// EventBridge
const bus = new sst.aws.Bus("EventBus");
// API Gateway + Lambda
const api = new sst.aws.ApiGatewayV2("Api");
const defaultFnProps = {
link: [table],
architecture: "arm64" as const,
nodejs: { esbuild: { minify: true, treeShaking: true } },
};
api.route("GET /items", {
handler: "packages/functions/src/api/items.list",
...defaultFnProps,
});
api.route("POST /items", {
handler: "packages/functions/src/api/items.create",
...defaultFnProps,
link: [table, bus],
});
api.route("PUT /items/{id}", {
handler: "packages/functions/src/api/items.update",
...defaultFnProps,
});
api.route("DELETE /items/{id}", {
handler: "packages/functions/src/api/items.remove",
...defaultFnProps,
});
// EventBridge サブスクリプション
bus.subscribe("ItemCreatedHandler", {
handler: "packages/functions/src/events/itemCreated.handler",
pattern: { source: ["myapp.items"], detailType: ["ItemCreated"] },
});
// Cronジョブ(毎日9:00 JST = 0:00 UTC)
new sst.aws.Cron("DailyReport", {
schedule: "cron(0 0 * * ? *)",
job: {
handler: "packages/functions/src/cron/dailyReport.handler",
link: [table],
},
});
return { apiUrl: api.url, bucketName: bucket.name };
},
});
Lambda関数の実装パターン
共通ミドルウェアで薄いハンドラーを実現する
import type { APIGatewayProxyHandlerV2 } from "aws-lambda";
type Handler = (event: any) => Promise<Record<string, unknown>>;
export function apiHandler(handler: Handler): APIGatewayProxyHandlerV2 {
return async (event) => {
try {
const result = await handler(event);
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify(result),
};
} catch (error) {
console.error("Handler error:", error);
const statusCode = (error as any).statusCode || 500;
return {
statusCode,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
error: error instanceof Error ? error.message : "Internal Server Error",
}),
};
}
};
}
import { Resource } from "sst";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, QueryCommand, PutCommand } from "@aws-sdk/lib-dynamodb";
import { apiHandler } from "../lib/middleware";
// DynamoDB clientはモジュールスコープで初期化(Lambda再利用時に再作成されない)
const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
export const list = apiHandler(async () => {
const result = await client.send(
new QueryCommand({
TableName: Resource.Items.name,
KeyConditionExpression: "pk = :pk",
ExpressionAttributeValues: { ":pk": "ITEM" },
})
);
return { items: result.Items ?? [] };
});
export const create = apiHandler(async (event) => {
const body = JSON.parse(event.body || "{}");
const id = crypto.randomUUID();
await client.send(
new PutCommand({
TableName: Resource.Items.name,
Item: {
pk: "ITEM",
sk: `ITEM#${id}`,
id,
name: body.name,
createdAt: new Date().toISOString(),
},
})
);
return { id };
});
Resource.Items.nameはSSTのResource Linkingで自動注入されるDynamoDBテーブル名です。環境変数を手動設定する必要がなく、link: [table]でテーブルへのアクセス権限も自動付与されます。DynamoDB Single-Table DesignをClaude Codeに設計させる
以下のアクセスパターンを満たすDynamoDB single-table designを設計してください。 エンティティ: User, Order, Product アクセスパターン: 1. ユーザーIDでユーザー取得 2. メールアドレスでユーザー検索 3. ユーザーの注文一覧(日付降順) 4. 注文IDで注文取得 5. カテゴリ別商品一覧(価格順) 制約: - GSI は最大2つ - pk/sk/GSI1PK/GSI1SK のパターンを表で示す - TypeScriptの型定義も含める
Claude Codeにアクセスパターンを伝えると、pk/sk設計、GSI設計、TypeScript型定義を一括で生成します。
/rewindでチェックポイントを作り、設計に納得してから実装に入りましょう。Live Lambda Devで高速にイテレーションする
sst devを起動すると、Lambda関数がローカルで実行されながら実際のAWSリソース(DynamoDB、S3等)に接続します。ファイル変更を検知して即座にホットリロードされるため、Claude Codeが生成したコードをすぐに動作確認できます。
# 開発サーバー起動 npx sst dev # 別ターミナルでAPIを叩いて確認 curl http://localhost:13557/items # Claude Codeとの開発ループ > items.ts の create ハンドラーにバリデーション(Zod)を追加してください # → Claude Codeがコード変更 → sst devが即ホットリロード → curlで確認
Lambda関数のテスト
packages/functions/src/api/items.ts の list と create に対する
ユニットテストを生成してください。
要件:
- aws-sdk-client-mock でDynamoDBをモック
- sst の Resource モックは vi.mock("sst")
- Vitest 使用
- 正常系と異常系(不正なJSON、DynamoDBエラー)を含む
import { describe, it, expect, vi, beforeEach } from "vitest";
import { mockClient } from "aws-sdk-client-mock";
import { DynamoDBDocumentClient, QueryCommand, PutCommand } from "@aws-sdk/lib-dynamodb";
const ddbMock = mockClient(DynamoDBDocumentClient);
vi.mock("sst", () => ({
Resource: { Items: { name: "test-table" } },
}));
import { list, create } from "../src/api/items";
function mockEvent(body?: Record<string, unknown>) {
return {
body: body ? JSON.stringify(body) : null,
headers: {},
isBase64Encoded: false,
pathParameters: {},
queryStringParameters: {},
} as any;
}
describe("items.list", () => {
beforeEach(() => ddbMock.reset());
it("アイテム一覧を返す", async () => {
ddbMock.on(QueryCommand).resolves({
Items: [{ pk: "ITEM", sk: "ITEM#1", id: "1", name: "Test" }],
});
const result = await list(mockEvent(), {} as any, () => {});
const body = JSON.parse((result as any).body);
expect(body.items).toHaveLength(1);
expect(body.items[0].name).toBe("Test");
});
});
describe("items.create", () => {
beforeEach(() => ddbMock.reset());
it("アイテムを作成してIDを返す", async () => {
ddbMock.on(PutCommand).resolves({});
const result = await create(mockEvent({ name: "New Item" }), {} as any, () => {});
const body = JSON.parse((result as any).body);
expect(body.id).toBeDefined();
});
});
テスト戦略全般はClaude Codeテスト完全ガイドをご覧ください。
GitHub Actions + SST でCI/CDを構築する
name: Deploy
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npx vitest run
deploy:
needs: test
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-arn: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ap-northeast-1
- run: npm ci
- run: npx sst deploy --stage production
id-token: write)を使うと、AWSアクセスキーをGitHub Secretsに保存せずにデプロイできます。IAM RoleのTrust PolicyでGitHub Actionsを許可する設定が必要です。GitHub Actionsの詳しい設定はClaude Code GitHub Actions完全ガイドをご覧ください。
よくある課題と対策
コールドスタート対策
// OK: DynamoDB clientはモジュールスコープで初期化(再利用される)
const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
// NG: ハンドラー内で毎回初期化(コールドスタートが長くなる)
export const handler = async () => {
const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
// ...
};
// OK: 重いライブラリは遅延ロード
async function getSharp() {
const sharp = await import("sharp");
return sharp.default;
}
SSTではarchitecture: "arm64"を指定するとGraviton2プロセッサで実行され、x86_64より起動が速くコストも20%安くなります。
シークレット管理
# ステージ別にシークレット設定
npx sst secret set STRIPE_KEY sk_live_xxx --stage production
npx sst secret set STRIPE_KEY sk_test_xxx --stage dev
# sst.config.ts での利用
const stripeKey = new sst.Secret("StripeKey");
api.route("POST /payments", {
handler: "packages/functions/src/payments.create",
link: [table, stripeKey], // 自動的に環境変数として注入
});
# Lambda内でアクセス
import { Resource } from "sst";
const key = Resource.StripeKey.value;
よくある質問
sst devは独自のステージ(通常はユーザー名)でリソースを作成するため、本番環境には影響しません。ただし同じAWSアカウントを使う場合、DynamoDB等のリソース名が衝突しないようSSTが自動で名前空間を分離します。architecture: "arm64"でGraviton2を使う。(2) DynamoDB clientなどの初期化をハンドラー外で行う。(3) 重いライブラリはawait import()で遅延ロードする。さらにクリティカルなAPIでは、SST v3でProvisioned Concurrencyを設定して常時ウォーム状態を維持できます。まとめ
- SST v3 + Claude Code: TypeScript 1ファイルでインフラもアプリも定義し、Claude Codeに一気通貫で生成させる
- CLAUDE.md設計ルール: ハンドラー規約・DynamoDB設計・IAM権限のルールを書いておけば正しいパターンで生成される
- Resource Linking:
linkプロパティで権限管理を自動化し、IAMポリシーの手動設定を不要にする - Live Lambda Dev:
sst devでローカル実行+実AWSリソース接続。Claude Codeの変更を即座に検証 - テスト:
aws-sdk-client-mockでDynamoDBモック + Vitestで高速ユニットテスト - CI/CD: GitHub Actions + OIDC +
sst deployでセキュアなデプロイ
IaC全般はClaude Code × Terraform完全ガイド、CLAUDE.mdの書き方はCLAUDE.md完全ガイドもあわせてご覧ください。

