배포: 무중단 롤아웃과 빠른 롤백으로 변경 위험을 통제하는 전략
배포(Deployment)는 검증된 코드를 운영 환경에 반영해 사용자가 쓸 수 있게 만드는 과정이다. 롤링, 블루/그린, 카나리 같은 전략은 서비스 중단과 장애 영향 범위를 줄이는 방식이 서로 다르다. 헬스 체크와 롤백 기준을 자동화하면 배포를 반복 가능하고 안전한 작업으로 만들 수 있다.
By 송민성
1. 개념
배포(Deployment)는 빌드된 산출물(artifact)을 운영 환경(production)에 설치하고 트래픽을 받도록 전환하는 과정이다. 릴리스(Release)와는 구분된다.
- 배포: 새 버전의 코드를 서버에 올리는 기술적 행위
- 릴리스: 새 기능을 사용자에게 노출하는 비즈니스적 결정
두 가지를 분리하면(예: 기능 플래그, Feature Flag) 코드는 먼저 배포하고 기능은 나중에 켤 수 있다.
2. 왜 사용하는가
수동 배포는 사람마다 절차가 달라 실수가 생기기 쉽고, 문제가 생겼을 때 되돌리기도 어렵다. 체계적인 배포 전략은 다음을 목표로 한다.
- 무중단(Zero Downtime): 배포 중에도 요청을 계속 처리한다.
- 장애 영향 범위(Blast Radius) 축소: 문제가 있는 버전에 노출되는 사용자를 제한한다.
- 빠른 복구: 이전 버전으로 되돌리는 시간을 줄인다.
- 재현성: 같은 산출물이 같은 절차로 배포된다.
3. 동작 원리
배포 파이프라인
일반적인 CI/CD(Continuous Integration/Continuous Delivery) 흐름은 다음과 같다.
- 커밋 → 빌드 → 테스트
- 불변(immutable) 산출물 생성 (예: 컨테이너 이미지, 버전 태그 부여)
- 스테이징 환경 검증
- 운영 환경 배포
- 헬스 체크(Health Check)와 모니터링 → 실패 시 롤백(Rollback)
주요 배포 전략
| 전략 | 방식 | 장점 | 단점 |
|---|---|---|---|
| 재생성(Recreate) | 기존 버전을 모두 내리고 새 버전 기동 | 단순함 | 다운타임 발생 |
| 롤링(Rolling) | 인스턴스를 일부씩 순차 교체 | 추가 자원이 적게 듦 | 구버전·신버전이 동시에 존재, 롤백이 느림 |
| 블루/그린(Blue/Green) | 새 환경(그린)을 별도로 띄우고 트래픽을 한 번에 전환 | 즉시 전환·즉시 롤백 | 환경 두 벌의 비용 |
| 카나리(Canary) | 소수 트래픽(예: 5%)만 신버전으로 보내 관찰 후 확대 | 영향 범위가 가장 작음 | 트래픽 분배와 지표 판단이 복잡함 |
롤링 배포의 핵심: 헬스 체크
로드 밸런서(Load Balancer)는 헬스 체크를 통과한 인스턴스에만 트래픽을 보낸다. 종료할 인스턴스에는 새 요청을 막고 처리 중인 요청을 마무리하게 하는 우아한 종료(Graceful Shutdown)가 필요하다.
데이터베이스 변경
롤링이나 카나리 중에는 구버전과 신버전이 같은 DB를 함께 쓴다. 그래서 스키마 변경은 확장 → 마이그레이션 → 축소(Expand/Contract) 순서로 나눠 하위 호환을 유지해야 한다.
4. 코드 예제
4-1. 헬스 체크와 우아한 종료 (TypeScript, Node.js)
import http from "node:http";
let isShuttingDown = false;
let inflight = 0;
const server = http.createServer((req, res) => {
if (req.url === "/healthz") {
// 종료 중이면 503을 반환해 로드 밸런서가 트래픽을 빼도록 한다
res.statusCode = isShuttingDown ? 503 : 200;
res.end(isShuttingDown ? "shutting down" : "ok");
return;
}
inflight++;
res.on("finish", () => inflight--);
// 실제 요청 처리 (예시: 0.5초 소요)
setTimeout(() => res.end("hello\n"), 500);
});
server.listen(3000, () => console.log("listening on :3000"));
function shutdown(signal: string) {
console.log(`${signal} received, draining...`);
isShuttingDown = true;
// 새 연결을 받지 않고, 진행 중인 요청이 끝나면 콜백이 호출된다
server.close(() => {
console.log("closed cleanly");
process.exit(0);
});
// 안전장치: 10초 안에 끝나지 않으면 강제 종료
setTimeout(() => {
console.error(`forced exit, inflight=${inflight}`);
process.exit(1);
}, 10_000).unref();
}
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));4-2. 헬스 체크 기반 배포 스크립트 (Shell)
#!/usr/bin/env bash
set -euo pipefail
NEW_URL="http://localhost:3000/healthz"
MAX_TRIES=10
for i in $(seq 1 "$MAX_TRIES"); do
code=$(curl -s -o /dev/null -w "%{http_code}" "$NEW_URL" || true)
if [ "$code" = "200" ]; then
echo "healthy: 트래픽 전환 가능"
exit 0
fi
echo "try $i/$MAX_TRIES: status=$code, 재시도"
sleep 2
done
echo "unhealthy: 롤백 필요" >&2
exit 14-3. 카나리 트래픽 분배 시뮬레이션 (Python)
import hashlib
def is_canary(user_id: str, percent: int) -> bool:
"""사용자 ID를 해시해 항상 같은 사용자를 같은 버전으로 보낸다."""
digest = hashlib.sha256(user_id.encode()).digest()
bucket = int.from_bytes(digest[:4], "big") % 100
return bucket < percent
users = [f"user-{i}" for i in range(10_000)]
canary_count = sum(is_canary(u, 5) for u in users)
print(f"카나리 대상: {canary_count} / {len(users)}")해시로 버킷을 정하면 같은 사용자가 요청마다 다른 버전을 만나는 현상을 막을 수 있다. 비율을 5에서 25, 50, 100으로 올리면 점진적 확대가 된다.
4-4. 하위 호환 스키마 변경 (SQL, PostgreSQL)
-- 1단계(Expand): 신규 컬럼을 NULL 허용으로 추가한다. 구버전 코드는 영향 없음
ALTER TABLE users ADD COLUMN display_name text;
-- 2단계(Migrate): 기존 데이터를 채운다. 대용량이면 배치로 나눠 실행한다
UPDATE users
SET display_name = name
WHERE display_name IS NULL;
-- 3단계(Contract): 모든 인스턴스가 신버전이 된 뒤 별도 배포에서 제약을 강화한다
ALTER TABLE users ALTER COLUMN display_name SET NOT NULL;5. 시간 복잡도 또는 성능 특성
배포는 알고리즘이 아니므로 점근 복잡도 대신 운영 특성으로 비교한다.
- 재생성: 다운타임이 새 버전 기동 시간만큼 발생한다.
- 롤링: 교체 시간이 인스턴스 수와 배치 크기에 비례한다. 인스턴스가 N개이고 한 번에 k개씩 교체하면 대략 ⌈N/k⌉ 단계가 필요하다.
- 블루/그린: 전환은 라우팅 변경 한 번이라 빠르지만, 전환 동안 두 환경 분량의 자원이 필요하다.
- 카나리: 지표를 충분히 관찰해야 하므로 전체 완료 시간이 가장 길 수 있다. 그 대신 장애 시 피해 사용자가 가장 적다.
6. 실무 사용 사례
- Kubernetes Deployment: 기본 전략이 롤링 업데이트이며
maxSurge,maxUnavailable로 동시에 교체할 범위를 조절한다.readinessProbe가 통과한 파드에만 트래픽이 간다. - 블루/그린: 로드 밸런서의 타깃 그룹이나 DNS 가중치를 바꿔 전환한다. 문제가 생기면 이전 환경으로 되돌린다.
- 카나리: 서비스 메시나 인그레스의 가중치 라우팅으로 일부 트래픽만 신버전에 보내고, 오류율과 지연시간을 기준으로 확대 여부를 정한다.
- 기능 플래그: 코드를 꺼진 상태로 배포한 뒤 사용자군별로 켜서, 배포와 릴리스를 분리한다.
7. 주의할 점
- 하위 호환성: 롤링과 카나리 중에는 두 버전이 공존한다. API와 DB 스키마는 구버전과 신버전이 함께 동작해야 한다.
- 롤백 가능성: 코드는 되돌려도 이미 실행된 마이그레이션이나 변환된 데이터는 되돌아가지 않는다. 파괴적 변경(컬럼 삭제 등)은 별도 배포로 분리한다.
- 불변 산출물: 서버에서 직접 빌드하거나 파일을 수정하면 환경별 차이가 생긴다. 한 번 만든 이미지를 모든 환경에서 그대로 쓴다.
- 우아한 종료 누락: SIGTERM을 처리하지 않으면 진행 중인 요청이 끊긴다.
- 헬스 체크 설계: 프로세스가 떠 있는지만 보는 체크는 부족하다. 필수 의존성 연결 여부를 확인하는 준비 상태(readiness) 체크를 두되, 외부 장애가 전체 인스턴스를 동시에 내리지 않도록 범위를 신중히 정한다.
- 롤백 기준의 자동화: "오류율이 X%를 넘으면 롤백"처럼 기준을 미리 정해 두지 않으면 장애 중 판단이 늦어진다.
8. 핵심 정리
- 배포는 코드를 운영에 올리는 행위이고, 릴리스는 기능을 사용자에게 노출하는 결정이다. 기능 플래그로 둘을 분리할 수 있다.
- 재생성은 단순하지만 다운타임이 있고, 롤링은 자원이 적게 들지만 두 버전이 공존한다. 블루/그린은 즉시 롤백이 가능하지만 비용이 크고, 카나리는 영향 범위가 가장 작다.
- 헬스 체크와 우아한 종료는 무중단 배포의 기본 조건이다.
- DB 스키마는 Expand/Contract로 나눠 하위 호환을 유지한다.
- 불변 산출물, 자동화된 파이프라인, 사전에 정의한 롤백 기준이 배포를 반복 가능하고 안전하게 만든다.