요약
nginx를 리버스프록시로 적용하면서 생기는 몇 문제를 해결했습니다.
기존엔 클라이언트는 WAS와 직접 통신하고 커넥션을 형성했는데, 이젠 리버스프록시와 통신해야하니 리버스프록시에 CORS 관련 코드를 심어줘야했습니다.
그리고 소켓 통신을 위한 커넥션을 형성하기 위해서 전달되는 헤더가 있었는데, 이게 hop-by-hop 헤더라서 프록시까지만 전달되고 소멸되어 소켓 커넥션이 형성되지 않는 문제가 있었습니다. 그래서 nginx에서 직접 관련 헤더들을 다시 직접 추가해줌으로써 문제를 해결했습니다. 핸드쉐이크를 심어줘야 했습니다.
마지막으로, url을 기준으로 api, 소켓통신, 프론트엔드 리소스용 요청을 분기처리 했습니다.
nginx의 리버스프록싱 기능을 통해 blue-green 무중단 배포를 할 수 있었습니다. 그런데 프로젝트를 진행하면서 몇 가지 문제를 직면했고, 하나하나 해결해나가며 새로운 지식을 체득했습니다.
기존엔 클라이언트가 서버와 직접 통신함으로써, 클라이언트의 Origin이 다를 땐 브라우저에서 보낸 Preflight를 서버가 받아 허용여부를 따졌습니다. 만약 적합한 Origin이라면 응답 헤더에 허용 Origin에 해당 Origin을 추가함으로써 브라우저가 요청이 가능함을 인지했습니다.
그런데 클라이언트와 앱서버 사이에 nginx가 끼게 됨으로써 사실상 클라이언트는 앱서버가 아닌 nginx와 통신하게 됩니다. nginx엔 Preflight에 대응하는 로직이 없기 때문에 허용 Origin 헤더를 추가하지 않게 되고, 브라우저 측에선 CORS 문제로 인식해 오류를 냅니다.
그래서 nginx측에 preflight(OPTIONS 메서드) 요청이 오면 CORS를 허용하는 설정을 추가함으로써 문제를 해결했습니다.
location /api {
# preflight 요청이라면 CORS를 허용하는 내용의 헤더를 추가하고, 204 응답을 내림
if (request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, PATCH, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, StorelId';
add_header 'Access-Control-Max-Age' 86400;
return 204;
}
# Preflight(OPTIONS)가 아니라면 api서버로 넘긴다.
proxy_pass http://dambae200;
}
이 서비스엔 소켓통신 기술이 포함되어있는데, nginx를 통해 소켓통신을 할 때 문제가 생기는 것을 발견했습니다. 알고보니 소켓통신 기술로 사용중인 Stomp의 기반기술인 WebSocket이 HTTP서버를 통해 소켓통신을 하기위해 전달하는 헤더가 hop-by-hop 헤더인 것이 문제였습니다. 그래서 해결한 방법은, nginx에서 소켓 통신에 대한 요청을 받으면, 직접 Upgrade와 Connetion, Host 헤더들을 다시 직접 추가해줌으로써 문제를 해결했습니다.
좀 더 구체적으로 어떻게 문제지?
웹소켓 프로토콜은 HTTP 프로토콜과 다르지만, 웹소켓 핸드셰이크(WebSocket HandShake)는 HTTP 요청을 웹소켓으로 업그레이드 시킴으로써 HTTP-웹소켓 프로토콜을 상호호환되게 만들어줍니다. 이 업그레이드를 하는 방법은, HTTP에서 WebSocket으로 연결 전환시에 HTTP 요청에 Upgrade 및 Connection 헤더를 전달하는 것입니다. 그런데 리버스 프록시 서버가 존재하게 되면, 앱서버가 이 헤더들을 리버스 프록시 서버가 먼저 받게 되는데, 이 헤더들은 다음 hop으로 넘겨지지 않도록 되어있기 때문에, 서버측에선 Upgrade 헤더를 받을 수 없게 되는 것입니다.
그리고 HTTP의 단기 연결과 달리 WebSocket은 오래 지속되기 때문에, 리버스 프록시는 연결을 닫지 않고 열린 상태로 유지하는 것을 허용해야 합니다. 그래서 Connection 헤더로 Upgrade 커넥션임을 명시함으로써 연결을 유지합니다.
# 소켓 요청 url에 대해
location ~ ^/stomp/ {
# preflight 요청에 CORS를 허용하는 헤더 추가
if (request_method = 'OPTIONS'){
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, PATCH, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, StorelId';
add_header 'Access-Control-Max-Age' 86400;
return 204;
}
# 소켓통신을 요청함을 의미하는 헤더들을 추가해줌.
proxy_pass http://dambae200;
# hop-by-hop 헤더인 upgrade 헤더를 그대로 서버로 보내주도록 명시
proxy_set_header Upgrade $http_upgrage;
proxy_set_header Connection "upgrade";
# 받는 대상 서버(WAS)
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# HTTP/1.1 버전에서 지원하는 프로토콜 전환 메커니즘을 사용한다
proxy_http_version 1.1;
}
프론트엔드 리소스를 사용자에게 보여줄 방법을 고민해봤었는데, nginx를 통해 쉽게 할 수 있는 방법이 있어서 적용했습니다.
아래처럼 적어놓으면 '/'경로에 접근할 때, 명시한 root 디렉토리를 기반으로 리소스를 찾아서 전달해줄 수 있었습니다.
location / {
root /home/dambae200/dambae200_front/www;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
중요한 점은 여기서 명시하는 root 디렉토리는 /root 하위의 디렉토리가 아닌 /home 하위의 특정 사용자의 디렉토리여야 하고, 그 디렉토리의 권한 역시 해당 사용자가 소유해야한다는 것입니다. 왜냐하면 nginx 프로세스는 root 권한이 없기 때문에, /root 디렉토리나 root 사용자가 만든 디렉토리에 접근할 수 없게되기 때문입니다.
그래서 github에서 웹훅을 통해 Jenkins로 불러온 빌드파일을 nginx로 보낼 때, nginx 서버의 root사용자가 아닌 일반 사용자에게 보내야 합니다. 아래 코드를 예로 들어보면 dambae200 사용자에게 보내고 있습니다.
sh 'scp -r /var/jenkins_home/workspace/dambae200-front dambae200@nginx-ssh:/home/dambae200'
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
include /etc/nginx/conf.d/*.conf;
# api 트래픽을 받을 포트들.
# 둘 다 내부적으로 1024로 포트포워딩된다.
upstream dambae200 {
server 49.50.164.244:1025;
server 49.50.164.244:1026;
}
server {
listen 80;
server_name 49.50.164.244;
# 프론트 리소스가 리턴됨
location / {
root /home/dambae200_front/www;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
# CORS로 인한 문제를 해소하기 위해 Preflight(OPTIONS) 요청의 응답에 몇 가지 헤더를 추가함
location /api {
if (request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, PATCH, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, StorelId';
add_header 'Access-Control-Max-Age' 86400;
return 204;
}
# Preflight(OPTIONS)가 아니라면 api서버로 넘긴다.
proxy_pass http://dambae200;
}
# 소켓통신 세션 생성을 위한 http 요청
# 마찬가지로 Preflight 응답에 몇가지 헤더를 추가해줌
location ~ ^/stomp/ {
if (request_method = 'OPTIONS'){
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, PATCH, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, StorelId';
add_header 'Access-Control-Max-Age' 86400;
return 204;
}
# 소켓통신을 요청함을 의미하는 헤더들을 추가해줌.
proxy_pass http://dambae200;
proxy_set_header Upgrade $http_upgrage;
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_http_version 1.1;
}
}
}