Claude Codeでサーバーレス関数を作る実践ガイド
Claude Codeでサーバーレス関数を安全に作る手順。要件定義、AWS Lambda/Workers選定、環境変数、冪等性、テスト、デプロイ確認まで解説。
サーバーレス関数とは、サーバーを常時管理せず、イベントやHTTPリクエストごとに短時間コードを実行する仕組みです。小さなAPI、Webhook、画像処理、定期ジョブの入口をすばやく作れる一方で、タイムアウト、再試行、権限、シークレット、課金の判断を雑にすると本番で痛みます。
Claude Codeはこの領域と相性が良いです。ハンドラー、イベントfixture、テスト、デプロイ手順、レビュー観点を同じ文脈で作れるからです。ただし「Claude Codeに全部任せる」のではなく、人間が要件、プラットフォーム、公開範囲、データ保護を決める使い方が安全です。
この記事では、AWS LambdaとCloudflare Workersを中心に、Claude Codeへ渡す要件プロンプト、ランタイム選定、ローカル開発、環境変数とシークレット、冪等性、タイムアウトとリトライ、テスト、デプロイ前チェックまでを実務の順番で整理します。AWS側の詳細はAWS Lambdaガイド、エッジ実行はCloudflare Workersガイド、API設計はAPI開発ガイド、機密情報の扱いはシークレット管理ガイドもあわせて確認してください。
公式ドキュメントは作業前に開いておきます。AWS Lambdaの基本はAWS Lambda Documentation、Node.jsランタイムはBuilding Lambda functions with Node.js、Workersのローカル開発はDevelopment & testing、開始手順はCloudflare Workers get started guideが基準です。
使いどころを先に決める
サーバーレス関数は「軽い処理なら何でも安くなる魔法」ではありません。処理時間が短い、状態を外部ストレージへ逃がせる、イベント単位で失敗を切り分けられる場面で強くなります。
| ユースケース | 向いている理由 | Claude Codeに任せやすい作業 | 人間が確認すること |
|---|---|---|---|
| 決済・フォームのWebhook受信 | リクエスト単位で完結しやすい | 署名検証の雛形、イベントfixture、失敗レスポンス | シークレット、重複通知、リトライ時の二重処理 |
| 画像リサイズ・CSV取り込みの入口 | 重い処理をキューやストレージへ逃がせる | 入力検証、ジョブID発行、ログ形式 | タイムアウト、ファイルサイズ、失敗時の削除 |
| 社内ツールの小さなJSON API | 常時稼働サーバーなしで始められる | ハンドラー、テスト、API Gateway/Workersルート | 認証、CORS、公開範囲、利用量制限 |
| 記事サイトのエッジ処理 | 地理的に近い場所で応答できる | キャッシュ制御、リダイレクト、A/B分岐 | キャッシュ破棄、個人情報、SEOへの影響 |
流れは次のように固定すると、Claude Codeの出力をレビューしやすくなります。
flowchart LR
A[要件プロンプトを書く] --> B[Lambda/Workersを選ぶ]
B --> C[ローカルでイベントを再現]
C --> D[環境変数とシークレットを分離]
D --> E[冪等性とリトライを設計]
E --> F[テストを通す]
F --> G[devへデプロイ]
G --> H[ログと削除手順を確認]
Claude Codeに渡す要件プロンプト
最初のプロンプトで「何を作るか」だけを書くと、便利そうなコードは出ますが、レビューできる成果物になりません。実務では制約、入力例、失敗時の期待値、触ってよいファイル、触ってはいけないものを明記します。
Node.jsでサーバーレス関数の最小実装を作ってください。
目的:
- POST /orders を受け取り、注文受付レスポンスを返す
- ローカルで node local-test.mjs だけで動作確認できる
- AWS LambdaのHTTP API v2イベントを想定する
要件:
- index.mjs, events/create-order.json, local-test.mjs, index.test.mjs を作る想定で説明
- idempotency-key ヘッダーがない場合は400
- 同じ idempotency-key が来たら同じレスポンスを返す
- JSON parse失敗、入力不足、未対応パスを分けて返す
- ログはJSON形式。ただしシークレットや個人情報は出さない
- デプロイ前チェックリストも出す
制約:
- 外部npmパッケージなし
- 本番の永続化はDynamoDBやKVに置き換える前提
- IAM、公開URL、課金が発生する操作は人間確認にする
このプロンプトの狙いは、Claude Codeを「コード生成係」ではなく「レビュー可能な作業単位をそろえる係」として使うことです。とくに冪等性、つまり同じリクエストが再送されても結果が壊れない性質は、Webhookやキュー処理では最初から設計しておきます。
ランタイムとプラットフォームを選ぶ
AWS LambdaはIAM、S3、DynamoDB、EventBridge、SQSなどAWSサービスとつなぐ処理に向いています。Node.jsランタイムの仕様は公式のNode.jsページで確認し、サンプルを作る時点のサポート状況に合わせます。
Cloudflare WorkersはエッジでのHTTP処理、軽い認証、リダイレクト、キャッシュ制御、KV/D1/R2との組み合わせに向いています。ローカル検証はWranglerを使うため、Workersの開発・テスト手順を先に確認します。
Vercel FunctionsやEdge FunctionsはNext.jsアプリと一体でAPIやOG画像生成を持ちたい時に便利です。ただしこの記事では、公式リンクを確認しやすく、サーバーレス関数の基礎を説明しやすいAWS LambdaとCloudflare Workersに絞ります。
| 判断軸 | AWS Lambda | Cloudflare Workers |
|---|---|---|
| 得意な処理 | AWSイベント、業務API、非同期ジョブ | エッジHTTP、キャッシュ、軽量API |
| ローカル開発 | Node.js、SAM、AWS CLI | Wrangler、Miniflare相当のローカル実行 |
| 権限管理 | IAMロールとポリシー | アカウント権限、binding、secret |
| 注意点 | IAM過大、VPC/NAT費用、ログ量 | 実行時間、binding差分、KVの整合性 |
Lambdaをローカルで動かす最小コード
まずAWSへデプロイせず、Node.jsだけで動くハンドラーを作ります。これは本番完成形ではありません。ローカルでイベント、レスポンス、ログ、冪等性の振る舞いを固定するための足場です。
// index.mjs
import crypto from "node:crypto";
const localIdempotencyStore = new Map();
function json(statusCode, body) {
return {
statusCode,
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
};
}
function readHeader(headers = {}, name) {
const target = name.toLowerCase();
const found = Object.entries(headers).find(([key]) => key.toLowerCase() === target);
return found?.[1];
}
function parseBody(event) {
if (!event.body) return {};
const raw = event.isBase64Encoded
? Buffer.from(event.body, "base64").toString("utf8")
: event.body;
return JSON.parse(raw);
}
export async function handler(event = {}, context = {}) {
const method = event.requestContext?.http?.method ?? event.httpMethod ?? "GET";
const path = event.rawPath ?? event.path ?? "/";
const requestId = context.awsRequestId ?? crypto.randomUUID();
console.log(JSON.stringify({ level: "info", message: "request.start", requestId, method, path }));
if (method !== "POST" || path !== "/orders") {
return json(404, { error: "not_found" });
}
const idempotencyKey = readHeader(event.headers, "idempotency-key");
if (!idempotencyKey) {
return json(400, { error: "idempotency_key_required" });
}
if (localIdempotencyStore.has(idempotencyKey)) {
return json(200, { ...localIdempotencyStore.get(idempotencyKey), replay: true });
}
let body;
try {
body = parseBody(event);
} catch {
return json(400, { error: "invalid_json" });
}
if (!Number.isFinite(body.amount) || body.amount <= 0 || typeof body.currency !== "string") {
return json(400, { error: "invalid_order" });
}
const accepted = {
orderId: crypto.randomUUID(),
status: "accepted",
amount: body.amount,
currency: body.currency,
};
localIdempotencyStore.set(idempotencyKey, accepted);
console.log(JSON.stringify({ level: "info", message: "order.accepted", requestId, orderId: accepted.orderId }));
return json(202, accepted);
}
イベントfixtureも一緒に用意します。
{
"version": "2.0",
"routeKey": "POST /orders",
"rawPath": "/orders",
"headers": {
"content-type": "application/json",
"idempotency-key": "demo-key-001"
},
"requestContext": {
"http": {
"method": "POST",
"path": "/orders"
}
},
"body": "{\"amount\":3200,\"currency\":\"JPY\"}",
"isBase64Encoded": false
}
ローカル実行用の小さなランナーです。
// local-test.mjs
import { readFile } from "node:fs/promises";
import { handler } from "./index.mjs";
const eventPath = process.argv[2] ?? "events/create-order.json";
const event = JSON.parse(await readFile(eventPath, "utf8"));
const first = await handler(event, { awsRequestId: "local-001" });
const second = await handler(event, { awsRequestId: "local-002" });
console.log("first:", first.statusCode, first.body);
console.log("second:", second.statusCode, second.body);
実行します。
node local-test.mjs events/create-order.json
2回目がreplay: trueを返せば、同じキーの再送を壊さず処理できています。ただし、このMapは本番の冪等性ストアではありません。Lambdaの実行環境は再利用されることも破棄されることもあるため、本番ではDynamoDBの条件付き書き込み、RDSのユニーク制約、Cloudflare KV/D1など外部ストアへ置き換えます。
テストで失敗例を固定する
サーバーレス関数は、AWSやCloudflareへ上げてから手で確認すると遅くなります。標準のnode:testだけでも、最低限の失敗例は固定できます。
// index.test.mjs
import crypto from "node:crypto";
import test from "node:test";
import assert from "node:assert/strict";
import { handler } from "./index.mjs";
function event(overrides = {}) {
return {
rawPath: "/orders",
headers: { "idempotency-key": crypto.randomUUID() },
requestContext: { http: { method: "POST" } },
body: JSON.stringify({ amount: 1200, currency: "JPY" }),
isBase64Encoded: false,
...overrides,
};
}
test("requires idempotency-key", async () => {
const result = await handler(event({ headers: {} }), {});
assert.equal(result.statusCode, 400);
});
test("accepts a valid order", async () => {
const result = await handler(event(), {});
assert.equal(result.statusCode, 202);
assert.equal(JSON.parse(result.body).status, "accepted");
});
test("rejects invalid JSON", async () => {
const result = await handler(event({ body: "not-json" }), {});
assert.equal(result.statusCode, 400);
});
テストも外部パッケージなしで動きます。CIに載せる前に、まずローカルで失敗例が固定されていることを確認します。
node --test index.test.mjs
Cloudflare Workersで同じ考え方を使う
Workersではfetch(request, env, ctx)が入口になります。KV bindingを使えば、冪等性キーの保存先を外へ出せます。
// src/worker.js
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (request.method !== "POST" || url.pathname !== "/orders") {
return Response.json({ error: "not_found" }, { status: 404 });
}
if (request.headers.get("x-webhook-secret") !== env.WEBHOOK_SECRET) {
return Response.json({ error: "unauthorized" }, { status: 401 });
}
const idempotencyKey = request.headers.get("idempotency-key");
if (!idempotencyKey) {
return Response.json({ error: "idempotency_key_required" }, { status: 400 });
}
const existing = await env.IDEMPOTENCY_KV.get(idempotencyKey, "json");
if (existing) {
return Response.json({ ...existing, replay: true });
}
const body = await request.json();
if (!Number.isFinite(body.amount) || typeof body.currency !== "string") {
return Response.json({ error: "invalid_order" }, { status: 400 });
}
const accepted = {
orderId: crypto.randomUUID(),
status: "accepted",
amount: body.amount,
currency: body.currency,
};
await env.IDEMPOTENCY_KV.put(idempotencyKey, JSON.stringify(accepted), {
expirationTtl: 86400,
});
return Response.json(accepted, { status: 202 });
},
};
設定ファイルは次の形です。idは自分のKV namespace IDに置き換えます。
{
"name": "serverless-orders-worker",
"main": "src/worker.js",
"compatibility_date": "2026-06-01",
"kv_namespaces": [
{
"binding": "IDEMPOTENCY_KV",
"id": "replace_with_real_kv_namespace_id"
}
]
}
ローカル開発とシークレット設定はWranglerで進めます。
npm create cloudflare@latest serverless-orders-worker
cd serverless-orders-worker
npx wrangler kv namespace create IDEMPOTENCY_KV
npx wrangler secret put WEBHOOK_SECRET
npx wrangler dev
Workersのenvは、通常の環境変数ではなくbindingとして渡されます。Claude Codeへ依頼するときは「.dev.varsにローカル用値を置くが、記事やログに実値を出さない」と明記してください。
タイムアウト、リトライ、冪等性の落とし穴
サーバーレスで一番多い失敗は「一度だけ実行される」と思い込むことです。Webhook送信元、キュー、非同期呼び出し、ユーザーのブラウザは、失敗やタイムアウトで同じリクエストを再送します。
1つ目の落とし穴は二重決済や二重登録です。idempotency-key、外部イベントID、注文IDなどをユニークキーとして保存し、再送時は同じ結果を返します。
2つ目はタイムアウト直前の副作用です。外部APIへ送った直後に関数がタイムアウトすると、呼び出し側は失敗と見なして再送することがあります。時間のかかる処理はキューへ逃がし、関数は「受け付けた」ことだけを返す設計が安定します。
3つ目はシークレットの露出です。環境変数は設定値には便利ですが、ログへ出したり、記事のサンプルへ実値を書いたりしてはいけません。AWSではSecrets ManagerやParameter Store、Cloudflareではwrangler secretを検討します。
4つ目は過大な権限です。Claude Codeが提案したIAMに*が多い場合、通ることより狭めることを優先します。S3、DynamoDB、CloudWatch Logsなど、必要なActionとResourceを1行ずつ説明させます。
5つ目は削除手順の不足です。検証用のAPI Gateway、Lambda、KV、ログ、ドメイン設定を作ったら、削除コマンドや管理画面での削除手順をPR本文に残します。小さな検証でも課金と公開URLは本番事故につながります。
デプロイ前チェックリスト
デプロイは「動いたら終わり」ではなく「戻せる、観測できる、止められる」状態にしてから行います。
実務では、このチェックリストをPR本文にそのまま貼るくらいがちょうどよいです。Claude Codeが生成した差分は速くても、レビュアーは背景を知りません。どのイベントで起動し、どの環境変数を読み、どのシークレットを参照し、同じ通知が再送された時に何が起きるのかを短く書くと、レビューはコードの好みではなく運用リスクの確認になります。
| チェック項目 | 確認内容 |
|---|---|
| 要件 | 入力、出力、失敗時レスポンス、所有者が書かれている |
| ランタイム | LambdaのNode.jsランタイム、Workersのcompatibility_dateを確認した |
| ローカル | fixtureとnode --testが通る |
| 環境変数 | 設定値とシークレットを分け、ログに実値が出ない |
| 冪等性 | 再送時に同じ結果を返すか、重複登録しない |
| タイムアウト | 外部API、画像処理、DB処理が制限時間内に収まる |
| リトライ | 呼び出し元の再試行仕様を読み、二重処理を防いだ |
| 監視 | JSONログ、エラー率、アラート、ログ保持期間を決めた |
| 公開範囲 | 認証、CORS、レート制限、削除日を確認した |
| ロールバック | 直前バージョンへ戻す手順とリソース削除手順がある |
Lambdaなら最小の更新は次のような流れです。新規作成やIAM作成は、チームのIaCや承認フローへ移してください。
zip function.zip index.mjs
aws lambda update-function-code \
--function-name serverless-orders-dev \
--zip-file fileb://function.zip
Workersはローカル確認後にデプロイします。
npx wrangler dev
npx wrangler deploy
Claude Codeには最後にこう依頼します。
このサーバーレス関数の変更を公開前レビューしてください。
観点は、冪等性、timeout/retry、secret漏えい、IAMまたはbindingの過大権限、
ログの個人情報、ローカルテストの再現性、削除手順、内部リンクと公式リンクです。
blocking / non-blocking / human confirmation に分けて返してください。
まとめ
Claude Codeでサーバーレス関数を作る価値は、コードを速く出すことだけではありません。要件、fixture、テスト、環境変数、冪等性、デプロイ前チェックを同じ流れに置けることが本当の利点です。小さい関数ほどレビューが甘くなりがちなので、最初から「再送される」「失敗する」「公開される」「課金される」前提で設計します。
個人開発なら、この記事のLambdaサンプルをローカルで動かし、次にWorkers版でKV bindingを試すだけでも十分な練習になります。チーム導入では、CLAUDE.md、レビュー用プロンプト、IAM方針、デプロイ承認、ログ確認までをセットにしてください。ClaudeCodeLabでは、こうした実務フローを教材・テンプレートとして整理しており、既存リポジトリに合わせた導入設計はClaude Code導入相談・研修で相談できます。
この記事で紹介した内容を実際に試した結果、もっとも効いたのは「先にイベントfixtureを作る」ことでした。Claude Codeの生成コードは速い一方、リトライ時の二重処理やシークレットの扱いは人間が明示しないと抜けやすいです。ローカル再現、冪等性、ログ確認、削除手順を最初のプロンプトに入れるだけで、サーバーレス関数のレビュー品質はかなり安定します。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
ObsidianメモをCLAUDE.mdに変えるClaude Code運用: 文脈を毎回説明しない仕組み
Obsidianの作業メモからCLAUDE.md用の運用ノートを作り、Claude Codeに安定した文脈を渡す方法。
Claude Code Revenue CTA Routing: 記事からPDF、Gumroad、相談へ送る設計
PVだけで終わらせず、読者の状態に合わせて無料PDF、Gumroad教材、導入相談へ分岐するCTA設計です。
Claude Codeチーム引き継ぎルール: レビュー、権限、収益導線まで渡す実務手順
Claude Codeの作業をチームで渡すための証拠、権限、ロールバック、無料PDF/Gumroad/相談導線の実務ルール。