겸손하기 꾸준하기 건강하기

[맛,잇다] 배포/트러블슈팅 EP3 (Nginx와 리버스 프록시 설정) 본문

project

[맛,잇다] 배포/트러블슈팅 EP3 (Nginx와 리버스 프록시 설정)

seminss 2024. 9. 2. 02:38

맛잇다에서 ①팀장, ②인프라, ③ 화상채팅(오픈비두+제스처 감지+ STT)을 맡아 개발했다.

 

배포 과정에서 겪은 문제들, 배운 점들을 중점으로 간단하게 회고하고자 한다.

1. SSL 인증서 발급 및 OpenVidu 배포
2. 어플리케이션 및 인프라 구성
3. Nginx와 리버스 프록시 설정

 

🙇‍♀️ 공부하면서 작성한 내용이기 때문에, 틀린 부분이 있다면 여과 없이 지적해 주시면 감사하겠습니다. 🙇‍♀️


🎯 Nginx와 리버스 프록시 설정

웹캠, 마이크 등 민감한 장치에 접근하기 위해서는 HTTPS 프로토콜 사용이 필수적이다. 오픈비두도 HTTPS를 강제하며, 따라서 모든 HTTP 요청을 HTTPS로 리다이렉션 하는 설정이 필요했다. 이에 Nginx를 리버스 프록시로 도입하여 클라이언트의 모든 요청을 가장 앞단에서 받고, HTTP 요청을 HTTPS로 자동 리다이렉션하도록 했다. Nginx는 리버스 프록시로서 클라이언트의 요청을 받아 내부 서버(프론트엔드, 백엔드, 채팅 서버)로 전달한다. 이 과정에서 SSL 인증서를 Nginx에서만 발급받아 관리함으로써, 각 서버에 별도의 인증서를 발급할 필요가 없게 했다.

 

(https://seminss.tistory.com/13에서 공부한 내용.)

 


📌 Nginx 설정

본 프로젝트에서 Nginx는 우분투 시스템에 설치하여 사용했다. 오픈비두의 기본 Nginx 컨테이너가 존재해서.. 헷갈리므로 그냥 팀에서 실제로 사용할 Nginx는 컨테이너로 띄우지 않은 것이다.

docker ps 명령어 실행 결과로, openvidu가 붙은 건 오픈비두를 실행했을 때 기본적으로 올라오는 컨테이너들이다.

 

Nginx를 우분투 시스템에서 올리게 되면, /etc/nginx 경로에 있는 Nginx.conf 파일을 기반으로 Nginx가 실행된다.

파일에 다음과 같은 설정을 추가했다.

  • user는 root로 변경 (권장 x)
  • http에 include /etc/nginx/conf.d/*.conf , include /etc/nginx/sites-enabled/* 와 같은 두 include를 진행

nginx.conf

user root;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
        worker_connections 768;
}

http {

        ...

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;

        ...

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;
}

/ect/nginx/sites-available에 deploy-test.conf 라는 config 파일을 따로 두었었고, 여기서 nginx 설정을 관리했었다. 이 파일을 sites-enabled에 심볼릭 링크를 걸어 nginx.conf에서 import 할 수 있게 하였다. 관리를 편하게 하고, 디버깅을 편하게 하기 위함이었다.

 

📌 리버스 프록시 설정

deploy-test.conf

/etc/nginx/sites-available/deploy-test.conf 파일에 리버스 프록시 설정을 추가하여 관리했다.

upstream frontend {                                                                
    server localhost:3000;                                   
}                                                                  
                                                     
upstream backend {                                                                 
    server localhost:8080;                                                       
}                                                                                  
                                                                   
upstream chatend {
    server localhost:8081;                                                         
}

upstream 지시어를 사용해 프론트엔드, 백엔드, 채팅 서버의 각각의 서비스를 지정했다. Nginx는 클라이언트 요청을 처리할 때, 이 upstream 블록에 정의된 서버 중 하나로 요청을 전달한다.

 

server {
    listen 80;
    server_name i11b206.p.ssafy.io;

    # HTTP 요청은 HTTPS로 리다이렉션 할게요
    if ($host = i11b206.p.ssafy.io) {
        return 301 https://$host$request_uri;
    }

    return 404;
}

80번 포트로 들어오는 모든 요청(HTTP)은 301 상태 코드로 HTTPS로 리다이렉션 했다.

301은 페이지가 이동했음을 알리는 상태 코드다.

 

server {
    listen 443 ssl;
    server_name i11b206.p.ssafy.io;

    ssl_certificate /etc/letsencrypt/live/i11b206.p.ssafy.io/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/i11b206.p.ssafy.io/privkey.pem;

    root /home/ubuntu/b206/S11P12B206/frontend;

	...
}

443으로 들어오는 요청, 즉 HTTPS 요청을 어디로 리다이렉션 할 지에 대한 설정을 했다.

ssl_certificate와 ssl_certificate_key 지시어로 SSL 인증서( fullchain.pem)와 개인 키( private.pem)의 위치를 지정했다. 이를 통해 모든 HTTPS 요청은 암호화되며, 클라이언트와 서버 간의 통신이 보호되었다.

 

root 지시어로 정적 파일을 제공할 때 사용할 서버의 루트 디렉터리를 지정했다. https://i11b206.p.ssafy.io/ 로 요청이 들어오면, 이 루트 경로에 있는 index.html이 기본적으로 사용자에게 보이는 것이다. 

 

server {

	...

    location / { #/로 들어오면 frontend 서버로 연결할게요
        proxy_pass http://frontend;
        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;

        # CORS settings
        ...
    }

    location /api/v1 { #/api/v1로 들어오면 backend 서버로 연결할게요
        proxy_pass http://backend;
        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;

        # CORS settings
        ...
   }

   location /chat { #/chat으로 들어오면 채팅 서버 중, 웹소켓 연결을 할게요
        proxy_pass http://chatend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_buffering off;
    }

    location /chatapi {  #/chatapi로 들어오면 채팅 서버 중, REST API 연결을 할게요
        proxy_pass http://chatend;
        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;
    }

}

location 지시어를 통해 요청이 들어오는 각 path를 어느 주소로 프록시 시킬지 설정하였다.

이 과정에서 정말 상상도 못 한 문제가 발생했다.🙀 하루를 통으로 날려버린... ⊙.☉

 

💫 채팅 서버의 API Path를 찾지 못하는 문제 (aka 404..)

결론적으로는  채팅 서버의 API 경로 문제로 인해 Nginx가 잘못된 서버로 요청을 라우팅 하면서 404 오류가 발생한 것이다. 그러나 이 문제가 발생하는 데는 참 많은 원인들이 있었다. 오타 같은 사소한 부분도 있었다. 

원인 1 : 서버의 Context Path

초기 설계를 할 당시에는 채팅 서버에서 REST API를 구현할 계획이 없었고, 메인 서버의 모든 path에 /api/v1이 붙기 때문에 메인 서버의 application.properties에 context-path를 /api/v1으로 잡아두었다.

server.servlet.context-path=/api/v1

 

그러나 개발을 하면서 채팅방에 참여 중인 참여자 목록을 불러오거나 채팅방 목록을 불러올 때 등, REST API를 구현해야 할 필요성이 생겼다. 따라서, 채팅 서버에서도 /chat/api/v1 ... 와 같은  방식으로 엔드 포인트를 잡게 되었다. 

@RestController
@RequestMapping("/chat/api/v1/chats")
public class ChatController {
...

 

여기서 문제가 발생했다. Nginx는 URL 패스를 기반으로 요청을 특정 서버로 라우팅 한다. /api/v1 경로를 메인 서버와 채팅 서버에서 공통으로 사용하게 되었기 때문에, Nginx로 들어가는 요청에 혼선이 생겼다.

 

채팅 서버로 들어가는 모든 REST API 요청에 404가 떴다. /chat/api/v1을 시작으로 들어간 요청이 /chat에 걸려 채팅 서버에 라우팅 되는 것이 아니라, /api/v1에 걸려 메인 서버로 라우팅 되어버렸다. 메인 서버에는 당연히 해당 경로를 받는 컨트롤러가 없으니 404가 뜰 수밖에 없던 것이었다.

@RestController
@RequestMapping("/chatapi/chats")
public class ChatController {
...

채팅 서버의 앤드 포인트를 /chatapi로 변경하여, 메인 서버의 context path와 겹치는 부분이 없도록 하였다.

향후 프로젝트에서는 /api/v1과 같이 여러 서버에서 범용적으로 사용되는 경로는 절대 context-path로 잡지 않을 것이다.😑😑😑 메인서버의 context-path는 /tastyties, 채팅 서버의 context-path는 /tastytieschat로 했다면 딱 적절했을 것 같다.

 

원인 2 : 도커 이미지 오타

분명 수정을 했는데.. 아무리 봐도 문제가 없는데!!! 여전히 404가 떴다 ㅠㅠ 

허무하게도 오타 문제였다.

나의 경우에는 빌드 파일을 docker hub에 올리고, docker compose.yml에서 docker hub에 올라간 이미지를 받아와 배포를 했었다. 그런데 초반에 채팅 서버 이미지를 tastytiescaht이라고 오타 낸 전적이 있었다. 그리고 귀찮아서 docker-compose에 그냥 오타난 이미지를 사용했었다. 아래와 같이 말이다....

  tastytieschat:
    image: seminss/tastytiescaht:latest

 

그런데 시간이 지나... 내가 오타를 낸 이미지를 사용하고 있다는 사실을 깜빡했다! 며칠 뒤에는 계속해서 tastytieschat이라는 정상적인 이름으로 계속 이미지를 빌드하고 올리게 되었다. 그러나 docker-compose는 tashtytiescaht을 받고 있지 않은가? 계속 잘못된 이미지인 (2일 전 버전)을 컨테이너로 올리고 있었던 것이다. 당연히 수정 사항이 반영이 안 되지...

뭔가 쎄하더라고요

원인 3 : 도커 이미지 찌꺼기

# 모든 컨테이너 정지
docker stop $(docker ps -aq)

# 모든 컨테이너 삭제
docker rm $(docker ps -aq)

# 모든 이미지 삭제
docker rmi $(docker images -q)

# 사용하지 않는 네트워크 삭제
docker network prune -f

# 사용하지 않는 볼륨 삭제
docker volume prune -f

오타만 수정한다고 해결되는 문제는 아니었다. docker rmi seminss/tastytieschat:latest로 기존 이미지를 삭제해줘야 새로운 이미지가 받아진다. 계속 배포를 반복하다 보니 태그를 계속 latest로 달았기 때문에, 우분투에 동일한 이름, 동일한 태그의 도커 이미지가 존재해 허브에서 새 이미지를 받아오지 않고 있었다.

이 시점을 계기로, 빌드 후 새로 배포 전, 기존 이미지를 깔끔하게 지우는 습관을 가지게 되었다. 추후에는 태그를 좀 더 명확히 해서 버전별로 관리하는 것도 괜찮을 것 같다.

 

📌 nginx 동작 확인

systemctl status nginx

 


https://github.com/Tasty-Ties

 

Tasty-Ties

맛잇다: 온라인 쿠킹 클래스 서비스. Tasty-Ties has one repository available. Follow their code on GitHub.

github.com

 

 

채팅 서버까지 배포를 완료하면서, 다사다난했던 수동 배포가 마무리되었다.

개인적으로는 보안을 조금 더 강화하고, 자동 배포도 해보고.. 해보고 싶은 건 많았는데, 시간이 부족해 못해서 아쉽다.

음성 채팅 개발하고, 팀원들 디버깅 같이하고.. 하는데 시간을 많이 썼다. 팀장이었기 때문에 그것 또한 내가 해야 하는 역할이었다고 생각한다. 프로젝트 전체의 완성도를 높이기 위한 과정이었기 때문에, 다시 돌아가도 같은 선택을 했을 것이다. 다음에 기회가 돼서 다시 인프라를 담당하게 된다면 진짜 잘할 수 있을 것 같다 ㅎ.ㅎ

 

다음 게시글은 시간이 허락한다면... STT와 제스처 인식 로직 정리해서 올게요😊