HAProxy + Nginx 혼합 배치: Blue-Green/Canary 설계 비교, 액티브 헬스체크, 무중단 DB 마이그레이션 팁
이전 글에서 Nginx 단독으로 graceful reload 기반 무중단 배포(Blue-Green/Canary)를 만들었습니다. 이번에는 HAProxy(4/7계층 로드밸런서)와 Nginx(리버스 프록시/정적/캐시/헤더)를 혼합해서 더 단단한 배포 파이프라인을 설계해봅니다. 핵심은 HAProxy의 액티브 헬스체크/스틱 테이블과 Nginx의 정적/캐시/보안 헤더/리라이트를 역할 분리로 묶는 것입니다. 마지막으로 배포와 함께 자주 동반되는 DB 스키마 마이그레이션을 무중단으로 처리하는 실전 팁도 정리합니다.
1) 왜 혼합 구조인가: 역할 분리 다이어그램
[Client] --HTTPS--> [Nginx edge: TLS, 정적, 캐시, WAF/헤더]
|
v
[HAProxy: LB, 액티브 헬스, 라우팅/가중치, 스틱테이블]
|
+----------+-----------+
| |
[App vBlue] [App vGreen]
:8081 :8082
- Nginx: TLS 종료, 정적/이미지, 캐시, 압축, 보안 헤더, URL 리라이트.
- HAProxy: 가중치 기반 라우팅(Blue/Green/Canary), 액티브 헬스체크, 레이트 리밋/스틱 테이블.
2) Blue-Green: HAProxy에서 스위칭, Nginx는 변하지 않는다
Nginx는 외곽(에지) 고정, 트래픽 스위칭은 HAProxy에서만 수행합니다. 배포 시 유휴 그룹(Green)에 새 버전을 올린 뒤, 헬스가 OK면 가중치 전환 → 모니터링 → 구버전 종료 순서로 진행합니다.
2-1) HAProxy 기본 설정(HTTP)
# /etc/haproxy/haproxy.cfg (발췌)
global
log /dev/log local0
maxconn 10000
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5s
timeout client 65s
timeout server 65s
default-server inter 2s fall 3 rise 2 # 액티브 헬스 주기/판정
frontend fe_app
bind 127.0.0.1:9000
default_backend be_app
backend be_app
balance leastconn
option httpchk GET /actuator/health
http-check expect status 200
server blue 127.0.0.1:8081 weight 10 check
server green 127.0.0.1:8082 weight 0 check
weight를 전환하는 순간 트래픽이 이동합니다. 실패 서버는 자동 격리됩니다.
2-2) Nginx는 HAProxy만 백엔드로
# /etc/nginx/sites-available/app.example.com (발췌)
server {
listen 443 ssl http2;
server_name app.example.com;
# TLS/헤더/캐시 등은 기존 구성
location / {
proxy_pass http://127.0.0.1:9000; # <-- HAProxy로 집약
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
3) Canary(점진 배포): 비율·쿠키·헤더 전략
HAProxy는 ACL + map/stick-table로 유연한 분기가 가능합니다.
3-1) 비율 기반(가중치)
backend be_app
balance roundrobin
option httpchk GET /actuator/health
http-check expect status 200
server stable 127.0.0.1:8081 weight 95 check
server canary 127.0.0.1:8082 weight 5 check
3-2) 쿠키/헤더 기반(내부 QA만 canary)
frontend fe_app
acl is_canary hdr_sub(cookie) -i CANARY=1
use_backend be_canary if is_canary
default_backend be_stable
backend be_stable
balance leastconn
server s1 127.0.0.1:8081 check
backend be_canary
balance leastconn
server c1 127.0.0.1:8082 check
팁: 비율 0% + 쿠키 기반을 상시 둬서, 장애 시 즉시 0% 롤백이 가능합니다.
4) 액티브 헬스체크: Nginx의 빈 곳을 매운다
- Nginx OSS는 능동 헬스체크가 없어
proxy_next_upstream같은 수동 전환만 지원합니다. - HAProxy는
option httpchk,http-check expect로 상시 상태 확인 후 불량 노드를 빼 줍니다.
# 핸드쉐이크만으로도 헬스 가능(HTTPS 백엔드)
backend be_app_tls
mode tcp
option ssl-hello-chk
server app1 127.0.0.1:8443 check ssl verify none
5) 스틱 테이블로 폭주 IP 제어(로그인/검색 보호)
frontend fe_app
stick-table type ip size 200k expire 10m store http_req_rate(10s)
http-request track-sc0 src
acl abuse sc_http_req_rate(0) gt 50
http-request deny if abuse
과도한 요청을 에지(Nginx)에서 1차 제한하고, 코어(HAProxy)에서 2차 제한하면 더 견고합니다.
6) 배포 스크립트(Blue→Green 전환) 예시
# /usr/local/sbin/hap-switch.sh
#!/usr/bin/env bash
set -euo pipefail
TARGET="${1:-green}" # blue|green
if [ "$TARGET" = "green" ]; then
echo "set server be_app/blue weight 0" | socat stdio tcp4:127.0.0.1:9999
echo "set server be_app/green weight 10" | socat stdio tcp4:127.0.0.1:9999
else
echo "set server be_app/blue weight 10" | socat stdio tcp4:127.0.0.1:9999
echo "set server be_app/green weight 0" | socat stdio tcp4:127.0.0.1:9999
fi
echo "$(date -Is) switched to $TARGET" >> /var/log/hap-switch.log
stats socket :9999를 HAProxy에 열어두면 런타임에 weight 등을 바꿀 수 있습니다.
7) 무중단 DB 마이그레이션: 배포와 함께 가는 실전 루틴
애플리케이션 무중단이어도 스키마 변경이 다운타임을 만들면 소용이 없습니다. 다음 원칙을 지키세요.
- 호환성 우선: 새 코드와 구 스키마가 공존 가능해야 합니다(Expand-Migrate-Contract).
- 작게 쪼개기: DDL을 여러 번으로 나눠 락/재빌드 시간을 줄입니다.
- 읽기/쓰기 분리: 필요한 경우 일시적으로 쓰기 경로를 큐잉/버퍼링합니다.
7-1) Expand 단계(추가/널허용/디폴트)
-- 1) 새 컬럼 추가(널허용)
ALTER TABLE orders ADD COLUMN note VARCHAR(255);
-- 2) 코드 배포: 새 컬럼 쓰기 시작(기존 컬럼도 유지)
-- 3) 백필 작업(배치/잡)
UPDATE orders SET note = concat('ord#', id) WHERE note IS NULL;
7-2) Migrate 단계(인덱스/타입 변경은 안전하게)
- PostgreSQL:
CONCURRENTLY로 인덱스 생성 → 스위치. - MySQL: 온라인 DDL(엔진/버전 제약), 필요 시 gh-ost/pt-osc 사용.
-- PostgreSQL 예시
CREATE INDEX CONCURRENTLY idx_orders_note ON orders(note);
DROP INDEX CONCURRENTLY idx_orders_old;
7-3) Contract 단계(구 컬럼/제약 제거)
-- 코드가 새 컬럼만 사용함을 확인 후
ALTER TABLE orders DROP COLUMN old_note;
체크리스트:
- DDL 전: 슬로우 쿼리/락 위험 경로 파악, 트래픽 저점 시간 선택
- 배포 전: 앱이 양쪽 스키마를 모두 처리 가능(기존 필드 null 허용)
- 배포 중: HAProxy Canary 5% → 20% → 100% 확대, 에러/지연 추적
- 배포 후: Contract DDL은 별도 윈도우에서 진행
8) 관찰성: 어느 계층에서 문제가 났는지 즉시 알기
- HAProxy:
http_err_rate,hrsp_5xx, 백엔드별status/헬스. - Nginx:
$upstream_status,$upstream_response_time포함 로그 포맷. - 앱/DB: p95/에러율/DB 락 대기/슬로우 쿼리.
9) 자주 겪는 함정과 해결
- 헬스 OK인데 5xx 증가 → 헬스 엔드포인트가 실제 의존성(외부 API/DB)을 검증하는지 재점검.
- 세션 유실 → 외부 세션 저장소(예: Redis)로 이동, 또는 cookie-based 스티키 세션.
- DB 마이그 중 락 장기화 → 배치 크기 축소, 오프피크 실행, 온라인 DDL 툴로 우회.
- 롤백 늦음 → HAProxy 런타임 스위치(stats socket) 스크립트를 미리 준비.
10) 마무리
Nginx는 에지, HAProxy는 코어 로드밸런싱에 집중시키면, 배포·복구·확장성 모두 향상됩니다. Blue-Green/Canary 스위치를 HAProxy로 일원화하고, 스키마 변경은 Expand→Migrate→Contract로 작게 나눠서 진행하세요. 이 구조가 잡히면 “배포는 평범한 일”이 됩니다.