Claude Code 환경 변수 관리 가이드: .env, Zod, Secrets, 프로덕션 배포
Claude Code에서 환경 변수와 Secrets를 안전하게 관리하는 방법: .env.example, Zod 검증, CI/CD 주입, 마스킹, 회전.
초보자가 Claude Code에 로그인, 결제, Webhook, AI 연동을 맡기면 가장 먼저 터지는 문제는 UI가 아니라 설정인 경우가 많습니다. 데이터베이스 URL이 빠져 있거나, staging key를 production에서 쓰거나, Webhook secret이 CI 로그에 찍히거나, 실제 API key가 GitHub에 올라가면 기능 구현이 끝났더라도 운영 사고가 됩니다.
이 글은 Claude Code로 환경 변수와 Secrets를 관리하는 구현 가능한 패턴을 정리합니다. 환경 변수는 PORT, APP_ORIGIN처럼 실행 시 앱에 전달되는 설정값입니다. Secret은 노출되면 악용될 수 있는 값입니다. 예를 들면 ANTHROPIC_API_KEY, DATABASE_URL, WEBHOOK_SECRET입니다. Secret도 환경 변수로 전달될 수 있지만, 취급 규칙은 더 엄격해야 합니다.
Claude Code 자체 설정은 공식 Claude Code environment variables를 확인하세요. 앱 설정 검증에는 Zod를 사용합니다. CI/CD와 배포는 GitHub Actions secrets, Vercel environment variables, Cloudflare Workers variables and secrets, Docker secrets 문서를 기준으로 삼으면 됩니다. 플랫폼마다 UI는 다르지만 원칙은 같습니다. 코드에는 필요한 key 목록을 남기고, 실제 secret 값은 남기지 않습니다.
전체 보안 관점은 Claude Code 보안 베스트 프랙티스와 Claude Code JWT 인증도 함께 보세요.
.env를 계약서로 다루기
.env는 편리하지만 개인 메모장이 되면 팀에서 바로 무너집니다. 필요한 것은 세 단계입니다.
- 선언:
.env.example에 필요한 key를 적는다 - 검증: 앱 시작 시 Zod로 필수 여부, URL 형식, 길이, 기본값을 확인한다
- 운영: CI/CD와 production은 로컬
.env를 복사하지 않고 플랫폼 Secrets에서 주입한다
flowchart LR
Dev["local .env.local"] --> Schema["Zod schema"]
CI["GitHub Actions secrets"] --> Schema
Prod["Vercel / Cloudflare / Docker secrets"] --> Schema
Schema --> App["Type-safe app config"]
Schema --> Logs["Redacted logs"]
Example[".env.example"] --> Dev
핵심은 모든 입력 경로가 같은 schema를 지나가게 하는 것입니다. Claude Code에게도 실제 값을 주는 것이 아니라 key 이름, 검증 조건, 실패 시 동작만 알려줘야 합니다.
실제 사용 사례
| 사용 사례 | 대표 변수 | 실패 시 문제 |
|---|---|---|
| 로컬 개발 | APP_ORIGIN, DATABASE_URL, ANTHROPIC_API_KEY | 특정 개발자 PC에서만 동작한다 |
| Webhook 검증 | STRIPE_WEBHOOK_SECRET, WEBHOOK_SECRET | 위조 요청을 정상 요청으로 받는다 |
| CI 테스트 | CI_DATABASE_URL, TEST_API_KEY | PR은 통과하지만 배포에서 실패한다 |
| 프로덕션 배포 | DATABASE_URL, SESSION_SECRET, APP_ORIGIN | 잘못된 DB 연결, 쿠키 오류, credential 노출 |
| Secret 회전 | ANTHROPIC_API_KEY_NEXT | 유출된 예전 key가 오래 살아 있다 |
Masa가 ClaudeCodeLab의 작은 SaaS 저장소에서 효과를 본 지점은 .env.example 작성 자체가 아니라, 설정이 부족하면 앱이 시작되지 않게 만든 것이었습니다. 운영 중 발견될 문제를 PR 단계에서 리뷰 가능한 실패로 바꾸는 효과가 있습니다.
1. 파일을 먼저 나누기
.env.example은 문서입니다. 실제 값은 넣지 않습니다. .env.local은 개발자 한 명의 로컬 값이고, .env.production.example은 production에 필요한 key 목록입니다.
mkdir -p src/config
touch .env.example .env.local .env.production.example src/config/env.ts
# .gitignore
.env
.env.*
!.env.example
!.env.production.example
# Cloudflare local secrets
.dev.vars
.dev.vars.*
# .env.example
APP_ENV=local
NODE_ENV=development
PORT=3000
APP_ORIGIN=http://localhost:3000
DATABASE_URL=postgresql://app:app@localhost:5432/app
ANTHROPIC_API_KEY=replace-with-local-dev-key
WEBHOOK_SECRET=replace-with-32-plus-character-secret
PUBLIC_ANALYTICS_KEY=
LOG_LEVEL=info
# .env.production.example
APP_ENV=production
NODE_ENV=production
PORT=3000
APP_ORIGIN=https://example.com
DATABASE_URL=<set-in-platform-secret-store>
ANTHROPIC_API_KEY=<set-in-platform-secret-store>
WEBHOOK_SECRET=<set-in-platform-secret-store>
PUBLIC_ANALYTICS_KEY=<optional-public-key>
LOG_LEVEL=info
replace-with-local-dev-key는 안전한 기본값이 아닙니다. 이 key가 필요하다는 표시일 뿐이며 production 값은 배포 플랫폼의 secret store에서 주입해야 합니다.
2. Zod로 시작 시 검증하기
Node.js에서 환경 변수는 모두 문자열입니다. 숫자로 보이는 PORT도 문자열이므로 z.coerce.number()로 변환하고 범위를 검증합니다.
npm install zod dotenv
npm install -D tsx typescript @types/node
// src/config/env.ts
import "dotenv/config";
import { z } from "zod";
const secretNamePattern = /(SECRET|TOKEN|PASSWORD|API_KEY|DATABASE_URL|DSN)/i;
function redactValue(key: string, value: unknown): string {
if (value === undefined || value === null || value === "") return "<empty>";
const text = String(value);
if (!secretNamePattern.test(key)) return text;
if (text.length <= 8) return "<redacted>";
return `${text.slice(0, 4)}...${text.slice(-4)}`;
}
const envSchema = z.object({
APP_ENV: z.enum(["local", "development", "staging", "production"]).default("local"),
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
APP_ORIGIN: z.string().url(),
DATABASE_URL: z.string().url(),
ANTHROPIC_API_KEY: z.string().min(20, "ANTHROPIC_API_KEY is too short"),
WEBHOOK_SECRET: z.string().min(32, "WEBHOOK_SECRET must be at least 32 characters"),
PUBLIC_ANALYTICS_KEY: z.string().optional(),
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
});
const parsed = envSchema.safeParse(process.env);
if (!parsed.success) {
console.error("Environment validation failed:");
for (const issue of parsed.error.issues) {
const key = String(issue.path[0] ?? "unknown");
console.error(`- ${key}: ${issue.message}; current=${redactValue(key, process.env[key])}`);
}
process.exit(1);
}
export const env = Object.freeze(parsed.data);
export type AppEnv = typeof env;
export function isProduction(): boolean {
return env.APP_ENV === "production";
}
export function publicEnv() {
return {
APP_ENV: env.APP_ENV,
APP_ORIGIN: env.APP_ORIGIN,
PUBLIC_ANALYTICS_KEY: env.PUBLIC_ANALYTICS_KEY ?? "",
};
}
로컬 확인:
cp .env.example .env.local
npx tsx src/config/env.ts
Claude Code에는 이렇게 리뷰를 요청하면 됩니다.
이 저장소에서 process.env를 직접 읽는 위치를 모두 찾아 주세요.
src/config/env.ts 외의 파일은 env 객체를 import해서 읽도록 수정안을 제시해 주세요.
secret은 로그, 에러 메시지, 테스트 스냅샷에 출력하지 마세요.
3. 로그는 반드시 마스킹하기
Secret 유출은 Git 커밋에서만 일어나지 않습니다. CI 로그, 디버그 출력, 에러 모니터링, 화면 녹화, Claude Code에 붙여 넣은 터미널 출력에서도 일어납니다.
// src/config/redact.ts
const sensitiveKeyPattern = /(SECRET|TOKEN|PASSWORD|API_KEY|DATABASE_URL|AUTH|COOKIE|PRIVATE)/i;
export function redactSecrets(input: Record<string, unknown>): Record<string, string> {
return Object.fromEntries(
Object.entries(input).map(([key, value]) => {
if (value === undefined || value === null || value === "") return [key, "<empty>"];
const text = String(value);
if (!sensitiveKeyPattern.test(key)) return [key, text];
return [key, text.length <= 10 ? "<redacted>" : `${text.slice(0, 4)}...${text.slice(-4)}`];
}),
);
}
import { env } from "./env";
import { redactSecrets } from "./redact";
console.info("Loaded config", redactSecrets(env));
마스킹은 마지막 방어선입니다. 가장 좋은 로그 설계는 secret을 애초에 로그 대상으로 넘기지 않는 것입니다.
4. CI/CD에서는 Secrets에서 주입하기
GitHub Actions에서는 repository, environment, organization 수준의 secrets를 workflow에 넘길 수 있습니다. 일반 PR 테스트에 production credential을 재사용하지 말고, 범위가 좁은 CI 전용 값을 만드세요.
# .github/workflows/env-check.yml
name: env-check
on:
pull_request:
push:
branches: [main]
jobs:
validate-env:
runs-on: ubuntu-latest
env:
APP_ENV: development
NODE_ENV: test
PORT: 3000
APP_ORIGIN: http://localhost:3000
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
WEBHOOK_SECRET: ${{ secrets.WEBHOOK_SECRET }}
LOG_LEVEL: info
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
- run: npm ci
- name: Mask runtime-only values
run: echo "::add-mask::$APP_ORIGIN"
- run: npx tsx src/config/env.ts
- run: npm test -- --runInBand
모든 workflow가 secrets를 받는다고 가정하면 안 됩니다. fork PR, reusable workflow, Dependabot 이벤트는 조건이 다를 수 있습니다. validation job을 명확히 두고, 생성 파일에 secret을 쓰지 마세요.
5. Docker, Vercel, Cloudflare에서의 주의점
Docker에서는 Dockerfile에 ENV API_KEY=...를 쓰지 마세요. 로컬 테스트에는 env file을 사용할 수 있지만 production은 런타임의 secret store를 사용하는 편이 안전합니다.
# local only
docker run --rm --env-file .env.local my-app:latest
secret이 파일로 마운트되는 런타임을 위해 NAME_FILE도 지원할 수 있습니다.
// src/config/secret-file.ts
import fs from "node:fs";
export function readEnvOrFile(name: string): string | undefined {
const direct = process.env[name];
if (direct) return direct;
const filePath = process.env[`${name}_FILE`];
if (!filePath) return undefined;
return fs.readFileSync(filePath, "utf8").trim();
}
Vercel에서는 Production, Preview, Development 값을 분리하세요. NEXT_PUBLIC_처럼 브라우저에 노출되는 prefix도 조심해야 합니다. Cloudflare Workers에서는 Worker binding, env 파라미터, 플랫폼 Secrets를 통해 값을 받습니다. 특정 플랫폼에 과하게 맞추기보다 schema를 기준으로 삼고, 플랫폼별 주입 위치를 문서화하세요.
Vercel, Cloudflare, Docker 환경 변수 설정을 리뷰해 주세요.
실제 production 값은 읽거나 요청하지 마세요.
필수 key, public/secret 경계, build-time/runtime 차이, rotation 메모 누락 여부만 확인하세요.
6. 회전 playbook을 먼저 쓰기
Secret rotation은 사고가 난 뒤에 만들면 늦습니다.
- 범위 확인: 서비스, 환경, 권한, 담당자
- 최소 권한의 새 값을 생성
- 가능하면
*_NEXT로 먼저 추가 - 짧은 기간 동안 신/구 값을 모두 허용
- 배포 후 health check 확인
- 예전 값을 revoke
- Git history, CI logs, monitoring logs, prompt history 검색
.env.example과 운영 문서 업데이트
Webhook secret, API key, database password는 회전 방식이 다릅니다. 각 값마다 담당자와 rollback 경로를 적어 두세요.
흔한 실패
| 실패 | 원인 | 대책 |
|---|---|---|
.env가 commit됨 | .gitignore가 늦게 추가됨 | 즉시 key를 폐기하고, history 정리만으로 끝내지 않기 |
secret을 NEXT_PUBLIC_에 넣음 | public prefix 의미를 모름 | public/private naming rule을 분리 |
console.log(process.env) | 급한 디버깅 | redaction과 로그 리뷰 적용 |
| production만 시작 실패 | 플랫폼에 필수 key 누락 | CI에서 src/config/env.ts 실행 |
| local 값으로 production build | build-time/runtime 혼동 | 플랫폼별 injection 문서화 |
| 실제 key를 Claude Code에 붙여넣음 | 구현 요청과 secret 공유를 혼동 | key 이름과 검증 규칙만 전달 |
바로 쓸 수 있는 Claude Code 프롬프트
이 프로젝트의 환경 변수 관리를 구현해 주세요.
요구사항:
- .env.example 과 .env.production.example 생성
- .env, .env.*, .dev.vars* 는 Git에서 제외
- src/config/env.ts 에 Zod schema를 만들고 누락/오류 값을 시작 시 실패 처리
- process.env 직접 참조는 src/config/env.ts로 집중
- 진단 로그에서 secret은 마스킹
- PR에서 env validation을 실행하는 GitHub Actions job 추가
- Vercel, Cloudflare, Docker 배포 메모를 짧게 작성
실제 API key나 production database URL은 읽지 말고, key 이름과 검증 규칙만 사용하세요.
정리
Claude Code로 환경 변수 관리를 잘하려면 secret을 채팅에 붙여 넣는 것이 아니라, 검증 가능한 계약을 구현하게 해야 합니다. .env.example로 key를 선언하고, Zod로 시작 시 검증하고, 로그를 마스킹하고, CI/CD와 배포 플랫폼에서 실제 값을 주입하고, rotation 절차를 준비합니다.
ClaudeCodeLab은 Claude Code 도입 컨설팅, 팀 교육, 저장소 보안 리뷰, 인증/결제/CI/CD/content operations 템플릿을 제공합니다. Claude Code의 속도를 살리면서 production key 유출을 막고 싶다면 환경 변수 관리부터 표준화하는 것이 좋습니다.
Masa의 테스트 저장소에서는 이 패턴으로 production key 누락, Webhook secret 로그 출력 가능성, 오래된 .env.example 세 가지를 배포 전에 잡았습니다. Zod 시작 검증은 단순하지만, 개인 지식이던 설정을 팀의 실행 가능한 계약으로 바꿉니다.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.
관련 글
Claude Code 권한 세이프티 래더: 통제력을 잃지 않고 allow 넓히기
read-only에서 제한 편집, 검증 명령, deploy 확인까지 권한을 단계적으로 넓히는 방법.
Claude Code Small PR Proof Pack: 작은 PR을 리뷰 가능한 상태로 만드는 증거 세트
Claude Code의 작은 PR에 diff, 검증, 공개 URL, CTA 경로, rollback을 붙이는 실무 체크리스트.
Claude Code 커밋 전 리뷰 게이트: diff, 테스트, 공개 URL, CTA 확인
Claude Code 작업을 커밋하기 전에 diff 범위, build, 공개 URL, Gumroad 링크, 상담 CTA, 테스트 누락과 무관한 파일을 확인하는 방법입니다.