Tomcat & nginx 운영 · nginx 리버스프록시 설정 · 이론
프록시 설정 여섯 줄과, 그 여섯 줄이 없을 때 벌어지는 일
한 줄 요약
프록시 설정에서 사고는 대개 여섯 줄짜리 헤더 전달과 proxy_pass 의 슬래시 하나에서 나며, 둘 다 빠져도 화면은 멀쩡히 뜨기 때문에 늦게 발견된다.
왜 앞에 웹서버를 두는가
톰캣도 HTTP 를 말할 줄 아는데 왜 앞에 nginx 를 두는가.
SI 현장의 실제 이유는 성능보다 운영 편의와 정책이다.
- 정적 파일을 WAS 가 처리하지 않게 한다 (WAS 스레드를 아낀다)
- SSL 종료를 한 지점에서 한다 (인증서 교체를 서버 하나에서 끝낸다)
- 여러 WAS 로 부하 분산하고, 하나가 죽어도 서비스가 유지되게 한다
- URL 기준으로 다른 시스템으로 라우팅한다 (
/api/는 신규, 나머지는 레거시) - 접근 제어, 요청 크기 제한, 압축, 캐시 헤더를 한 곳에서 건다
특히 마지막 줄이 크다. **정책을 애플리케이션에 넣으면 배포해야 바뀌지만,
프록시에 넣으면 reload 로 바뀐다.** 운영 조직이 프록시를 좋아하는 이유다.
프록시 설정 여섯 줄
location / { proxy_pass http://127.0.0.1:8080; 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; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; proxy_connect_timeout 3s; proxy_send_timeout 30s; proxy_read_timeout 60s;}각 줄이 없을 때 무슨 일이 나는지 알아야 외운 게 아니라 아는 것이 된다.
Host $host가 없으면 백엔드가 받는 Host 는127.0.0.1:8080이다.X-Forwarded-For가 없으면 애플리케이션이 보는 클라이언트 IP 는 전부X-Forwarded-Proto가 없으면 — 이게 리다이렉트 무한 루프의 최대 원인이다.
애플리케이션이 절대 URL 을 만들 때(리다이렉트, 메일 링크, 결제 콜백)http://127.0.0.1:8080/... 이 나간다. 이걸 오픈 후에 발견하면
결제 콜백이 안 돌아온다.
프록시 IP 다. 접속 이력, 부정 사용 탐지, IP 기반 접근 제어가 전부 무의미해진다.
감사에서 "접속자 IP 를 남기고 있습니까"에 답할 수 없다.$proxy_add_x_forwarded_for 는 기존 헤더에 이어 붙이는 변수라
프록시가 여러 단이면 목록이 된다.
프록시가 TLS 를 종료하고 백엔드에는 HTTP 로 넘긴다. 백엔드는
"HTTPS 가 아니네" 하며 https:// 로 리다이렉트한다. 다시 프록시로 오고,
또 HTTP 로 넘어가고, 무한 반복. 브라우저는 ERR_TOO_MANY_REDIRECTS 를 띄운다.
루프의 절대다수는 **TLS 종료 지점과 애플리케이션의 HTTPS 강제 로직이
서로를 모르는 데서** 나온다.
그리고 중요한 보안 원칙 하나: 이 헤더들은 최전방 프록시에서 반드시 덮어써야 한다.
클라이언트가 X-Forwarded-For: 10.0.0.1 을 직접 보낼 수 있기 때문이다.proxy_set_header 로 덮어쓰면 클라이언트가 보낸 값은 무시된다(이어 붙는 변수는 예외).
proxy_pass 의 슬래시 — 가장 흔한 nginx 버그
location /api/ { proxy_pass http://backend; # 슬래시 없음 → /api/users/1 이 그대로 전달}location /api/ { proxy_pass http://backend/; # 슬래시 있음 → /api 부분이 잘리고 /users/1 로 전달}URI 부분이 있으면(슬래시 포함) location 에 매칭된 부분이 잘려 나간다.
백엔드가 /api 컨텍스트를 기대하는데 슬래시를 붙이면 404 가 쏟아지고,
반대로 백엔드가 루트를 기대하는데 슬래시를 빼면 /api/api/users 가 된다.
이 한 글자로 반나절을 쓰는 사람을 매년 본다. 헷갈리면
액세스 로그에 $upstream_addr 와 백엔드 로그의 요청 경로를 함께 보면 즉시 판별된다.
요청 크기 — 413 의 정체
nginx 의 client_max_body_size 기본값은 1MB 다.
파일 업로드가 있는 시스템에서 이 값을 안 올리면 3MB 짜리 첨부를 올릴 때413 Request Entity Too Large 가 난다. 그리고 이 오류는 **nginx 가 반환하므로
애플리케이션 로그에는 아무것도 안 남는다.** 개발자는 "서버에서 아무 로그도 없어요"라며
며칠을 헤맨다.
client_max_body_size 20m;client_body_buffer_size 128k;large_client_header_buffers 4 16k;large_client_header_buffers 는 헤더가 클 때 쓴다. SSO 를 붙이면 쿠키가
커지는데, 기본 버퍼를 넘으면 400 Bad Request 가 난다. 이것도 로그가 애매하다.
압축과 정적 파일
gzip on;gzip_comp_level 6;gzip_min_length 1000;gzip_types text/plain text/css application/json application/javascript text/xml;gzip_vary on;gzip_comp_level은 6 근처가 CPU 대비 압축률 균형점이다. 9 로 올려도gzip_min_length 1000— 1KB 미만은 압축해도 이득이 없고 오버헤드만 있다.gzip_vary on을 빼면 중간 캐시가 압축본을 비압축 클라이언트에 줄 수 있다.gzip_types에text/html은 항상 포함돼 있으니 안 적어도 된다.
크기는 몇 % 줄고 CPU 는 눈에 띄게 는다.
정적 파일은 아예 프록시를 태우지 않는 것이 낫다.
location /static/ { alias /app/static/; expires 7d; access_log off;}root 와 alias 를 헷갈리는 것도 단골이다. location /static/ 에 대해root /app; 이면 /app/static/파일, alias /app/static/; 이면 /app/static/파일.
같아 보이지만 location /s/ 에 alias /app/static/; 라면 /s/a.js →/app/static/a.js 다.
location 경로와 실제 디렉터리 이름이 다를 때 alias 를 쓴다.
관리자 화면 차단
톰캣 매니저나 액추에이터가 외부에 열려 있는 것은 실제 사고로 이어진다.
프록시에서 막는 것이 가장 확실하다.
location ~ ^/(manager|host-manager)/ { return 403; }location /actuator/ { allow 10.0.0.0/8; deny all; proxy_pass http://127.0.0.1:8080;}여기서 배울 점: **애플리케이션 설정으로도 막을 수 있지만, 프록시에서 막으면
애플리케이션을 배포하지 않고도 정책을 바꿀 수 있다.** 보안 점검 지적이
금요일 오후에 내려와도 reload 로 끝난다.
액세스 로그에 무엇을 남길 것인가
기본 combined 포맷으로는 장애 분석이 안 된다. 최소한 이 네 개는 넣는다.
log_format labhub '$remote_addr - $remote_user [$time_local] ' '"$request" $status $body_bytes_sent ' 'ua="$upstream_addr" us=$upstream_status ' 'urt=$upstream_response_time rt=$request_time';$upstream_addr— 어느 백엔드로 갔는가 (부하분산 시 필수)$upstream_status— 백엔드가 준 상태코드 (nginx 가 만든 502 와 구분된다)$upstream_response_time— 백엔드가 쓴 시간$request_time— 클라이언트 기준 전체 시간
$request_time 은 크고 $upstream_response_time 은 작으면 문제는 백엔드가 아니라
클라이언트 네트워크나 응답 전송이다. 이 두 값의 차이 하나로
"백엔드가 느리다"는 오해를 몇 번이나 깰 수 있다.
현장에서 만나는 모습
헤더 여섯 줄이 빠졌을 때 나타나는 증상은 서로 무관해 보인다. 접속 로그의 클라이언트 IP 가 전부 프록시 IP 한 개로 찍히고 (X-Real-IP·X-Forwarded-For 누락), 로그인 뒤 리다이렉트가 내부 주소나 http 로 튀고 (Host·X-Forwarded-Proto 누락), IP 기반 접근 제어나 감사 로그가 통째로 무의미해진다.
공통점은 평상시에는 아무 문제가 없다는 것이다. 그래서 오픈하고 몇 주 뒤 보안 감사나 장애 분석에서야 드러나고, 그때는 이미 그 로그로 아무것도 추적할 수 없다.
proxy_pass 슬래시는 더 조용하다. 끝에 슬래시가 있으면 location 경로를 떼고 넘기고 없으면 붙여서 넘기는데, 개발 환경에서는 경로가 루트라 차이가 안 드러나다가 운영에서 컨텍스트 경로가 붙는 순간 404 가 된다.