nginx proxy_pass 업스트림 장애 격리하기

proxy_pass에 호스트명을 그대로 쓰면 기동 시 한 번만 해석되고, 실패하면 nginx가 뜨지 않는다. 변수 + resolver로 요청 시점 해석으로 바꾸는 방법과 주의점.

nginx proxy_pass 업스트림 장애 격리하기

증상

Docker Compose에서 nginx 컨테이너가 여러 서비스의 리버스 프록시를 맡는 구성이다. 서비스 두 개의 배포가 겹치자 nginx가 기동에 실패했고 도메인 전체가 502가 됐다.

[emerg] host not found in upstream "pharmacy-finder" in
        /etc/nginx/conf.d/locations/pharmacy-finder.conf:2
nginx: configuration file /etc/nginx/nginx.conf test failed

배포 스크립트는 서비스를 올린 뒤 nginx를 재시작한다.

pharmacy-finder-up:
	docker compose up -d pharmacy-finder
	docker compose restart nginx

겹친 순서는 이렇다.

  1. A 배포가 docker compose stop A로 A 컨테이너를 내린다
  2. 같은 시점에 B 배포가 docker compose restart nginx를 실행한다
  3. nginx 설정이 참조하는 A가 없어서 설정 테스트에 실패하고 nginx가 뜨지 않는다

원인

location /pharmacy-finder {
    proxy_pass http://pharmacy-finder:3001;
}

proxy_pass에 호스트 이름을 문자열로 쓰면 nginx는 기동 시점에 한 번만 그 이름을 IP로 해석한다. 해석에 실패하면 설정 오류로 보고 기동을 거부한다.

  • 업스트림 하나가 내려간 상태에서 nginx가 재시작되면 nginx 전체가 못 뜬다
  • 평소에는 드러나지 않는다. 컨테이너를 재생성해 IP가 바뀌어도 배포 끝의 restart nginx가 다시 해석해 주기 때문이다

해결: 변수 + resolver

proxy_pass에 변수가 들어가면 기동 시점이 아니라 요청이 올 때 resolver로 이름을 해석한다.

server {
    # Docker 내장 DNS. 컴포즈 네트워크 안의 이름을 해석한다
    resolver 127.0.0.11 valid=10s;

    location /pharmacy-finder {
        set $upstream http://pharmacy-finder:3001;   # 변수로 받아서
        proxy_pass $upstream;                        # 런타임에 해석
    }
}
  • 업스트림이 죽어 있어도 nginx는 기동한다. 해당 경로만 502가 된다
  • 컨테이너가 살아나면 nginx 재시작 없이 복구된다

주의: 변수를 쓰면 URI 치환이 안 된다

끝에 슬래시가 붙은 proxy_pass는 /anki/foo를 업스트림에 /foo로 넘긴다.

location /anki/ {
    proxy_pass http://anki-generator:8000/;   # 끝의 슬래시
}

변수를 쓰면 이 치환이 동작하지 않는다. /anki/foo가 그대로 넘어가서 404가 난다. rewrite로 직접 잘라야 한다.

location /anki/ {
    resolver 127.0.0.11 valid=10s;
    set $upstream http://anki-generator:8000;
    rewrite ^/anki/(.*)$ /$1 break;    # 접두사를 직접 제거
    proxy_pass $upstream;
}

같이 고친 것: 안 쓰는 포트 매핑

nginx를 다시 띄울 때 에러가 하나 더 났다.

failed to bind host port 0.0.0.0:443/tcp: address already in use
  • compose에 "443:443" 매핑이 있었지만 nginx에는 443 리스너가 없었다. TLS는 앞단 CDN에서 종단하고 내부는 80만 쓴다
  • nginx가 죽어 443을 놓은 사이 같은 서버의 다른 데몬이 그 포트를 잡았고, nginx 재기동이 막혔다
  • 매핑을 지웠다
ports:
  - "80:80"
  # - "443:443"   ← 삭제. 리스너가 없는 매핑

검증

# 1. 서비스 하나를 죽인다
docker stop pharmacy-finder

# 2. 그 상태에서 nginx를 재시작한다 (사고 당시와 같은 조건)
docker restart nginx
  • nginx 정상 기동
  • 다른 서비스는 200, 죽인 서비스만 502
  • docker start pharmacy-finder 후 nginx를 건드리지 않아도 해당 경로가 200으로 복구