Claude Code로 AWS Lambda를 안전하게 만드는 실전 가이드
Claude Code로 Node.js Lambda를 만들고 로컬 테스트, IAM 최소 권한, 환경 변수, API Gateway, 로그까지 확인합니다.
Claude Code는 AWS Lambda 작업에서 특히 빠릅니다. handler, 테스트 이벤트, IAM policy 초안, 배포 명령, 리뷰 체크리스트를 한 번에 준비할 수 있기 때문입니다. 하지만 AWS 권한과 비용은 실제 운영 결정입니다. Claude Code는 초안 작성과 검토 보조에 쓰고, IAM 권한 부여, 공개 API 생성, 과금 리소스 생성은 반드시 사람이 확인해야 합니다.
이 글에서는 팀에서 바로 재사용하기 쉬운 최소 흐름을 다룹니다. Node.js Lambda를 만들고, 로컬에서 테스트하고, IAM을 최소 권한에 가깝게 줄이고, 환경 변수, API Gateway, CloudWatch Logs, zip package 업데이트, 안전한 리뷰 루프까지 연결합니다. Lambda는 이벤트가 들어올 때 실행되는 작은 런타임이고, IAM은 함수가 무엇을 할 수 있는지 정하는 권한 표이며, API Gateway는 HTTP 요청을 Lambda로 전달하는 입구입니다.
실행 전 공식 문서를 같이 열어 두세요. AWS Lambda 시작하기, Node.js Lambda 가이드, Lambda 환경 변수, CloudWatch Logs 모니터링, Claude Code common workflows를 기준으로 확인합니다. 이 예시는 2026년 6월 1일 기준 Node.js 24 Lambda runtime을 사용합니다.
언제 이 방식이 맞는가
Lambda는 하루 종일 서버를 켜 둘 필요는 없지만, 짧은 작업을 안정적으로 처리해야 할 때 좋습니다. Claude Code는 반복 구조를 빠르게 만들고, 최종 판단은 reviewer가 맡는 방식에서 가장 효과적입니다.
| 사용 사례 | Lambda가 맞는 이유 | Claude Code가 만들 초안 | 사람이 확인할 부분 |
|---|---|---|---|
| Webhook 수신 | 결제, 폼, SaaS 이벤트는 짧게 처리된다 | 서명 검증, event fixture, 실패 응답 | secret, retry, 중복 이벤트 |
| 내부 JSON API | 작은 API는 상시 서버가 필요 없다 | handler, API Gateway 명령, 로그 형식 | 인증, CORS, 공개 범위, 비용 |
| 배치 작업 입구 | CSV, 이미지 처리, 알림 작업을 작게 시작할 수 있다 | 입력 검증, 구조화 로그, 에러 분류 | timeout, 재시도, 안전한 삭제 |
처음 검증할 때는 AWS CLI와 zip package가 빠릅니다. 팀 운영으로 넘어가면 SAM, CDK 또는 다른 IaC 리뷰 흐름으로 옮기는 것이 좋습니다.
| 방법 | 적합한 상황 | 주의할 점 |
|---|---|---|
| AWS Console | 한 번만 동작 확인 | 수동 절차가 리뷰 기록에 남지 않는다 |
| AWS CLI + zip | 작은 재현 가능한 검증 | IAM과 환경 변수를 꼼꼼히 봐야 한다 |
| SAM / CDK | 장기 팀 운영 | IaC 리뷰와 배포 승인 절차가 필요하다 |
flowchart LR
A[Claude Code 초안] --> B[로컬 Node.js 테스트]
B --> C[사람이 IAM과 환경 변수 확인]
C --> D[zip package를 dev에 배포]
D --> E[API Gateway로 테스트]
E --> F[CloudWatch Logs 확인]
F --> A
1. Node.js Lambda handler 만들기
외부 의존성이 없는 index.mjs부터 시작합니다. HTTP API v2 이벤트와 오래된 REST API 형식 모두 처리하도록 작성합니다.
// index.mjs
import crypto from "node:crypto";
const allowedStages = new Set(["dev", "staging", "prod"]);
function readConfig() {
const stage = process.env.APP_STAGE ?? "dev";
if (!allowedStages.has(stage)) {
throw new Error(`Invalid APP_STAGE: ${stage}`);
}
return {
stage,
tableName: process.env.TABLE_NAME ?? "local-orders",
logLevel: process.env.LOG_LEVEL ?? "info",
};
}
function log(level, message, details = {}) {
console.log(
JSON.stringify({
level,
message,
service: "orders-api",
...details,
})
);
}
function json(statusCode, body) {
return {
statusCode,
headers: {
"content-type": "application/json",
},
body: JSON.stringify(body),
};
}
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 config = readConfig();
const method = event.requestContext?.http?.method ?? event.httpMethod ?? "GET";
const path = event.rawPath ?? event.path ?? "/";
const requestId = context.awsRequestId ?? "local";
log("info", "request.start", {
requestId,
method,
path,
stage: config.stage,
});
try {
if (method === "GET" && path === "/health") {
return json(200, { ok: true, stage: config.stage });
}
if (method === "POST" && path === "/orders") {
const payload = parseBody(event);
const orderId = payload.orderId ?? crypto.randomUUID();
log("info", "order.accepted", {
requestId,
orderId,
tableName: config.tableName,
});
return json(202, {
orderId,
status: "accepted",
storedIn: config.tableName,
});
}
return json(404, { error: "not_found", method, path });
} catch (error) {
log("error", "request.failed", {
requestId,
errorName: error.name,
errorMessage: error.message,
});
return json(500, { error: "internal_error", requestId });
}
}
아직 DynamoDB에 쓰지 않습니다. 초보 단계에서는 이벤트 수신, JSON 파싱, 응답 형식, 검색 가능한 로그를 먼저 안정화하는 것이 더 중요합니다.
2. Event fixture로 로컬 테스트하기
fixture는 고정된 테스트 입력입니다. Claude Code가 생성하더라도 저장소에 남겨야 reviewer가 같은 입력으로 재현할 수 있습니다.
{
"version": "2.0",
"routeKey": "POST /orders",
"rawPath": "/orders",
"requestContext": {
"http": {
"method": "POST",
"path": "/orders"
}
},
"headers": {
"content-type": "application/json"
},
"body": "{\"orderId\":\"demo-1001\",\"amount\":3200,\"currency\":\"JPY\"}",
"isBase64Encoded": false
}
events/create-order.json으로 저장하고 로컬 실행 스크립트를 추가합니다.
// local-test.mjs
import { readFile } from "node:fs/promises";
import { handler } from "./index.mjs";
process.env.APP_STAGE = "dev";
process.env.TABLE_NAME = "orders-dev";
process.env.LOG_LEVEL = "debug";
const eventPath = process.argv[2] ?? "events/create-order.json";
const event = JSON.parse(await readFile(eventPath, "utf8"));
const result = await handler(event, { awsRequestId: "local-001" });
console.log(JSON.stringify(result, null, 2));
AWS를 만지기 전에 실행합니다.
node local-test.mjs events/create-order.json
로컬에서 statusCode: 202가 나오지 않으면 배포해도 디버깅만 느려집니다. 구체적인 fixture가 있으면 Claude Code도 추측이 아니라 실제 실패 입력을 보고 분석할 수 있습니다.
3. IAM은 최소 권한으로 시작하기
Lambda execution role에 AdministratorAccess를 붙이지 마세요. 먼저 로그 권한만 주고, 데이터 접근은 실제 handler가 필요할 때만 추가합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CreateOwnLogGroupIfMissing",
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": "arn:aws:logs:ap-northeast-1:123456789012:*"
},
{
"Sid": "WriteOwnLambdaLogs",
"Effect": "Allow",
"Action": ["logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": "arn:aws:logs:ap-northeast-1:123456789012:log-group:/aws/lambda/claude-orders-dev:*"
},
{
"Sid": "ReadWriteOnlyOrdersTable",
"Effect": "Allow",
"Action": ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:Query"],
"Resource": "arn:aws:dynamodb:ap-northeast-1:123456789012:table/orders-dev"
}
]
}
이 JSON은 운영 정답이 아니라 초안입니다. account, region, function name, table name을 바꾸고, DynamoDB를 아직 쓰지 않는다면 해당 블록을 삭제하세요. 권한 리뷰는 AWS IAM 가이드도 함께 보면 좋습니다.
4. AWS CLI로 zip package 배포하기
아래 명령은 실제 AWS 리소스를 만들며 비용이 발생할 수 있습니다. dev account에서 실행하고, region과 삭제 절차를 먼저 확인하세요.
export AWS_REGION=ap-northeast-1
export FUNCTION_NAME=claude-orders-dev
export ROLE_NAME=claude-orders-dev-lambda-role
export ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
zip function.zip index.mjs
Lambda가 role을 assume할 수 있도록 trust-policy.json을 만듭니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
앞의 IAM 초안을 lambda-policy.json으로 저장한 뒤 role과 함수를 만듭니다.
aws iam create-role \
--role-name "$ROLE_NAME" \
--assume-role-policy-document file://trust-policy.json
aws iam put-role-policy \
--role-name "$ROLE_NAME" \
--policy-name claude-orders-dev-inline \
--policy-document file://lambda-policy.json
ROLE_ARN=$(aws iam get-role \
--role-name "$ROLE_NAME" \
--query "Role.Arn" \
--output text)
aws lambda create-function \
--function-name "$FUNCTION_NAME" \
--runtime nodejs24.x \
--handler index.handler \
--zip-file fileb://function.zip \
--role "$ROLE_ARN" \
--architectures arm64 \
--timeout 10 \
--memory-size 128 \
--environment "Variables={APP_STAGE=dev,TABLE_NAME=orders-dev,LOG_LEVEL=info}" \
--region "$AWS_REGION"
코드만 업데이트할 때는 함수를 다시 만들 필요가 없습니다.
zip function.zip index.mjs
aws lambda update-function-code \
--function-name "$FUNCTION_NAME" \
--zip-file fileb://function.zip \
--region "$AWS_REGION"
환경 변수 업데이트는 특히 조심해야 합니다. update-function-configuration에서 Variables를 지정하면 전체 map이 교체됩니다. 기존 설정을 먼저 읽고 수정하세요. password나 API key는 환경 변수에 직접 넣기보다 Secrets Manager나 Parameter Store를 검토합니다.
aws lambda update-function-configuration \
--function-name "$FUNCTION_NAME" \
--environment "Variables={APP_STAGE=dev,TABLE_NAME=orders-dev,LOG_LEVEL=debug}" \
--region "$AWS_REGION"
5. API Gateway와 CloudWatch Logs 확인
먼저 Lambda를 직접 호출합니다.
aws lambda invoke \
--function-name "$FUNCTION_NAME" \
--payload fileb://events/create-order.json \
--region "$AWS_REGION" \
response.json
cat response.json
다음으로 HTTP API를 만듭니다. 공개 endpoint가 생기므로 인증, CORS, rate limit, 삭제 계획을 확인한 뒤 실행합니다.
API_ID=$(aws apigatewayv2 create-api \
--name claude-orders-dev-api \
--protocol-type HTTP \
--target "arn:aws:lambda:${AWS_REGION}:${ACCOUNT_ID}:function:${FUNCTION_NAME}" \
--query "ApiId" \
--output text)
aws lambda add-permission \
--function-name "$FUNCTION_NAME" \
--statement-id AllowHttpApiInvoke \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:${AWS_REGION}:${ACCOUNT_ID}:${API_ID}/*/*" \
--region "$AWS_REGION"
API_ENDPOINT=$(aws apigatewayv2 get-api \
--api-id "$API_ID" \
--query "ApiEndpoint" \
--output text)
curl -s "$API_ENDPOINT/health"
curl -s -X POST "$API_ENDPOINT/orders" \
-H "content-type: application/json" \
-d '{"amount":3200,"currency":"JPY"}'
테스트 직후 로그를 봅니다.
aws logs tail "/aws/lambda/${FUNCTION_NAME}" \
--follow \
--region "$AWS_REGION"
로그가 없다면 region, function name, logs:CreateLogStream, logs:PutLogEvents를 확인하세요. API Gateway가 500을 반환하면 Lambda response에 statusCode, headers, body가 있는지 봅니다. 더 자세한 내용은 AWS CloudWatch 가이드와 AWS API Gateway 가이드를 참고하세요.
Lambda 리뷰용 Claude prompt
Claude Code가 수정하거나 배포하기 전에 먼저 위험을 설명하게 합니다.
You are reviewing a Node.js AWS Lambda change for a team repository.
Scope:
- Files: index.mjs, events/*.json, IAM policy JSON, deployment commands in docs
- Runtime: nodejs24.x
- Entry point: index.handler
Review for:
1. API Gateway event compatibility and response shape
2. Input validation and JSON parsing failures
3. Environment variables that overwrite existing values
4. IAM actions that are wider than needed
5. CloudWatch logs that expose secrets or personal data
6. AWS resources that can create unexpected cost
7. Local test commands that a reviewer can copy and run
Return:
- blocking issues
- non-blocking improvements
- exact commands to verify locally
- questions a human must answer before deploying
안전한 순서는 탐색, 계획, 편집, 로컬 테스트, dev 배포, 로그 확인, 사람의 IAM 및 비용 승인입니다. 이 CLI proof of concept를 CI/CD와 IaC로 옮길 때는 AWS deployment guide를 이어서 보면 됩니다.
자주 하는 실수
첫 번째는 로컬 재현 없이 배포하는 것입니다. 이벤트 형태가 틀렸다면 CloudWatch보다 fixture가 더 빨리 잡아냅니다.
두 번째는 secret을 환경 변수에 직접 넣는 것입니다. 환경 변수는 설정에는 편하지만 secret에는 접근 제어와 회전 정책이 필요합니다.
세 번째는 IAM을 넓게 주는 것입니다. dynamodb:*와 Resource: "*"는 demo를 쉽게 통과시키지만 사고 범위도 넓힙니다. Claude Code에게 모든 권한을 줄 단위로 설명하게 하세요.
네 번째는 API Gateway URL을 owner 없이 공개하는 것입니다. 공유 전에 인증, CORS, throttling, logging, cleanup date를 정해야 합니다.
다섯 번째는 비용을 무시하는 것입니다. Lambda는 작아 보여도 API Gateway, CloudWatch Logs, DynamoDB, NAT, 외부 API 호출이 모두 비용을 만들 수 있습니다.
ClaudeCodeLab은 이런 리뷰 흐름을 Claude Code templates and training material로 정리하고 있습니다. 실제 저장소 기준으로 AWS 권한, CLAUDE.md, review prompt, 배포 승인 규칙을 설계하려면 Claude Code consultation and training을 참고하세요.
검증이 끝난 리소스는 삭제합니다.
aws apigatewayv2 delete-api --api-id "$API_ID" --region "$AWS_REGION"
aws lambda delete-function --function-name "$FUNCTION_NAME" --region "$AWS_REGION"
aws iam delete-role-policy --role-name "$ROLE_NAME" --policy-name claude-orders-dev-inline
aws iam delete-role --role-name "$ROLE_NAME"
이 글의 흐름을 실제로 따라 해 보니, Claude Code의 장점은 handler, fixture, IAM 초안, CLI 명령을 하나의 리뷰 가능한 문맥에 묶어 주는 데 있었습니다. 위험한 부분은 코드 생성이 아니라 권한과 공개 범위 판단이었습니다. 로컬 fixture, dev 배포, CloudWatch 증거, 그리고 IAM과 비용에 대한 사람의 승인 순서로 진행하면 작은 Lambda 자동화도 팀에서 안전하게 다룰 수 있습니다.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.
관련 글
Obsidian 메모를 CLAUDE.md로 바꾸는 Claude Code 워크플로
Obsidian 작업 메모를 CLAUDE.md 운영 노트로 정리해 Claude Code 세션의 문맥 반복을 줄입니다.
Claude Code Revenue CTA Routing: 글에서 PDF, Gumroad, 상담으로 보내기
독자 의도에 따라 무료 PDF, Gumroad 상품, 상담으로 나누는 Claude Code CTA 설계입니다.
Claude Code 팀 인계 규칙: 리뷰 증거, 권한, 롤백, 수익 경로까지 넘기는 법
Claude Code 작업을 팀에 넘길 때 필요한 증거, 권한 규칙, 롤백, 무료 PDF, Gumroad, 상담 경로 체크리스트.