Nginx로 Reverse Proxy 구현해보기

Two-Jay·2022년 11월 23일
1

Reverse-Proxy-구축

목록 보기
2/2

이제 실제로 Reverse Proxy 구축을 해보자.

들어가기 이전에, Reverse Proxy 구축하는 것은 어렵지 않지만, 여기에는 기존의 시스템을 얼마나 잘 파악했는지에 따라 난이도가 달라질 수 있다. 각 서버가 어디서 요청을 보내고, 어디서 요청을 받는지 잘 파악되어 있지 않다면 시간을 많이 잡아먹을 가능성이 크다. Reverse Proxy 만드는 게 뭐가 어렵겠어라고 생각했던 나도 기존 시스템을 파악하지 않고 들어갔다가 하루 걸릴 것을 일주일이나 잡아먹었다.


- 현재 상황

현재는 아래와 같이 서비스가 배포되어 있는 상황이다. 진행하고 있는 프로젝트의 특성상 프록시 서버가 요청받는 주소는 localhost로 진행해도 되었기에 당장에 이는 중요한 요소가 아니니, 여러분의 프로젝트에서 알맞는 상황을 상정하기를 바란다.

프로젝트의 특성상 강제된 부분이 몇몇 있었다. 이는 아래와 같다.

- 프로젝트 배포시 호스트에서 Oracle VirtualBox를 이용해 가상머신을 만들고, 그 안에서 배포할 것
- docker-compose 활용할 것
- dockerfile 안에서 베이스 이미지는 alpine || debian:buster 를 사용할 것

호스트 내부에 front, db, auth, socket 서버가 각각 분산되어 배포되어 있고, 이를 docker-compose를 이용해 각각의 컨테이너로 올라가 있는 상태이다. 각각 서버에서는 요청을 처리할 때엔 각각의 서버로 직접 요청을 보내고 있던 상황이었다. 그러나 각 서버의 컨테이너에서 포트를 개방시켜놓고 있던 관계로 브라우저에서 db나 auth서버에 직접 요청을 걸 수도 있던 상황이었다.

# 프로젝트 구조
# /가 끝에 붙으면 폴더를 의미
# Makefile을 이용하여 docker-compose 실행

- project/
ㄴ Makefile 
ㄴ srcs/
  ㄴ requirements/
    ㄴ front/
      ㄴ (...)
    ㄴ auth/
      ㄴ (...)
    ㄴ db/
      ㄴ (...)
    ㄴ socket/
      ㄴ (...)
  ㄴ docker-compose.yml

docker-compose에서 필요한 docker-compose.yml은 아래처럼 작성되어 있었다.

#./srcs/docker-compose.yml
version: '3'

networks:
	project:

services:
  front:
    image: alpine:latest
    ports:
      - 3002:3002
    container_name: front
    networks: project
    (...)
  auth:
    image: backend-api:latest
    ports:
      - 3000:3000
    container_name: auth
    networks: project
    (...)
  db:
  	image: alpine:latest
    ports:
      - 5432:5432
    container_name: db
    networks: project
    (...)
  socket:
  	image: alpine:latest
    ports:
      - 3001:3001
    container_name: socket
    networks: project
    (...)

- 개선할 구조 잡기

이에 아래와 같이 해결하고자 했다.

우선 서버에 접근 가능한 프록시 서버를 세팅해 둔 후에, 유저의 요청이 프록시 서버로만 가능하게 할 것이다. 프록시 서버는 페이지 조회시 내부의 프론트엔드 서버를 넘겨주며, 이에 필요한 데이터들은 localhost에 배포된 auth, db, socket 서버에 요청을 하면서 받아올 것이다. 이 과정에서 auth, db, socket은 외부 요청시에 프록시로만 접근이 가능하도록 해서, 실제로 주소나 포트가 공개되어 서버에 요청을 넣지는 못하도록 막을 것이다.


- docker-compose 수정

먼저 docker-compose.yml을 수정해서 각 서버의 포트를 expose로 명시하여 물리적으로 공개되지 않도록 하자. (docker ports vs expose 참고) expose로만 열어두면 내부의 포트로만 접근이 가능하기 때문에, 현재처럼 vm안에서 localhost로 여러 서버를 배포하는 경우 외부에서 접근이 불가능하게 할 수 있다.

실제 배포상황에서는 각 서버가 인스턴스 단위로 배포가 될 것이기에 그 주소를 공개하지 않으면 되니 불필요한 사항이긴하다. 하지만 위와 같은 방식으로 호스트 안에서 여러 서버를 배포하고 있다면, 접근이 불가능하도록 설정해주어야 한다.

설정을 하면서 프록시 서버를 위한 컨테이너를 배포하기 위해 docker-compose.yml에 proxy 컨테이너에 대한 룰도 추가했다.

#./srcs/docker-compose.yml
version: '3'

networks:
	project:

services:
  front:
    image: alpine:latest
    expose:
      - 3002
    container_name: front
    networks: project
    (...)
  auth:
    image: backend-api:latest
    expose:
      - 3000
    container_name: auth
    networks: project
    (...)
  db:
  	image: alpine:latest
    expose:
      - 5432
    container_name: db
    networks: project
    (...)
  socket:
  	image: alpine:latest
    expose:
      - 3001
    container_name: socket
    networks: project
    (...)
  proxy:
    image: nginx:alpine
    ports:
      - "80:80"
    container_name:proxy
    networks: project
    (...)

proxy 서버는 외부의 요청을 처리해야하니 ports로 포트를 공개했다. 80번 포트는 VM에서 8080:80으로 포트포워딩을 진행하였으니, 이제 proxy 컨테이너는 localhost:8080으로 들어오는 요청을 80번 포트를 리스닝하여 처리할 수 있는 상태가 되었다.

proxy 컨테이너의 dockerfile에서는 로그를 체크하기 위해 logrotate를 설치하고 설정한 뒤에, 호스트에서 저장하고 수정하고 있는 nginx를 빌드할 때에 복사하고 난 뒤 nginx를 실행시켜 주었다.

./srcs/requirements/proxy/dockerfile
# production docker
FROM nginx:alpine

# Install logrotate
RUN apk add --no-cache logrotate

COPY ./nginx.conf /etc/nginx/nginx.conf
COPY ./logrotate.conf /etc/logrotate.conf

COPY ./script.sh /script.sh
RUN chmod +x /script.sh
RUN mkdir -p /logs
RUN logrotate -d -f /etc/logrotate.d/nginx 

ENTRYPOINT ["/script.sh"]

EXPOSE 80

- 포트 설정

이제 포트 설정 차례이다. 이전에는 프론트에서 데이터가 필요하거나 서버의 작업이 필요한 경우, auth 서버 등 백엔드 서버로 직접 요청을 보내는 방식이었다. 이를 프록시 서버를 통해 전달되도록 바꾸었다. 즉 이전에는 auth 서버에 요청하기 위해선 localhost:3000/auth/login 으로 직접 요청을 보내었다면, 이제는 localhost:8080/auth/login으로 요청을 보내어 proxy 서버가 그 요청을 받도록 하는 것이다. Path는 수정하지 않고, Domain만 proxy 서버에 요청하도록 수정했다.

요청하는 Domain을 수정하는 것이기에 만약 auth 서버에 걸려있는 Oauth 설정도 수정해주어야 한다. callback으로 돌아오는redirection-url도 auth서버에 직접 넣는 게 아니라, proxy 서버를 통해 요청하도록 했다. 마찬가지로 Domain만 수정을 했다.


- Proxy Server의 Nginx 설정하기

이제 proxy 서버에서 돌아갈 nginx 의 설정 파일을 수정했다. 로그 설정과 같은 부분은 아래의 내용에서 삭제를 했다.


http {
    include         /etc/nginx/mime.types;
    default_type    application/octet-stream;
    sendfile        on;

    upstream front_server {
        server front:3002;
    }

    upstream socket_server {
        server socket:3001;
    }
    
    upstream db {
        server postgres:5432;
    }
    
    upstream auth_server {
        server auth:3000;
    }

    server {
        listen 80;
        listen [::]:80;

        sendfile            on;
        proxy_http_version  1.1;
        proxy_set_header    Host                $host;
        proxy_set_header    X-Real-IP           $remote_addr;
        proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;


        location /socket.io/ {
            proxy_pass          http://socket_server;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $host;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
        }

        location /auth {
            proxy_pass          http://auth_server;
        }

        location / {
            proxy_pass          http://front_server;
        }
    }
}

proxy 서버에서는 클라이언트의 요청을 받고, 이를 실제 애플리케이션 서버에다 중계하는 역할을 한다. 따라서 현재 컨테이너에서 열려있는 80번 포트를 통해 클라이언트의 요청이 들어오면, 이를 url에 따라 적절하게 중계해주면 된다. location 블록을 url에 따라 분기처리하였고, upstream 블록에서 각각의 서버의 주소를 넣어 둔 뒤 proxy_pass를 통해 적절한 서버로 요청이 넘어가도록 했다.

socket 서버를 제외하고는 url 상으로 단일한 prefix가 있었기 때문에 설정이 수월했다. 앞으로 분산형 배포를 설계한다면, url 정책을 결정할 때 서버단위로 prefix를 정해서 이런 작업이 더 수월하게 진행되도록 하면 좋겠다고 느낀 부분이었다.

socket 서버를 어떻게 처리해야하나 고민이 많았다. socket 서버의 api도 요청을 받을 때 사용하는 url가 저마다 달랐고, 무언가 특수한 처리가 필요하지 않나 싶었지만, 레퍼런스에서 이를 어떻게 처리할 수 있는지 잘 나와있는 부분이 있어 똑같이 처리했다. (링크 참고) socket.io를 기반으로 socket 통신을 하기에 location 에서 체크하는 url을 socket.io로 정했다. (링크 참고)


후기

  1. 시스템을 얼마나 잘 파악하는 지의 정도가 이후의 나의 생산성에 영향을 주는 경험을 했다. 지금의 팀에서 신입 포지션으로 위의 이슈를 처리했는데, 앞으로 취업 이후에 알 수 있는 것을 미리 아는 좋은 경험을 했다고 생각한다. 어떤 팀에 합류하든 이슈를 처리하기 이전에 레거시를 빠르고 정확하게 먼저 파악하는 것이 좋다고 이번 기회를 통해 배울 수 있었다.

  2. 이전에 학교 프로젝트에서 Nginx를 구현도 해보고 한 두 번 써보았지만, 스스로 사용해보고 친숙해지는 계기가 되었다.

  3. 분산 서비스로 배포하는 것의 장점과 단점을 느꼈다. 첫 빌드타임이 오래 갔지만, 오히려 일부분의 수정이 잦고 그에 맞춰 빌드가 반복해서 진행할 경우에는 docker가 이미지화 시켜놓은 걸 사용했기 때문에 전체 서비스를 리빌드하지 않아 편리했다. 앞으로 진행할 프로젝트에서 비슷한 구조를 가져가고 싶은 것은 물론, 빌드타임을 어떻게 하면 줄일 수 있을지 고민도 해보는 계기가 되었다.

포스팅 내용에 질문이 있으시거나 부정확한 내용이 있다면 댓글로 알려주시면 감사드리겠습니다 :)

profile
해본 것을 말하고 싶습니다.

2개의 댓글

comment-user-thumbnail
2022년 11월 27일

글을 잘 써주셔서 읽기가 편했습니다. 리버스 프록시 서버 구축에 대해 많이 배워갑니다!

1개의 답글