Nginx·HAProxy로 리버스 프록시 서버 구축하기: 로드밸런싱·캐싱·헬스체크 실전 설정 전체 공개

1부에서 리버스 프록시가 어떤 원리로 클라이언트와 오리진 사이에 끼어드는지, TCP 커넥션 재사용과 헤더 재작성이 왜 중요한지를 살펴봤다면(리버스 프록시 원리 가이드(1부)), 2부인 이 글에서는 실제 트래픽을 받는 서버를 손으로 세웁니다. 이론을 알고 있어도 설정 파일 한 줄 차이로 서비스가 무너지는 경험은 누구나 합니다. 우리 팀도 그랬습니다. WebSocket 프록시에서 proxy_http_version 1.1을 빠뜨려 모든 실시간 연결이 101 Switching Protocols 단계에서 끊겼고, HAProxy 헬스체크 fall 임계값을 너무 낮게 잡아 순간적인 네트워크 지터 한 번에 백엔드 풀이 통째로 비어버리는 일도 겪었습니다. 이 글은 그 시행착오 전부를 담은 Nginx·HAProxy 실전 운영 설정서입니다. 1부를 읽지 않아도 독립적으로 따라올 수 있지만, 원리를 함께 이해하면 각 설정의 "왜"가 훨씬 선명해집니다.
도구 선택 기준 — Nginx, HAProxy, Envoy, Caddy를 언제 쓰는가
리버스 프록시 도구를 처음 고를 때 가장 많이 듣는 말은 "그냥 Nginx 쓰면 되잖아요"입니다. 틀린 말은 아닙니다. 하지만 팀의 규모, 트래픽 패턴, 인프라 환경에 따라 최선의 선택이 달라집니다. 2026년 현재 주요 4개 도구의 포지셔닝은 다음과 같습니다.
Nginx 1.29.x (mainline) 는 정적 파일 서빙과 리버스 프록시를 동시에 처리할 수 있는 단일 바이너리입니다. 설정 파일이 선언형으로 직관적이고, Let's Encrypt 자동화, gzip, Brotli, HTTP/2까지 플러그인 없이 지원합니다. 커뮤니티 규모가 가장 크기 때문에 트러블슈팅 자료도 풍부합니다. 처음 리버스 프록시를 세운다면 Nginx가 최우선 선택입니다.
HAProxy 3.2 (최신 LTS) 는 정적 파일 서빙 기능이 전혀 없는 순수 프록시·로드밸런서입니다. 대신 L4·L7 트래픽을 처리하는 데에 Nginx보다 빠르고, ACL 기반 라우팅과 헬스체크 설정이 훨씬 세밀합니다. 사내 서비스 메시의 엣지, 고트래픽 API 게이트웨이처럼 "프록시만 제대로 해야 하는" 자리에 어울립니다.
Envoy (최신 안정 버전) 는 클라우드 네이티브 환경의 표준입니다. 정적 설정 파일이 아닌 xDS API를 통해 런타임에 라우팅 규칙을 변경할 수 있습니다. Istio·Contour·Linkerd의 데이터플레인이 모두 Envoy 위에서 동작합니다. 단독 서버에 띄우는 용도로는 설정 복잡도가 지나치게 높습니다.
Caddy 는 Let's Encrypt 인증서 발급과 갱신을 완전 자동화합니다. Caddyfile 문법이 Nginx 설정의 절반 수준으로 짧습니다. 사이드 프로젝트, 소규모 팀, 인증서 관리를 직접 하기 싫은 상황에 적합합니다.
| 항목 | Nginx 1.29 | HAProxy 3.2 | Envoy (최신) | Caddy |
|---|---|---|---|---|
| 정적 파일 서빙 | 지원 | 미지원 | 미지원 | 지원 |
| L4/L7 성능(RPS) | 높음 | 매우 높음 | 높음 | 중간 |
| 설정 복잡도 | 낮음 | 중간 | 높음 | 매우 낮음 |
| 동적 설정(런타임 변경) | 제한적(SIGHUP) | 제한적(SIGHUP) | 완전 지원(xDS) | 제한적 |
| K8s 생태계 통합 | ingress-nginx | 드뭄 | 사실상 표준 | 드뭄 |
| 자동 HTTPS | certbot 연동 | 미지원 | 미지원 | 기본 내장 |
| 커뮤니티 규모 | 매우 큼 | 큼 | 중간 | 중간 |
이 표를 보고 "모든 항목에서 1위인 도구가 없다"는 사실을 느끼셨다면 정답입니다. 우리 팀은 엣지 서버는 HAProxy, 앱 서버 앞단은 Nginx, Kubernetes 클러스터 내부는 Envoy로 계층을 나눠 운영합니다.
버전 표기 참고: HAProxy 2.9는 2025년 Q1부터 유지보수가 종료됐습니다. 이 글의 설정 예시는 HAProxy 3.x에서도 동일하게 적용됩니다.
Nginx 기본 리버스 프록시 설정 — proxy_pass 하나로 시작하는 최소 구성
Nginx 리버스 프록시의 출발점은 proxy_pass 디렉티브 하나입니다. 그런데 여기서 가장 많이 발생하는 실수가 trailing slash입니다. location /api/에서 proxy_pass http://backend/처럼 둘 다 슬래시로 끝내면 /api/users가 백엔드에 /users로 전달됩니다. 반면 proxy_pass http://backend처럼 슬래시를 생략하면 /api/users가 그대로 /api/users로 전달됩니다. 이 차이를 모르면 404 지옥에 빠집니다.
Nginx ngx_http_proxy_module 공식 문서에 따르면 proxy_http_version의 기본값은 1.29.7 이전 버전까지는 1.0이었고, 1.29.7부터는 1.1로 변경됐습니다. HTTP/1.0은 커넥션을 요청마다 끊기 때문에, 구버전 Nginx를 쓰거나 기본값을 믿지 않겠다면 1.1을 명시하고 Connection "" 헤더를 비워야 합니다. 이 설정이 없으면 백엔드 서버에 SYN-ACK가 요청마다 발생하고, 고트래픽 상황에서 TIME_WAIT 소켓이 쌓이기 시작합니다. 현재 1.29.x mainline을 쓴다면 기본값이 이미 1.1이지만, 명시적으로 적어두는 편이 설정 파일을 읽는 사람에게 의도를 분명히 전달합니다.
다음은 우리 팀이 프로덕션에서 실제로 사용하는 최소 구성입니다.
upstream backend_upstream {
keepalive 32;
server 10.0.0.1:8080;
server 10.0.0.2:8080;
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location /api/ {
proxy_pass http://backend_upstream/;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
upstream 블록의 keepalive 32는 Nginx 워커 프로세스 하나가 백엔드와 유지할 수 있는 유휴 keepalive 커넥션 수입니다. 워커가 8개라면 백엔드와 최대 256개의 커넥션을 상시 재사용합니다. X-Forwarded-For에 $proxy_add_x_forwarded_for를 쓰는 이유는 기존 헤더가 있을 경우 누적 형태로 이어붙이기 위해서입니다. 단순히 $remote_addr를 쓰면 앞단에 CDN이나 다른 프록시가 있을 때 원본 IP 정보가 날아갑니다. 이 설정은 Nginx Admin Guide — Reverse Proxy 문서의 권장 패턴을 따른 것입니다.
Nginx 로드밸런싱 전략 4가지 — Round Robin부터 Least Connections까지 실전 비교
Nginx의 upstream 블록에서 별도 지시어를 넣지 않으면 Round Robin이 기본입니다. 서버 목록을 순서대로 순환합니다. 처리 시간이 균일한 API 서버라면 가장 단순하고 효과적입니다.
# Round Robin (기본값, 별도 지시어 불필요)
upstream api_rr {
keepalive 32;
server 10.0.0.1:8080;
server 10.0.0.2:8080;
server 10.0.0.3:8080;
}
Least Connections (least_conn)은 현재 활성 커넥션이 가장 적은 서버에 요청을 보냅니다. 요청마다 처리 시간이 크게 달라지는 경우, 예를 들어 빠른 조회와 느린 집계 쿼리가 뒤섞이는 API 서버에서 Round Robin보다 부하가 훨씬 균등하게 분산됩니다.
upstream api_lc {
least_conn;
keepalive 32;
server 10.0.0.1:8080;
server 10.0.0.2:8080;
}
IP Hash (ip_hash)는 클라이언트 IP를 해시해 항상 같은 백엔드 서버로 연결합니다. Redis 같은 외부 저장소 없이 서버 사이드 세션을 유지해야 하는 레거시 앱에서 임시방편으로 쓸 수 있습니다. 다만 기업망처럼 특정 IP 대역이 몰리는 트래픽 구조에서는 특정 서버로 부하가 집중되는 문제가 생깁니다.
upstream api_iphash {
ip_hash;
server 10.0.0.1:8080;
server 10.0.0.2:8080;
}
Consistent Hash (hash $request_uri consistent)는 URL 기반으로 동일 요청을 동일 서버로 보내 업스트림 캐시 효율을 극대화합니다. CDN 팝(PoP) 서버 앞에서, 또는 오리진 수준에서 요청 로컬리티를 높이고 싶을 때 유용합니다. consistent 키워드는 Ketama 알고리즘을 적용해 서버가 추가되거나 빠질 때 캐시 미스를 최소화합니다.
upstream api_hash {
hash $request_uri consistent;
server 10.0.0.1:8080;
server 10.0.0.2:8080;
server 10.0.0.3:8080;
}
아래 표는 우리 팀 내부 환경(wrk -t8 -c200 -d30s)에서 동일 API 서버 3대를 대상으로 각 전략을 비교한 측정 결과입니다. 응답 시간 분포가 짧고 균일한 API(평균 5ms 응답)는 전략 차이가 거의 없고, 응답 시간 편차가 큰 집계 API(p50 40ms / p99 800ms)에서 least_conn이 유의미한 차이를 만들어냈습니다.
| 전략 | 균일 부하 RPS | 불균일 부하 RPS | p99 latency(불균일) | 세션 어피니티 |
|---|---|---|---|---|
| round_robin | 18,400 | 11,200 | 920ms | 없음 |
| least_conn | 18,200 | 14,600 | 430ms | 없음 |
| ip_hash | 17,900 | 10,800 | 880ms | IP 기반 |
| hash $request_uri | 18,100 | 12,400 | 590ms | URI 기반 |
불균일 부하에서 least_conn이 RPS 30% 향상, p99 53% 감소를 보인 것이 핵심입니다. 처리 시간 분산이 크다면 기본 Round Robin을 그대로 쓰지 마세요.
HAProxy 설정 완전 분해 — frontend·backend·ACL·헬스체크 한 파일로 이해하기
HAProxy 설정은 크게 4개 섹션으로 나뉩니다. global은 프로세스 수준 설정, defaults는 모든 frontend·backend에 공통 적용되는 기본값, frontend는 클라이언트가 접속하는 진입점, backend는 실제 서버 풀 정의입니다. HAProxy — four essential sections 문서가 이 구조를 가장 잘 설명합니다.
ACL(Access Control List)은 HAProxy의 가장 강력한 기능 중 하나입니다. 경로 prefix, 헤더 값, 메서드 등 거의 모든 조건으로 트래픽을 다른 backend로 분기할 수 있습니다. 아래 설정은 /api 경로는 API 서버로, WebSocket 업그레이드 요청은 별도 WS 서버로, 나머지는 웹 서버로 보내는 실전 예시입니다.
global
log /dev/log local0
maxconn 50000
defaults
mode http
timeout connect 5s
timeout client 30s
timeout server 30s
option httplog
option dontlognull
frontend http_front
bind *:80
bind *:443 ssl crt /etc/haproxy/certs/example.pem alpn h2,http/1.1
redirect scheme https if !{ ssl_fc }
acl is_api path_beg /api
acl is_ws hdr(Upgrade) -i websocket
use_backend api_servers if is_api
use_backend ws_servers if is_ws
default_backend web_servers
backend api_servers
balance leastconn
option httpchk GET /health
server api1 10.0.0.1:8080 check fall 3 rise 2
server api2 10.0.0.2:8080 check fall 3 rise 2
backend web_servers
balance roundrobin
server web1 10.0.1.1:3000 check
server web2 10.0.1.2:3000 check
option httpchk GET /health는 HAProxy가 백엔드 서버의 /health 엔드포인트를 주기적으로 GET 요청해 생존 여부를 확인하도록 합니다. fall 3은 헬스체크가 3번 연속 실패해야 서버를 다운으로 판정한다는 뜻이고, rise 2는 2번 연속 성공해야 다시 풀에 복귀시킨다는 뜻입니다. 이 임계값 설계가 생각보다 중요합니다. 우리는 초창기에 fall 1을 썼다가 백엔드 앱이 가비지 컬렉션으로 잠깐 멈추는 순간마다 HAProxy가 해당 서버를 풀에서 빼버리는 상황을 경험했습니다. fall 3으로 올리자 순간적인 GC 일시 정지에 과잉 반응하는 현상이 사라졌습니다.
스티키 세션이 필요한 경우 두 가지 방식 중 하나를 고릅니다. balance source는 IP 해시 방식으로 단순하지만 NAT 뒤의 클라이언트가 한 서버에 몰릴 수 있습니다. cookie insert는 HAProxy가 Set-Cookie 헤더를 주입해 쿠키 값으로 서버 어피니티를 관리합니다. 둘 다 서버 장애 시 세션이 날아간다는 한계는 동일합니다. 세션은 항상 외부 저장소에 두는 것이 정답이고, 스티키 세션은 그 리팩토링 전까지의 임시 처치로 여겨야 합니다. HAProxy Configuration Guide는 이 트레이드오프를 명확히 설명합니다.
응답 캐싱 설정 — Nginx proxy_cache로 오리진 부하를 70% 줄이는 방법
캐싱은 가장 확실한 오리진 부하 절감 수단입니다. 우리는 정적 API 응답이 많은 서비스에서 Nginx proxy_cache를 붙인 뒤 오리진 RPS가 1.2k에서 320으로 떨어지는 걸 직접 봤습니다. 약 73% 부하 절감이었습니다. 그때까지는 캐싱을 애플리케이션 레이어에서만 다뤘는데, 프록시 레이어에서 먼저 잘라내는 것이 훨씬 저렴하다는 사실을 그제서야 깨달았습니다.
# nginx.conf 또는 /etc/nginx/conf.d/cache.conf
proxy_cache_path /var/cache/nginx
levels=1:2
keys_zone=api_cache:64m
max_size=2g
inactive=10m
use_temp_path=off;
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location /api/v1/products {
proxy_pass http://backend_upstream;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_cache api_cache;
proxy_cache_key "$scheme$host$request_uri";
proxy_cache_valid 200 10m;
proxy_cache_valid 404 1m;
proxy_cache_bypass $http_cache_control;
proxy_no_cache $http_pragma;
add_header X-Cache-Status $upstream_cache_status;
}
}
proxy_cache_path의 levels=1:2는 캐시 파일을 2단계 디렉터리 구조로 저장한다는 뜻입니다. 파일이 하나의 디렉터리에 몰리면 inode 탐색이 느려지므로 이 설정은 실제 운영에서 필수입니다. keys_zone=api_cache:64m의 64m은 캐시 키 인덱스를 저장하는 공유 메모리 크기입니다. 1MB당 약 8,000개 키를 저장할 수 있으니, 64MB면 50만 개가 넘는 고유 URL을 인메모리 색인으로 유지할 수 있습니다.
proxy_cache_bypass $http_cache_control은 요청에 Cache-Control: no-cache 헤더가 있으면 캐시를 건너뛰고 오리진으로 직접 요청을 보냅니다. 개발자 도구에서 강제 새로고침을 누르면 이 경로로 빠집니다. add_header X-Cache-Status $upstream_cache_status는 응답 헤더에 HIT, MISS, BYPASS, EXPIRED 값을 노출해 캐시 동작을 실시간으로 확인할 수 있게 합니다. 이 헤더가 없으면 캐시가 실제로 동작하는지 검증할 방법이 사라지니 반드시 붙여두세요.
proxy_cache_valid 200 10m는 200 응답만 10분간 캐싱하고, 나머지 상태 코드는 캐시하지 않습니다. 404도 1분 캐싱하는 이유는 존재하지 않는 경로를 반복 탐색하는 트래픽이 오리진까지 도달하지 못하게 막기 위해서입니다.
WebSocket 프록시 — Upgrade 헤더를 살아남게 하는 두 줄 차이
WebSocket 핸드셰이크는 HTTP/1.1 기반의 프로토콜 업그레이드로 시작합니다. 클라이언트가 Upgrade: websocket, Connection: Upgrade 헤더를 보내면 서버가 101 Switching Protocols로 응답하고, 이후 TCP 커넥션을 양방향 스트림으로 유지합니다. 리버스 프록시를 거칠 때 이 헤더가 살아남지 못하면 101이 아닌 400 또는 502가 돌아옵니다.
Nginx는 기본적으로 hop-by-hop 헤더인 Upgrade와 Connection을 백엔드로 전달하지 않습니다. 다음 두 줄이 없으면 WebSocket이 절대 동작하지 않습니다.
location /ws/ {
proxy_pass http://ws_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
proxy_set_header Upgrade $http_upgrade는 클라이언트가 보낸 Upgrade 값을 그대로 백엔드에 전달합니다. proxy_set_header Connection "upgrade"는 Connection 헤더를 리터럴 문자열 "upgrade"로 고정합니다. 이 두 번째 줄에서 주의할 점은 $http_connection을 그대로 쓰면 안 된다는 것입니다. 일반 HTTP 요청에서 Connection 헤더 값은 keep-alive인 경우가 많고, 그 값이 그대로 백엔드로 전달되면 WebSocket 업그레이드가 실패합니다. 리터럴 "upgrade"로 고정해야 합니다.
proxy_read_timeout 3600s와 proxy_send_timeout 3600s를 1시간으로 늘리는 이유는 WebSocket 커넥션은 오래 유지되는 것이 목적이기 때문입니다. 기본값(60초)을 그대로 두면 유휴 상태의 WebSocket 커넥션이 1분마다 끊깁니다.
HAProxy에서는 추가로 timeout tunnel 1h를 backend 블록에 넣어야 합니다. defaults의 timeout server와 별개로, 프로토콜이 전환된 터널 커넥션에는 별도 타임아웃이 적용됩니다. 이 설정을 빠뜨리면 HAProxy의 기본 타임아웃(대부분 수십 초)에 의해 WS 커넥션이 조용히 끊립니다.
backend ws_servers
timeout tunnel 1h
balance leastconn
server ws1 10.0.2.1:8080 check
server ws2 10.0.2.2:8080 check
우리가 겪은 가장 흔한 실수는 앞서 언급한 proxy_http_version 1.1 누락이었습니다. HTTP/1.0으로 WebSocket 핸드셰이크를 시도하면 서버는 101 Switching Protocols를 보내려다 바로 커넥션을 끊습니다. 로그에는 upstream prematurely closed connection while reading response header from upstream이 찍힙니다. 이 메시지가 보이면 가장 먼저 proxy_http_version 1.1이 있는지 확인하세요.
모니터링과 장애 대응 — 502·504·503이 보내는 신호 해석법
리버스 프록시를 운영하면서 마주치는 4xx·5xx 오류 코드는 각각 다른 장애를 가리킵니다. 오류 코드만 보고 무작정 재시작하는 것은 최악의 대응입니다. 배포 파이프라인 자체에서 장애 신호를 자동으로 감지하고 알림을 보내는 방법은 GitHub Actions CI/CD 파이프라인 구축 가이드에서 다루고 있습니다.
502 Bad Gateway 는 Nginx가 백엔드에 요청을 전달했지만 유효한 응답을 받지 못한 경우입니다. 백엔드 프로세스가 다운됐거나 재시작 중인 경우, 또는 백엔드가 연결을 바로 끊어버리는 경우입니다. Nginx 에러 로그에는 upstream prematurely closed connection이 찍힙니다. 백엔드 프로세스 상태와 포트 리슨 여부를 먼저 확인하세요.
504 Gateway Timeout 은 백엔드가 응답을 너무 늦게 보낸 경우입니다. proxy_read_timeout 값을 초과하면 발생합니다. 기본값은 60초입니다. 이 오류가 빈번하다면 두 가지를 확인해야 합니다. 하나는 백엔드 처리 시간이 진짜 느린 것인지(DB 쿼리 슬로우로그 확인), 다른 하나는 proxy_read_timeout 값이 지나치게 짧게 설정돼 있는 것인지입니다.
503 Service Unavailable 은 모든 백엔드가 헬스체크를 통과하지 못해 upstream 풀이 비어있는 경우입니다. HAProxy에서 backend has no server available 메시지가 함께 기록됩니다. 이 경우 개별 백엔드 서버의 상태가 아니라 헬스체크 설정 자체를 의심해야 합니다. /health 엔드포인트가 200을 반환하는지 curl로 직접 확인하세요.
WebSocket 즉시 끊김 은 앞 섹션에서 다룬 Upgrade 헤더 미전달이 원인인 경우가 대부분입니다. 브라우저 개발자 도구의 Network 탭에서 WS 커넥션 상태가 101이 아닌 200으로 뜨거나 즉시 CLOSED가 되면 이 문제입니다.
X-Forwarded-For IP 오염 은 프록시 체인이 여러 겹일 때 나타납니다. Nginx가 이미 프록시된 요청을 다시 프록시하면서 $remote_addr가 앞단 프록시의 IP로 덮여버리는 상황입니다. real_ip_module의 set_real_ip_from으로 신뢰할 수 있는 프록시 IP 대역을 지정하고, real_ip_header X-Forwarded-For로 원본 IP를 복원해야 합니다.
Nginx의 access.log 포맷에 $upstream_response_time과 $upstream_addr를 추가하면 어느 백엔드가 느린지 즉시 파악할 수 있습니다.
log_format proxy_log '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'upstream=$upstream_addr '
'upstream_time=$upstream_response_time '
'request_time=$request_time '
'cache=$upstream_cache_status';
access_log /var/log/nginx/access.log proxy_log;
Nginx의 stub_status 모듈은 간단한 내부 엔드포인트로 active connections, accepts, handled, requests, reading, writing, waiting 지표를 노출합니다. waiting이 비정상적으로 높다면 keepalive 큐가 쌓이는 것이고, reading이 높다면 클라이언트 요청 수신이 느린 것입니다. HAProxy는 /haproxy-status 페이지를 통해 각 서버의 세션 수, 상태, 헬스체크 히스토리를 HTML 테이블로 보여줍니다.
Envoy xDS 동적 설정 맛보기 — Kubernetes 환경에서 리버스 프록시가 달라지는 이유
Nginx와 HAProxy는 설정을 바꾸면 SIGHUP 시그널로 재로드합니다. 마스터 프로세스가 새 설정을 읽고 워커를 교체하는 방식이라 무중단이지만, 설정 변경이 파일 수정 → reload 사이클에 묶입니다. 수십 개의 마이크로서비스 파드가 수시로 생성되고 사라지는 Kubernetes 환경에서 이 방식은 금세 한계를 드러냅니다.
Envoy는 이 문제를 xDS(Discovery Service) 로 해결합니다. xDS는 Envoy가 컨트롤 플레인(예: Istiod)에 gRPC 스트림을 열고, 라우팅 규칙 변경을 실시간 푸시로 받는 프로토콜입니다. 서버 재시작이나 reload 없이 라우팅 테이블이 갱신됩니다.
xDS는 4가지 Discovery API로 구성됩니다.
- LDS (Listener Discovery Service): 리스너(포트, TLS 설정) 정의를 동적으로 수신합니다.
- RDS (Route Discovery Service): HTTP 라우팅 규칙(path prefix, header match → cluster 매핑)을 수신합니다.
- CDS (Cluster Discovery Service): 업스트림 클러스터(백엔드 서버 그룹)의 정의를 수신합니다.
- EDS (Endpoint Discovery Service): 각 클러스터에 속한 실제 엔드포인트(IP:포트)를 수신합니다.
Kubernetes에서 Envoy를 사용하는 방식은 크게 두 가지입니다. 첫 번째는 Front Proxy 방식으로, 클러스터 진입점에 Envoy 인스턴스를 배치하고 모든 인그레스 트래픽을 처리합니다(Envoy front_proxy deployment). 두 번째는 Sidecar 방식으로, 각 파드 옆에 Envoy 컨테이너를 붙여 파드 간 통신을 모두 Envoy가 중계합니다. 이것이 Istio 서비스 메시의 기반 원리입니다.
Kubernetes Ingress 컨트롤러 선택에서도 이 관점이 중요합니다. ingress-nginx는 Nginx 기반으로 설정이 익숙하고 레퍼런스가 많지만, 라우팅 규칙이 ConfigMap 수정 후 reload 사이클에 묶입니다. Contour(Envoy 기반)나 Istio IngressGateway는 xDS로 즉시 갱신됩니다. 파드 수가 수백 개를 넘어가거나 카나리 배포, A/B 테스트를 세밀하게 제어해야 한다면 Envoy 기반 컨트롤러로 전환을 고려할 시점입니다.
Envoy를 직접 운영하지 않더라도 "왜 K8s에서는 Nginx를 그대로 쓰지 않는가"에 대한 답을 알고 있으면 인프라 아키텍처 논의에서 훨씬 명확한 판단을 내릴 수 있습니다. 1부에서 다룬 커넥션 모델과 헤더 흐름을 Envoy 맥락에서 다시 떠올리면 동적 설정이 왜 필요한지가 자연스럽게 이어집니다(리버스 프록시 원리 가이드(1부)).
실무 체크리스트 — 프로덕션 투입 전 반드시 확인할 5가지
2부에서 다룬 내용을 배포 직전 점검 목록으로 압축했습니다. 각 항목이 왜 필요한지는 위 섹션에서 이미 설명했지만, 배포 직전 한 번 더 눈으로 훑는 것이 가장 확실한 사고 예방법입니다.
-
proxy_http_version 1.1+Connection ""쌍으로 명시했는가. Nginx 1.29.7 이상이면 기본값이 1.1이지만, 설정 파일에 명시적으로 남겨두는 편이 팀 내 의도 전달과 버전 호환성 모두에 안전합니다. - WebSocket location 블록에
Upgrade $http_upgrade와 리터럴Connection "upgrade"가 모두 있는가.$http_connection을 그대로 쓰면 일반 HTTP 요청의keep-alive값이 그대로 전달되어 핸드셰이크가 실패합니다. - HAProxy 헬스체크
fall임계값이 3 이상인가.fall 1이나fall 2는 GC 일시 정지, 네트워크 지터 한 번에 서버를 풀에서 빼버립니다. 순간 지연과 실제 장애를 구분하려면fall 3 rise 2이상을 권장합니다. -
proxy_cache_path설정 시levels=1:2와use_temp_path=off가 포함돼 있는가.levels를 생략하면 캐시 파일이 단일 디렉터리에 몰려 inode 탐색이 느려지고,use_temp_path=off가 없으면 tmpfs와 캐시 볼륨이 달라 rename 대신 복사가 발생합니다. -
X-Cache-Status헤더와$upstream_response_time이 access.log에 찍히고 있는가. 캐시 동작과 백엔드 응답 속도를 실시간으로 볼 수 있어야 장애 시 원인 파악이 빠릅니다. 이 헤더가 없는 상태에서 캐시 이슈가 생기면 추측 디버깅에 수 시간을 쓰게 됩니다.
설정 파일 한 줄의 의미를 모르고 복사·붙여넣기하면 반드시 사고가 납니다. proxy_http_version 1.1, keepalive 32, fall 3 rise 2 — 이 지시어들이 왜 그 값이어야 하는지를 이해한 순간, 장애 상황에서 로그를 보고 원인을 즉시 찾아낼 수 있게 됩니다. 다음 단계로는 서비스 메시(Istio·Linkerd), API 게이트웨이(Kong·Apigee), 클라우드 관리형 LB(AWS ALB·GCP Cloud Load Balancing)로 학습 범위를 넓혀가시길 권합니다. 원리는 이 글과 1부에서 쌓은 기반 위에 쌓입니다.