Claude Code × サーバーレス開発実践ガイド|SST v3・AWS Lambda・DynamoDB・Live Lambda Dev・CI/CD

Claude Code × サーバーレス開発実践ガイド|SST v3・AWS Lambda・DynamoDB・Live Lambda Dev・CI/CD AI開発

サーバーレスアーキテクチャは、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不要)
Claude Codeとの好相性:インフラもアプリケーションもすべてTypeScriptで記述するため、Claude Codeが「IaC定義→Lambda関数→テスト」を一貫した文脈で生成できます。JSON/YAMLの設定ファイルと行き来する必要がありません。

プロジェクトセットアップ

SST v3 プロジェクト作成
# 新規プロジェクト
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にサーバーレス設計ルールを記述する

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 生成プロンプト
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アーキテクチャ(コールドスタート改善)
生成されるsst.config.ts
/// <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関数の実装パターン

共通ミドルウェアで薄いハンドラーを実現する

packages/functions/src/lib/middleware.ts
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",
        }),
      };
    }
  };
}
packages/functions/src/api/items.ts
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設計プロンプト
以下のアクセスパターンを満たす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型定義を一括で生成します。

DynamoDBのsingle-table designは一度決めると変更コストが高いです。/rewindでチェックポイントを作り、設計に納得してから実装に入りましょう。

Live Lambda Devで高速にイテレーションする

sst devを起動すると、Lambda関数がローカルで実行されながら実際のAWSリソース(DynamoDB、S3等)に接続します。ファイル変更を検知して即座にホットリロードされるため、Claude Codeが生成したコードをすぐに動作確認できます。

Live Lambda Dev の使い方
# 開発サーバー起動
npx sst dev

# 別ターミナルでAPIを叩いて確認
curl http://localhost:13557/items

# Claude Codeとの開発ループ
> items.ts の create ハンドラーにバリデーション(Zod)を追加してください
# → Claude Codeがコード変更 → sst devが即ホットリロード → curlで確認
Live Lambda Devは実際のAWSリソースに接続するため、DynamoDBの実データで動作確認できます。モック不要で本番に近い環境での開発が可能です。

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を構築する

.github/workflows/deploy.yml
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
OIDC(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%安くなります。

シークレット管理

SST Secret の使い方
# ステージ別にシークレット設定
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;

よくある質問

QSSTとAWS CDKの違いは何ですか?
ASSTはサーバーレスアプリ構築に特化しており、Live Lambda Dev・Resource Linking・Console UIなど開発体験を重視した機能が豊富です。CDKは汎用のIaCツールで、Lambda以外のAWSリソースも幅広くカバーしますが、サーバーレス特化の開発ツール(ローカル実行等)は別途用意する必要があります。Claude Codeとの連携では、SST v3がTypeScript 1ファイルで完結する点が大きな利点です。
Qsst devのLive Lambda Devは本番環境に影響しますか?
Asst devは独自のステージ(通常はユーザー名)でリソースを作成するため、本番環境には影響しません。ただし同じAWSアカウントを使う場合、DynamoDB等のリソース名が衝突しないようSSTが自動で名前空間を分離します。
QDynamoDB single-table designは必須ですか?
A必須ではありません。エンティティが少ない(3つ以下程度の)シンプルなアプリでは、テーブルを分けたほうが理解しやすくなります。single-table designが有効なのは、「1回のクエリで関連エンティティをまとめて取得したい」場合です。Claude Codeにアクセスパターンを伝えると、テーブル分割かsingle-tableかを含めて設計を提案してくれます。
Qコールドスタートが気になります。対策を教えてください
A3つの基本対策があります。(1) architecture: "arm64"でGraviton2を使う。(2) DynamoDB clientなどの初期化をハンドラー外で行う。(3) 重いライブラリはawait import()で遅延ロードする。さらにクリティカルなAPIでは、SST v3でProvisioned Concurrencyを設定して常時ウォーム状態を維持できます。
QTerraform記事との違いは何ですか?
AClaude Code × Terraform記事は汎用IaCツールとしてのTerraformとClaude Codeの連携に焦点を当てています。この記事はAWSサーバーレスアプリケーションの「構築」に焦点を当て、SST v3というサーバーレス特化フレームワークを使ったLambda関数の実装・DynamoDB設計・テスト・デプロイの実践的なフローを解説しています。

まとめ

  • 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完全ガイドもあわせてご覧ください。