웹 UI 리버스 프록시 구성하기

웹 UI 리버스 프록시 구성하기

NGINX는 웹 서비스를 리버스 프록시하고 동일한 서비스의 여러 인스턴스에 걸쳐 로드를 분산하는 데 사용할 수 있어요. 리버스 프록시는 여러 웹 서비스가 하나의 기억하기 쉬운 도메인을 공유하고, 내부 시스템을 보기 위한 인증을 활성화할 수 있다는 추가 이점이 있어요.

노마드 UI의 모든 기능이 완전하게 동작하도록 하려면, 노마드의 특정 네트워킹 요구사항을 충족하도록 리버스 프록시를 제대로 구성해야 해요.

이 가이드는 노마드 웹 UI를 리버스 프록시할 때 필요한 일반적인 구성 변경을 다루어요. 기본 프록시 구성에서 흔히 발생하는 문제를 논의하고 시연해요. 각 문제를 배우면서 그 문제를 해결하는 NGINX 구성 변경을 배포하게 될 거예요.

출처: 문서

본문

사전 요구사항

이 가이드는 노마드와 NGINX에 대한 기본 지식을 가정해요. 이 가이드에 필요한 것은 다음과 같아요:

  • 로컬에 설치된 Nomad 0.11.0
  • Docker

노마드 시작

노드에 대한 최소 접근 권한 모범 사례 때문에, 노마드 UI 사용자는 보통 노마드 클라이언트 노드에 직접 접근할 수 없어요. 이 가이드에서는 잘못된 http 주소를 알리는 방식으로 그 상황을 시뮬레이션할 수 있어요.

nomad.hcl이라는 파일을 다음 구성 스니펫으로 만들어요.

# Advertise a bogus HTTP address to force the UI
# to fallback to streaming logs through the proxy.
advertise {
  http = "internal-ip:4646"
}

이 사용자 정의 구성 파일로 노마드를 dev 에이전트로 시작해요.

$ sudo nomad agent -dev -config=nomad.hcl

다음으로 stdout에 로그를 자주 쓰는 서비스 작업 파일을 만들어요. 아래 샘플 작업 파일을 자신의 것이 없으면 사용할 수 있어요.

# fs-example.nomad.hcl

job "fs-example" {
  datacenters = ["dc1"]

  task "fs-example" {
    driver = "docker"

    config {
      image = "dingoeatingfuzz/fs-example:0.3.0"
    }

    resources {
      cpu    = 500
      memory = 512
    }
  }
}

노마드 CLI나 UI를 사용해 이 서비스 작업을 실행해요.

$ nomad run fs-example.nomad.hcl

이 시점에 하나의 작업이 들어 있는 노마드 클러스터가 로컬에서 실행되고 있어요. http://localhost:4646에서 웹 UI를 방문할 수 있어요.

웹 UI를 리버스 프록시하도록 NGINX 구성

앞서 언급했듯이, 전체 목표는 노마드 UI 사용자에서 노마드 클러스터에서 실행 중인 노마드 UI로의 프록시를 구성하는 거예요. 이를 위해 NGINX 인스턴스를 리버스 프록시로 구성할 거예요.

웹 UI를 리버스 프록시할 기본 NGINX 구성 파일을 만들어요. NGINX 구성 파일 이름을 nginx.conf로 지정하는 것이 중요해요. 그렇지 않으면 파일이 제대로 바인딩되지 않아요.

# nginx.conf
events {}

http {
  server {
    location / {
      proxy_pass http://host.docker.internal:4646;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
  }
}

참고: Docker for Mac이나 Docker for Windows를 사용하지 않는다면 host.docker.internal DNS 레코드를 사용할 수 없을 수도 있어요.

이 기본 NGINX 구성은 두 가지를 수행해요. 첫 번째는 NGINX로 들어오는 모든 트래픽을 http://host.docker.internal:4646 프록시 주소로 전달해요. NGINX가 Docker에서 실행되고 노마드가 로컬에서 실행되므로 이 주소는 노마드 API와 웹 UI가 서비스되는 http://localhost:4646과 동일해요. 이 구성이 하는 두 번째 일은 X-Forwarded-For 헤더를 추가해 HTTP 요청을 발신지까지 추적할 수 있게 하는 거예요.

이제 새 터미널 세션에서 이 구성 파일을 사용해 NGINX를 Docker에서 시작해요.

$ docker run --publish=8080:80 \
    --mount type=bind,source=$PWD/nginx.conf,target=/etc/nginx/nginx.conf \
    nginx:latest

Docker가 레이어를 모두 가져오면 NGINX가 시작돼요. 이 시점에 http://localhost:8080을 방문해 NGINX 리버스 프록시를 통해 노마드 웹 UI에 접근할 수 있어요.

연결 타임아웃 확장

노마드 웹 UI는 실시간 업데이트 기능에 오래 지속되는(long-lived) 연결을 사용해요. 프록시가 연결 타임아웃 때문에 연결을 일찍 닫으면, 웹 UI가 데이터를 계속 실시간으로 다시 로드하지 못하게 될 수 있어요.

노마드 웹 UI는 노마드 서버 상태가 바뀔 때마다 뷰가 항상 최신 상태를 유지하도록 모든 데이터를 실시간으로 다시 로드해요. 이는 노마드 API에 대한 블로킹 쿼리(blocking queries)로 달성돼요. 블로킹 쿼리는 서버 측 상태가 바뀔 때까지 HTTP 연결을 열어 두는 롱 폴링(long-polling) 구현이에요. 이는 종종 새 정보를 반환하지 않는 더 많은 요청을 발생시키는 전통적인 폴링보다 유리해요. 새 정보가 생기자마자 연결이 닫히므로 폴링 루프의 다음 반복을 기다릴 필요 없이 더 빠르기도 해요. 이 설계의 결과로 HTTP 요청이 항상 짧을 것이라고 기대하면 안 돼요. NGINX는 기본 프록시 타임아웃이 60초인 반면, 노마드의 블로킹 쿼리 시스템은 기본적으로 연결을 5분 동안 열어 둬요.

프록시가 연결을 타임아웃하는 것을 관찰하려면, 브라우저 개발자 도구를 열어 프록시를 통해 http://localhost:8080/ui/jobs의 노마드 작업 목록을 방문해요.

노마드 UI 페이지가 열린 상태에서 F12 키를 눌러 개발자 도구를 열어요. 아직 선택되지 않았다면 개발자 도구 창으로 가서 Network 탭을 선택해요. 도구 창을 열어 둔 채 UI 페이지를 새로고침해요. 작업에 대한 블로킹 쿼리 연결은 "(pending)" 상태로 남아 있을 거예요.

약 60초 후 "(pending)" 상태에서 "504 Gateway Time-out" 상태로 전환돼요.

이 타임아웃을 방지하려면 NGINX 구성의 location 블록을 업데이트해 proxy_read_timeout 설정을 확장해요. 노마드 API 문서의 Blocking Queries 섹션은 노마드가 선언된 대기 시간에 (wait / 16) 결과를 더한다고 설명해요. proxy_read_timeout을 노마드가 계산한 대기 시간보다 약간 크게 설정해야 해요.

이 가이드는 기본 블로킹 쿼리 대기 시간인 300초를 사용해요. 노마드는 그 대기 시간에 18.75초를 더하므로, proxy_read_timeout은 318.75초보다 커야 해요. proxy_read_timeout을 319s로 설정해요.

# ...
proxy_pass http://host.docker.internal:4646;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

# Nomad blocking queries will remain open for a default of 5 minutes.
# Increase the proxy timeout to accommodate this timeout with an
# additional grace period.
proxy_read_timeout 319s;
# ...

이 구성 변경을 적용하려면 NGINX 도커 컨테이너를 다시 시작해요.

프록시 버퍼링 비활성화

가능하면 웹 UI는 스트리밍 HTTP 요청을 사용해 작업 로그 페이지에서 로그를 스트리밍해요. NGINX는 기본적으로 프록시 응답을 버퍼링해 프록시되는 백엔드 서버와의 연결을 최대한 빨리 해제하려 해요.

프록시 버퍼링으로 인해 로그 이벤트는 스트리밍되지 않아요. 연결이 닫히거나 프록시 버퍼 크기에 도달해 데이터가 마침내 클라이언트로 플러시될 때까지 로그가 NGINX의 프록시 버퍼 안에 일시적으로 갇히기 때문이에요.

오래된 브라우저는 이 기술을 지원하지 않을 수 있으며, 이 경우 로그는 단순한 폴링 메커니즘으로 스트리밍돼요.

이 문제를 관찰하려면 먼저 http://localhost:8080/jobs/fs-example에서 샘플 작업을 방문하고, 가장 최근 할당을 클릭하고, fs-example 작업을 클릭한 뒤, logs 탭을 클릭해 샘플 작업의 작업 로그 페이지를 방문해요.

로그가 로드되지 않고 결국 UI에 다음 오류가 나타나요. 브라우저 개발자 도구 콘솔에도 이 추가 오류가 나타나요.

GET http://internal-ip:4646/v1/client/fs/logs/131f60f7-ef46-9fc0-d80d-29e673f01bd6?follow=true&offset=50000&origin=end&task=ansi&type=stdout net::ERR_NAME_NOT_RESOLVED

이 ERR_NAME_NOT_RESOLVED 오류는 무시해도 안전해요. 웹 UI는 불필요할 때 노마드 서버 노드를 통해 로그를 스트리밍하지 않기 위해, 작업이 실행 중인 클라이언트 노드에 낙관적으로 직접 연결을 시도해요. 이 가이드에서 사용한 노마드 구성 파일이 도달할 수 없는 주소를 고의로 알리므로, UI는 자동으로 프록시를 통해 로그를 요청하도록 폴백해요.

NGINX를 통한 로그 스트리밍을 허용하려면 프록시 버퍼링을 비활성화하도록 NGINX 구성을 업데이트해야 해요. 기존 NGINX 구성 파일의 location 블록에 다음을 추가해요.

# ...
proxy_read_timeout 319s;

# Nomad log streaming uses streaming HTTP requests. In order to
# synchronously stream logs from Nomad to NGINX to the browser
# proxy buffering needs to be turned off.
proxy_buffering off;
# ...

이 구성 변경을 적용하려면 NGINX 도커 컨테이너를 다시 시작해요.

WebSocket 연결 활성화

Nomad 0.11.0부터 웹 UI는 클러스터의 실행 중인 작업과 대화형 exec 세션을 지원해요. 이는 WebSocket으로 구현된 exec API를 사용해 달성돼요.

WebSocket은 exec API에 필요해요. 양방향 데이터 전송을 허용하기 때문이에요. 이는 원격 출력의 변경을 수신하고 브라우저 기반 터미널에서 명령과 신호를 보내는 데 사용돼요.

WebSocket 연결이 수립되는 방식은 핸드셰이크 요청을 통해서예요. 핸드셰이크는 특별한 Connection과 Upgrade 헤더가 있는 HTTP 요청이에요.

WebSocket은 CORS 헤더도 지원하지 않아요. WebSocket 연결의 서버 쪽은 신뢰할 수 있는 오리진(Origin)을 자체적으로 검증해야 해요. 노마드는 핸드셰이크 요청의 Origin 헤더가 노마드 API 주소와 같은지 확인해 이 검증을 수행해요.

기본적으로 NGINX는 핸드셰이크나 오리진 검증을 수행하지 않아요. 그 결과 exec 세션이 즉시 종료돼요. 웹 UI에서 http://localhost:8080/jobs/fs-example로 가서 Exec 버튼을 클릭하고 작업을 선택한 뒤 /bin/sh 명령을 실행해 보면 이 현상을 경험할 수 있어요.

핸드셰이크를 이행하려면 NGINX가 Connection과 Upgrade 헤더를 전달해야 해요. 노마드 API가 요구하는 오리진 검증을 충족하려면 NGINX가 기존 Origin 헤더를 호스트 주소와 일치하도록 재정의해야 해요. 기존 NGINX 구성 파일의 location 블록에 다음을 추가해요.

# ...
proxy_buffering off;

# The Upgrade and Connection headers are used to establish
# a WebSockets connection.
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

# The default Origin header will be the proxy address, which
# will be rejected by Nomad. It must be rewritten to be the
# host address instead.
proxy_set_header Origin "${scheme}://${proxy_host}";
# ...

이 구성 변경을 적용하려면 NGINX 도커 컨테이너를 다시 시작해요.

WebSocket 연결은 상태 유지(stateful) 방식이기도 해요. NGINX를 사용해 모든 노마드 서버 노드에 걸쳐 로드를 분산할 계획이라면, WebSocket 연결이 일관된 호스트로 라우팅되도록 하는 것이 중요해요.

이는 NGINX에서 upstream을 지정해 프록시 패스로 사용하면 가능해요. 기존 NGINX 구성 파일의 server 블록 뒤에 다음을 추가해요.

# ...
# Since WebSockets are stateful connections but Nomad has multiple
# server nodes, an upstream with ip_hash declared is required to ensure
# that connections are always proxied to the same server node when possible.
upstream nomad-ws {
  ip_hash;
  server host.docker.internal:4646;
}
# ...

트래픽도 upstream을 통과해야 해요. 이렇게 하려면 NGINX 구성 파일에서 proxy_pass를 변경해요.

# ...
location / {
  proxy_pass http://nomad-ws
# ...

dev 환경은 노드가 하나뿐이므로 이 변경은 관찰 가능한 효과가 없어요.

완성된 NGINX 구성 검토

이 시점에서 모든 웹 UI 기능이 NGINX 프록시를 통해 동작해요. 다음은 완성된 NGINX 구성 파일이에요.

events {}

http {
  server {
    location / {
      proxy_pass http://nomad-ws;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

      # Nomad blocking queries will remain open for a default of 5 minutes.
      # Increase the proxy timeout to accommodate this timeout with an
      # additional grace period.
      proxy_read_timeout 319s;

      # Nomad log streaming uses streaming HTTP requests. In order to
      # synchronously stream logs from Nomad to NGINX to the browser
      # proxy buffering needs to be turned off.
      proxy_buffering off;

      # The Upgrade and Connection headers are used to establish
      # a WebSockets connection.
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";

      # The default Origin header will be the proxy address, which
      # will be rejected by Nomad. It must be rewritten to be the
      # host address instead.
      proxy_set_header Origin "${scheme}://${proxy_host}";
    }
  }

  # Since WebSockets are stateful connections but Nomad has multiple
  # server nodes, an upstream with ip_hash declared is required to ensure
  # that connections are always proxied to the same server node when possible.
  upstream nomad-ws {
    ip_hash;
    server host.docker.internal:4646;
  }
}

다음 단계

이 가이드에서 노마드 UI용으로 구성된 NGINX 리버스 프록시를 설정했어요. 또한 프록시를 통해서도 노마드 UI가 제대로 동작하도록 하는 데 필요한 일반적인 구성 속성(연결 타임아웃, 프록시 버퍼링, WebSocket 연결, Origin 헤더 재작성)도 살펴봤어요.

이런 구성 요소를 사용해 여러분이 선호하는 프록시 서버 소프트웨어를 노마드 UI와 함께 동작하도록 구성할 수 있어요. 이 가이드에서 다룬 NGINX 특정 구성에 대한 자세한 내용은 다음을 참고해 주세요:

더 알아보기 (Learn more)