From ecc
배포 워크플로, CI/CD 파이프라인 패턴, Docker 컨테이너화, 헬스 체크, 롤백 전략 및 웹 애플리케이션을 위한 프로덕션 준비 체크리스트입니다.
npx claudepluginhub sam42-lab/everything-claude-code-krThis skill uses the workspace's default tool permissions.
프로덕션 배포 워크플로 및 CI/CD 모범 사례입니다.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
프로덕션 배포 워크플로 및 CI/CD 모범 사례입니다.
인스턴스를 점진적으로 교체합니다. 교체되는 동안 이전 버전과 새 버전이 동시에 실행됩니다.
인스턴스 1: v1 → v2 (첫 번째 업데이트)
인스턴스 2: v1 (여전히 v1 실행 중)
인스턴스 3: v1 (여전히 v1 실행 중)
인스턴스 1: v2
인스턴스 2: v1 → v2 (두 번째 업데이트)
인스턴스 3: v1
인스턴스 1: v2
인스턴스 2: v2
인스턴스 3: v1 → v2 (마지막 업데이트)
장점: 제로 다운타임, 점진적 출시 단점: 두 버전이 동시에 실행되므로 하위 호환성 유지가 필수적임 사용 시점: 표준 배포, 하위 호환성이 보장되는 변경
동일한 두 개의 환경을 운영합니다. 트래픽을 원자적으로(atomically) 전환합니다.
Blue (v1) ← 트래픽 처리
Green (v2) 대기 중, 새 버전 실행 중
# 검증 완료 후:
Blue (v1) 대기 중 (백업 상태가 됨)
Green (v2) ← 트래픽 처리
장점: 즉각적인 롤백(블루로 전환), 깔끔한 전환 단점: 배포 중에 2배의 인프라 자원이 필요함 사용 시점: 중요한 서비스, 장애에 대한 무관용 원칙 적용 시
새 버전의 트래픽 중 일부(작은 비율)만 먼저 라우팅합니다.
v1: 트래픽의 95%
v2: 트래픽의 5% (카나리)
# 지표가 양호한 경우:
v1: 트래픽의 50%
v2: 트래픽의 50%
# 최종 단계:
v2: 트래픽의 100%
장점: 전체 출시 전 실제 트래픽으로 문제 감지 가능 단점: 트래픽 분할 인프라 및 모니터링이 필요함 사용 시점: 고트래픽 서비스, 위험한 변경, 피처 플래그 사용 시
# 1단계: 의존성 설치
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --production=false
# 2단계: 빌드
FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
RUN npm prune --production
# 3단계: 프로덕션 이미지
FROM node:22-alpine AS runner
WORKDIR /app
RUN addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001
USER appuser
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/package.json ./
ENV NODE_ENV=production
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /server ./cmd/server
FROM alpine:3.19 AS runner
RUN apk --no-cache add ca-certificates
RUN adduser -D -u 1001 appuser
USER appuser
COPY --from=builder /server /server
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost:8080/health || exit 1
CMD ["/server"]
FROM python:3.12-slim AS builder
WORKDIR /app
RUN pip install --no-cache-dir uv
COPY requirements.txt .
RUN uv pip install --system --no-cache -r requirements.txt
FROM python:3.12-slim AS runner
WORKDIR /app
RUN useradd -r -u 1001 appuser
USER appuser
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY . .
ENV PYTHONUNBUFFERED=1
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health/')" || exit 1
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "4"]
# 권장 사항 (GOOD)
- 특정 버전 태그 사용 (node:latest 대신 node:22-alpine)
- 이미지 크기 최소화를 위한 멀티 스테이지 빌드 활용
- root가 아닌 사용자로 실행
- 의존성 파일을 먼저 복사 (레이어 캐싱 활용)
- .dockerignore를 사용하여 node_modules, .git, tests 등 제외
- HEALTHCHECK 명령어 추가
- docker-compose 또는 k8s에서 리소스 제한 설정
# 피해야 할 사항 (BAD)
- root 사용자로 실행
- :latest 태그 사용
- 하나의 COPY 레이어에서 전체 저장소 복사
- 프로덕션 이미지에 개발 의존성 설치
- 이미지에 시크릿 저장 (환경 변수나 시크릿 매니저 사용)
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm test -- --coverage
- uses: actions/upload-artifact@v4
if: always()
with:
name: coverage
path: coverage/
build:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: production
steps:
- name: Deploy to production
run: |
# 플랫폼별 배포 명령
# Railway: railway up
# Vercel: vercel --prod
# K8s: kubectl set image deployment/app app=ghcr.io/${{ github.repository }}:${{ github.sha }}
echo "Deploying ${{ github.sha }}"
PR 오픈 시:
lint → typecheck → 유닛 테스트 → 통합 테스트 → 프리뷰 배포
main 브랜치 병합 시:
lint → typecheck → 유닛 테스트 → 통합 테스트 → 이미지 빌드 → 스테이징 배포 → 스모크 테스트 → 프로덕션 배포
// 단순 헬스 체크
app.get("/health", (req, res) => {
res.status(200).json({ status: "ok" });
});
// 상세 헬스 체크 (내부 모니터링용)
app.get("/health/detailed", async (req, res) => {
const checks = {
database: await checkDatabase(),
redis: await checkRedis(),
externalApi: await checkExternalApi(),
};
const allHealthy = Object.values(checks).every(c => c.status === "ok");
res.status(allHealthy ? 200 : 503).json({
status: allHealthy ? "ok" : "degraded",
timestamp: new Date().toISOString(),
version: process.env.APP_VERSION || "unknown",
uptime: process.uptime(),
checks,
});
});
async function checkDatabase(): Promise<HealthCheck> {
try {
await db.query("SELECT 1");
return { status: "ok", latency_ms: 2 };
} catch (err) {
return { status: "error", message: "Database unreachable" };
}
}
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 30
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 2
startupProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 0
periodSeconds: 5
failureThreshold: 30 # 30 * 5s = 150s 최대 시작 시간
# 모든 설정은 환경 변수를 통함 — 절대 코드에 포함하지 않음
DATABASE_URL=postgres://user:pass@host:5432/db
REDIS_URL=redis://host:6379/0
API_KEY=${API_KEY} # 시크릿 매니저에 의해 주입됨
LOG_LEVEL=info
PORT=3000
# 환경별 동작
NODE_ENV=production # 또는 staging, development
APP_ENV=production # 명시적인 앱 환경
import { z } from "zod";
const envSchema = z.object({
NODE_ENV: z.enum(["development", "staging", "production"]),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
});
// 시작 시 검증 — 설정이 잘못된 경우 즉시 종료(Fail fast)
export const env = envSchema.parse(process.env);
# Docker/Kubernetes: 이전 이미지로 가리킴
kubectl rollout undo deployment/app
# Vercel: 이전 배포본으로 승격
vercel rollback
# Railway: 이전 커밋으로 재배포
railway up --commit <previous-sha>
# 데이터베이스: 마이그레이션 롤백 (가역적인 경우)
npx prisma migrate resolve --rolled-back <migration-name>
프로덕션 배포 전 최종 확인 사항: