Thumbnail

9분

Use A Reverse Proxy(Nginx)

https://www.fastify.io/docs/latest/Guides/Recommendations/
https://medium.com/intrinsic/why-should-i-use-a-reverse-proxy-if-node-js-is-production-ready-5a079408b2ca

최근에 GraphQL을 익혀보려고 Node.js로 graphql 서버를 연습삼아 작성하고 있습니다.

서버는 Fastify를 사용하고 있어서 해당 Docs 사이트를 많이 참고하는데 그 중에 Reverse Proxy에 대한 내용이 꽤 자세히 되어 있어서 이렇게 기억하기 위해 옮겨보려고 합니다.

들어가면서

Node.js는 표준 라이브러리 내에서 easy-to-use 웹 서버를 가진 프레임워크 입니다. 이전에는 PHP, Python과 같은 언어에서는 웹 서버나 해당 언어와 작동하는 일종의 CGI gateway를 설정할 수 있는 기능이 필요했습니다.

CGI gatweay: Common Gateway Interface
웹 서버 상에서 어플리케이션을 동작시키기 위한 조합입니다. 이름에서 알 수 있듯이, CGI는 어디까지나 인터페이스이며, 특정 플랫폼에 의존하지 않고, 웹 서버 등으로부터 외부 프로그램을 호출하는 조합을 가리킵니다. 근래에는 어지간한 웹 서버는 CGI를 다 지원합니다.

Node.js를 사용하면 직접 HTTP request를 다룰 수 있습니다. 그로 인해 multiple domain에 대한 요청, muptiple port(ex, HTTP, HTTPS)에 대한 listen을 하는 이러한 어플리케이션을 인터넷에 직접 노출하여 request를 처리하려는 유혹이 있습니다.

Fastify 팀은 이것은 안티패턴이자 극도로 나쁜 케이스로 강력하게 고려합니다.

  1. 어플리케이션에 불필요한 복잡성을 추가합니다.
  2. horizontal scalability(스케일 아웃)을 막습니다.

Reverse Proxy를 사용하는 논의에 대한 예는 다음과 같은 상황들이 있을 수 있습니다.

  1. 앱은 load를 처리하기 위해 여러 인스턴스가 필요합니다.
  2. 앱은 TLS(SSL) Termination이 필요합니다.
    (TLS(SSL) Termination은 암호화된 데이터 트래픽을 해독하는 프로세스 입니다. HTTPS를 말합니다.)
  3. 앱은 HTTP요청을 HTTPS로 redirect 하는 기능이 필요합니다.
  4. 앱은 muptiple domain을 serve하는 기능이 필요합니다.
  5. 앱은 static 리소스(ex. jpeg files)를 serve하는 기능이 필요합니다.

사용 가능한 Reverse Proxy 솔루션이 많이 있습니다. AWS나 GCP같은 환경에 따라 솔루션이 정해질 수 있습니다.

왜 Node.js는 Reverse Proxy를 사용해야만 하는가?

https://medium.com/intrinsic-blog/why-should-i-use-a-reverse-proxy-if-node-js-is-production-ready-5a079408b2ca

2012년 PHP, Ruby on Rails가 웹 어플리케이션 서버의 최강자였습니다. 그러나 새로운 경쟁자가 커뮤니티를 폭풍으로 몰아넣었습니다. 그 경쟁자는 동시에 1M 연결을 다루는 서버였습니다. 이 기술은 Node.js였으며 그 이후로 꾸준히 인기를 얻고 있습니다.

당시 대부분의 경쟁 서비스와 달리 Node.js는 웹 서버가 내장(built-in)되어 있었습니다. 이것은 개발자들이 많은 설정 파일(php.ini, .htaccess등)을 지나칠 수 있다는 것이었습니다. 내장된 웹 서버가 있으면 업로드된 파일을 처리하는 기능, Websocket 구현의 용이성과 같은 다른 편의성도 제공되었습니다.

매일 Node.js 기반 웹 어플리케이션은 수십억 개의 요청을 문제없이 처리합니다. 세계의 많은 큰 회사에서도 대부분은 Node.js를 사용하여 서비스를 제공되고 있습니다. 그러나 Node.js가 시작된 이래로 유지되는 몇 가지 조언이 있습니다:

Node.js process를 웹에 노출하지 말고 reverse proxy뒤에 숨겨라.

reverse proxy를 사용해야하는지 이유를 보기 전에 먼저 reverse proxy가 무엇인지 살펴보겠습니다.

Reverse Proxy

Reverse Proxy는 기본적으로 요청을 받고, 다른 곳에 있는 HTTP 서버에게 넘기고, 응답을 받고, 원래 요청자에게 응답을 하는 웹 서버의 특별한 형태입니다.
no proxy:
request: requester -> http server
response: http server -> requester
with reverse proxy:
request: requester(요청) -> reverse proxy -> http server(처리)
response: http server(응답) -> reverse proxy -> requester

reverse proxy는 일반적으로 받았던 request와 동일한 request을 보내지 않습니다. 일반적으로 어떤 방식으로든 request를 수정합니다.

다양한 reverse proxy가 있습니다.

가장 유명한 2가지는 NginxHAProxy 입니다.

이 2개 모두 gzip 압축을 수행하고 HTTPS 지원을 추가할 수 있으며 다른 영역에도 특화되어 있습니다. Nginx가 2가지 중에서 더 많이 사용되며 파일 시스템에서 static files를 제공하는 기능과 다른 유용한 기능도 있으므로 이 글 전체에서 예로 사용할 것입니다.

Reverse Proxy를 사용해야하는 이유

  • SSL Ternimation
  • gzip Compression
  • Clustering
  • Enterprise Routing
  • Performance Benefits
  • Simplified Application Code

SSL Termination

https

SSL Termination은 reverse proxy를 사용하는 가장 일반적인 이유 중 하나입니다.

http에서 https로 변경하는 것은 단순히 s를 붙히는 것 보다는 더 많은 것을 해야 합니다. Node.js는 스스로 https를 위해 필요한 encryption, decryption을 수행할 수 있습니다. 그리고 필요한 인증서 파일을 읽도록 구성할 수 있습니다.

그러나 어플리케이션과 통신하는데 사용되는 프로토콜을 구성하는 것과 계속 만료되는 SSL 인증서를 관리하는 것은 실제로 어플리케이션에서 걱정할 것들이 아닙니다. 코드베이스에서 인증서를 확인하는 것은 지루할 뿐만 아니라 보안상 위험이 있습니다. 어플리케이션 시작할 때 central location에서 인증서를 얻는 것 또한 위험을 가집니다.

expressjs security best practices#use-tls
Use TLS
...일반적으로 TLS을 처리하는데 Nginx를 권장합니다...

이러한 이유로 어플리케이션 외부, 일반적으로 reverse proxy 내에서 SSL Termination을 수행하는 것이 좋습니다.

reverse proxy가 SSL Termination을 수행하도록 허용한다는 것은 reverse proxy 작성자가 만든 코드만이 private SSL 인증서에 접근할 수 있다는 것을 의미합니다.

그러나 Node.js 어플리케이션에서 SSL을 처리한다면 어플리케이션에서 사용하는 모든 third-party 모듈(심지어 잠재적인 악성 모듈 포함)이 privacy SSL 인증서에 접근할 수 있습니다.

gzip Compression

accept-encoding: gzip, br, ...

gzip 압축은 어플리케이션에서 reverse proxy로 짐을 덜어내야만 하는 또다른 기능입니다. gzip 압축 정책은 각 어플리케이션에서 지정하고 구성하는 대신 organization 레벨에서 가장 잘 설정되는 것입니다.

무엇을 gzip 압축할지 결정할 때 몇 가지 로직을 적용하는 것이 좋습니다. 예를 들어, 매우 작거나 1kb보다 작은 파일은 gzip 압축 결과물이 종종 더 클 수 있거나 클라이언트가 파일 압축을 풀 때 CPU 오버헤드가 가치가 없을 수 있으므로 gzip 압축하는 것이 아마도 가치가 없습니다. 또한 바이너리 data를 다룰 때, format에 따라 압축할 때 이익이 되지 않을 수 있습니다. gzip은 쉽게 활성화하거나 비활성화할 수 없으며 대응 가능한 압축 알고리즘을 위해 request Accept-Encoding header를 검사해야 합니다.

Clustering

scaling

JavaScript는 싱글 스레드 언어입니다. 따라서 Node.js는 전통적으로 싱글 스레드 서버 플랫폼이었습니다. 즉, Node.js 어플리케이션에서 최대한 많은 처리량을 얻으려면 CPU 코어 수와 거의 동일한 수의 인스턴스를 실행해야 합니다.

Node.js는 내장된 cluster module과 함께 제공됩니다. 들어오는 HTTP request는 마스터 process에서 만들어질 것입니다. 그리고 cluster worker로 전달될 것입니다.

그러나, cluster worker를 동적으로 스케일링하는 것은 몇 가지 노력이 필요합니다. 또한 마스터 process를 전달하는 것과 같은 추가적인 Node.js process를 동작하는 것은 보통 overhead가 발생합니다. 다른 machine으로 process를 스케일링하는 것은 cluster module이 할 수 없는 것들입니다.

이러한 이유로 실행 중인 Node.js process에게 request를 전달하는 것은 reverse proxy를 사용하는 것이 더 좋습니다. reverse proxy는 request가 도착할 때 새로운 어플리케이션 process를 가리키도록 동적으로 구성할 수 있습니다. 실제로, 어플리케이션은 자신의 일을 하는데 신경써야만 합니다. request를 복사하거나 전달하는 것을 관리하는데 신경써서는 안됩니다.

Enterprise Routing

많은 팀으로 구성된 기업에서 만든것과 같이 거대한 웹 어플리케이션을 다룰 때, request를 어디로 보내야할지를 결정하기 위해 reverse proxy를 사용하는 것이 매우 유용합니다.

예를 들어 example.org/search/* 에 대한 request는 internal 검색 어플리케이션으로 라우팅될 수 있습니다. 반면 example.org/profile/* 에대한 request는 internal 프로필 어플리케이션으로 전달될 수 있습니다.

이러한 도구는 sticky session, Blue/Green 배포, A/B 테스트 등과 같은 강력한 기능을 허용합니다.

sticky session: 한 서버 인스턴스에만 붙어있는 세션(A 서버에서 발급된 세션이어서 B 서버에서는 세션이 없습니다. 그래서 A 서버에서 계속 응답을 할 수 있게 처리하는 것, 복제해서 다른 서버에 동기화 해주는 것과 같은 것들이 해결방법으로 있습니다.)
Blue/Green 배포: 무중단 배포 기능 중 하나로 Blue는 구 버전, Green은 신 버전으로 서버들을 동시에 분리하여 구성하고, 일제히 전환하는 전략입니다.
A/B 테스트: 변수 A에 비해 대상이 변수 B에 대해 보이는 응답을 테스트하는 방식입니다.

저자: 개인적으로 어플리케이션 내에서 수행했던 로직들이 있던 코드베이스에서 일했었는데, 이 접근법은 어플리케이션을 유지관리하는데 상당히 어려움을 주었습니다.

Performance Benefits

Node.js는 매우 유연합니다. 파일 시스템에서 static asset을 제공하고, HTTP reponse와 함께 gzip 압축을 수행하고, HTTPS 및 기타 여러 기능에 대한 기본 기능이 제공됩니다. cluster 모듈을 통해 여러 인스턴스를 실행하고 자체 request 전달 수행하는 기능도 있습니다.

그러나 궁극적으로 Node.js 어플리케이션이 처리하는 대신에 reverse proxy가 이러한 작업을 처리하도록 하는 것이 가장 좋습니다. 위에 리스트된 이유 외에도 Node.js 외부에서 이러한 작업을 수행하려는 또 다른 이유는 바로 효율성 때문입니다.

SSL 암호화와 gzip 압축은 CPU 사용이 높은 2가지 작업들입니다. Nginx와 HAProxy와 같은 reverse proxy 툴을 사용한다면 일반적으로 Node.js보다 이러한 작업을 더 빠르게 수행합니다. Nginx와 같은 웹 서버가 디스크에서 static content를 읽는 것도 Node.js보다 빠를 것입니다. Nginx와 같은 reverse proxy가 추가적인 Node.js process보다 메모리와 CPU를 덜 사용하므로 클러스터링도 보통 더 효율적일 수 있습니다.

다음은 벤치마크 결과입니다.

  • siege를 이용한 벤치마크
  • a concurrency value of 10 * 20,000 iterations = total requests: 200,000
  • Nginx: 2개 인스턴스(1 master, 1 worker)
  • Node.js: 3개 클러스터(1 master, 2 workers)

benchmark

  • Nginx 사용 시 Node.js에 비해 SSL Termination 수행 처리량이 ~16% 더 많습니다.
  • Nginx 사용 시 Node.js에 비해 gzip 압축 수행 처리량이 ~50% 더 많습니다.
  • Nginx 사용 시 Node.js에 비해 클러스터 관리에 ~-1% 패널티가 있습니다. (아마도 루프백 네트워크 장치에서 추가적으로 request를 전달하는 overhead 때문에)

기본적으로 단일 Node.js proces 메모리 사용량은 ~600MB입니다. 반면에 Nginx는 ~50MB 정도입니다. 이것은 사용하는 기능에 따라 약간 달라질 수 있습니다.

  • Node.js는 SSL Termination 수행 시 ~13MB 메모리를 더 사용합니다.
  • Nginx는 파일시스템으로 부터 static content를 제공하는 reverse proxy를 사용 시 ~4MB를 더 사용합니다.

흥미로운 점은 Nginx는 lifetime 전반에 일관적인 메모리 사용량을 보입니다. 그러나 Node.js는 JavaScript의 garbage-collecting 환경 때문에 끊임없이 출렁입니다.

Simplified Application Code

Node.js 어플리케이션에서 reverse proxy로 작업을 옮길 때의 가장 큰 이점은 코드 단순성입니다. 잠재적으로 버그가 있는 명령형(imperative) 어플리케이션 코드를 줄이고 선언적(declarative) 구성으로 변경하는 것입니다. 개발자들 사이의 일반적인 감정은 스스로 작성한 코드보다 Nginx와 같은 외부 엔지니어 팀이 작성한 코드에 더 자신감을 가진다는 것입니다.

reverse proxy는 프로토콜과 프로세스 관리를 잊게하여 비지니스 로직에 집중하도록 만들어줍니다.

추가: Nginx Configuration

# This upstream block groups 3 servers into one named backend fastify_app
# with 2 primary servers distributed via round-robin
# and one backup which is used when the first 2 are not reachable
# This also assumes your fastify servers are listening on port 80.
# more info: http://nginx.org/en/docs/http/ngx_http_upstream_module.html
upstream fastify_app {
  server 10.10.11.1:80;
  server 10.10.11.2:80;
  server 10.10.11.3:80 backup;
}
 
# This server block asks NGINX to respond with a redirect when
# an incoming request from port 80 (typically plain HTTP), to
# the same request URL but with HTTPS as protocol.
# This block is optional, and usually used if you are handling
# SSL termination in NGINX, like in the example here.
server {
  # default server is a special parameter to ask NGINX
  # to set this server block to the default for this address/port
  # which in this case is any address and port 80
  listen [::]:80 default_server;
 
  # With a server_name directive you can also ask NGINX to
  # use this server block only with matching server name(s)
  # listen 80;
  # listen [::]:80;
  # server_name example.tld;
 
  # This matches all paths from the request and responds with
  # the redirect mentioned above.
  location / {
    return 301 https://$host$request_uri;
  }
}
 
# This server block asks NGINX to respond to requests from
# port 443 with SSL enabled and accept HTTP/2 connections.
# This is where the request is then proxied to the fastify_app
# server group via port 3000.
server {
  # This listen directive asks NGINX to accept requests
  # coming to any address, port 443, with SSL, and HTTP/2
  # if possible.
  listen 443 ssl http2 default_server;
  listen [::]:443 ssl http2 default_server;
 
  # With a server_name directive you can also ask NGINX to
  # use this server block only with matching server name(s)
  # listen 443 ssl http2;
  # listen [::]:443 ssl http2;
  # server_name example.tld;
 
  # Your SSL/TLS certificate (chain) and secret key in the PEM format
  ssl_certificate /path/to/fullchain.pem;
  ssl_certificate_key /path/to/private.pem;
 
  # A generic best practice baseline for based
  # on https://ssl-config.mozilla.org/
  ssl_session_timeout 1d;
  ssl_session_cache shared:FastifyApp:10m;
  ssl_session_tickets off;
 
  # This tells NGINX to only accept TLS 1.3, which should be fine
  # with most modern browsers including IE 11 with certain updates.
  # If you want to support older browsers you might need to add
  # additional fallback protocols.
  ssl_protocols TLSv1.3;
  ssl_prefer_server_ciphers off;
 
  # This adds a header that tells browsers to only ever use HTTPS
  # with this server.
  add_header Strict-Transport-Security "max-age=63072000" always;
 
  # The following directives are only necessary if you want to
  # enable OCSP Stapling.
  ssl_stapling on;
  ssl_stapling_verify on;
  ssl_trusted_certificate /path/to/chain.pem;
 
  # Custom nameserver to resolve upstream server names
  # resolver 127.0.0.1;
 
  # This section matches all paths and proxies it to the backend server
  # group specified above. Note the additional headers that forward
  # information about the original request. You might want to set
  # trustProxy to the address of your NGINX server so the X-Forwarded
  # fields are used by fastify.
  location / {
    # more info: http://nginx.org/en/docs/http/ngx_http_proxy_module.html
    proxy_http_version 1.1;
    proxy_cache_bypass $http_upgrade;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    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;
 
    # This is the directive that proxies requests to the specified server.
    # If you are using an upstream group, then you do not need to specify a port.
    # If you are directly proxying to a server e.g.
    # proxy_pass http://127.0.0.1:3000 then specify a port.
    proxy_pass http://fastify_app;
  }
}

마무리

비록 Node.js가 production에서 완벽하게 수행함에도 불구하고, HTTP Node.js 어플리케이션을 reverse proxy와 함께 사용하는 것은 많은 이점이 있습니다.
SSL과 gzip과 같은 작업이 더 빠르고. SSL 인증서 관리를 더 간편하게 만들고. 많은 양의 어플리케이션 코드 또한 줄일 수 있습니다.
Node.js 어플리케이션과 함께 reverse proxy를 사용하는 것을 강력하게 권장합니다.

Reverse proxy에 대한 추상적인 이해보다 조금 더 구체적으로 이해하게 된 좋은 기회였습니다.

Node.js에서 제공되는 기능에도 불구하고, reverse proxy를 사용하는 것이 많은 이점이 있다는 것을 배웠습니다.

이와 관련된 일이 있다면 다음엔 좀 더 이해를 바탕으로 작업을 할 수 있을 것 같습니다.

reference

마지막 업데이트

3/26/2023


Avatar

JHSeo

배우는 것을 좋아하고 관심이 많은 웹 엔지니어 입니다. 느리더라도 꾸준하게 성장하려고 노력하는 개발자입니다.