개발일지

[WebRTC] EC2, Nginx로 WebSocket 서버 배포하기 + HTTPS/WSS 본문

NestJS, Node.js/#03 WebRTC

[WebRTC] EC2, Nginx로 WebSocket 서버 배포하기 + HTTPS/WSS

lyjin 2024. 5. 9.

개요

지금까지 구현한 SFU를 실제 EC2 서버에 배포해보자. 단발성 프로젝트로 하나의 EC2 인스턴스에서 FE, BE 전부 관리하고자 한다. 참고로 SFU 구현에는 mediasoup 라이브러리를 사용했으며, BE 서버는 오로지 웹소켓 통신을 위한 역할만을 한다.

 


경로 기반 라우팅 - Nginx

현재 내부적으로 BE는 8000 포트, FE는 3000 포트를 사용하고 있다. 그러나 EC2는 하나이기 때문에 요청이 들어올 수 있는 입구도 하나일 것이다. 따라서 어떠한 기준에 따라 FE 또는 BE 포트로 적절히 연결시켜 줄 수 있어야한다.
 
기본 도메인, 즉 “http://ec2-domain.com”으로 요청이 들어왔을 땐 로컬 3000 포트(FE)로 연결해줬다. 그리고 “/socket.io” 경로로 들어온 요청은 로컬 8000 포트(BE)로 연결해주면 된다. 경로가 “/socket.io” 인 이유는 socket.io 에서 소켓 연결을 위한 경로로 “/socket.io”를 사용하기 때문이다. 만약 별도의 경로를 지정해줬을 경우에는 해당 경로를 적어주면 된다.
 
프록시 설정을 위한 방법으로 Nignx를 사용했다.

server {
    listen 80;
    server_name ec2-domain.com;

    /** FE */
    location / {
        proxy_pass <http://localhost:3000>;
    }

    /** BE */
    location ^~ /socket.io {
        proxy_pass <http://localhost:8000/socket.io>;
    }
}

 
 
 

HTTPS, WSS 설정하기 - Cerbot

HTTP, WS 프로토콜은 제약이 많아 정상적으로 동작하지 않을 수 있다. 따라서 HTTPS, WSS 프로토콜을 사용하도록 변경해야한다. 웹소켓 연결 요청할 때 HTTPS 프로토콜로 요청하면 알아서 WSS 프로토콜을 호출해준다.
 
HTTPS 프로토콜을 사용하기 위해서는 SSL 인증서가 필요하다. cerbot을 사용해서 SSL 인증서를 무료로 발급 받을 수 있다.
아래의 명령어로 cerbot을 설치하고 ssl 인증서를 발급한다.

$ sudo apt-get update
$ sudo apt-get install certbot python3-certbot-nginx
$ sudo certbot certonly --nginx -d <ec2-domain.com>

 
발급된 인증서는 “/etc/letsencrypt/live/ec2-domain.com/” 디렉토리에 저장된다.
 
파일을 다시 열어보면 다음과 같이 변경되어 있을 것이다. 이제 HTTPS 요청이 가능하고 80 포트, 즉 HTTP로 요청이 들어오면 HTTPS 프로토콜로 리다이렉트 하게 된다.

server {
    listen 80;
    server_name ec2-domain.com;

    if ($host = ec2-domain.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

   return 404;
}

server {
    server_name ec2-domain.com;

    # managed by Certbot
    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/ec2-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ec2-domain.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    ...
}

 
 


프로젝트 수정

이에 맞춰서 코드도 수정해주자.
 

클라이언트

import io from 'socket.io-client';

const SERVER_URL = 'https://ec2-domain.com';  // "wss://"가 아닌 "https://"
const socket = io(SERVER_URL, {
  // path: '/socket.io'  // 사용자 지정 경로
});

 
주의할 점은 WSS 프로토콜이 아니라 HTTPS 프로토콜로 요청해야한다는 것이다. 수많은 삽질 과정 속에서 웹소켓이니까 WSS 프로토콜을 사용해야하나..? 헷갈렸었는데 아니다. 웹소켓은 HTTPS로 초기 핸드셰이크 및 연결이 수립되면 그때부터 WSS 프로토콜을 사용하기 때문에 연결 요청은 HTTPS 프로토콜로 하는 게 맞다.
 

서버

// mediasoup

const io = new SocketIO(httpServer, {
  // path: '/socket.io' // 클라이언트와 동일한 path
});

const transport: WebRtcTransport = await router.createWebRtcTransport({
  listenIps: [
    {
      ip: <EC2_Private_IP>,
      announcedIp: <EC2_Public_IP>,
    },
  ],
  ...
});
  • announcedIp: 외부로부터 접근 가능한 ip 주소
  • ip: 실제 수신할 로컬 ip 주소

listenIps 주소를 잘못 지정 해도 비디오가 재생되지 않는 등, 제대로 동작하지 않기 때문에 주의해야한다.
 
 


WebSocket 관련 설정하기

HTTP(S)에서 WS(S) 프로토콜로 전환하기 위해 Connection, Upgrade 헤더를 사용해야한다.
 

Connection 헤더와 Upgrade 헤더

Connection 헤더는 클라이언트와 서버 간의 커넥션을 제어하는 데 사용한다. keep-alive, close, upgrade 등 원하는 옵션을 지정해줄 수 있다. 그 중 upgrade는 “프로토콜 전환을 위한 요청임”을 나타낸다. Upgrade 헤더는 Connection: upgrade 옵션과 함께 사용되며 전환할 프로토콜을 지정한다. 여기선 웹소켓을 사용할 것이기 때문에 Upgrade: websocket 으로 명시해줘야한다.
 
 
따라서 nginx에서 요청을 프록시할 때 실제 브라우저에서 들어온 헤더 값 그대도 전달할 수 있도록 설정 해줘야한다. Upgrade와 Connection을 포함하여 필요한 설정 값들을 지정해주자.

server {
    server_name ec2-domain.com;

    /** FE */
    location / {
        proxy_pass <http://localhost:3000>;
        
        // websocket 설정
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;   
    }

    /** BE */
    location ^~ /socket.io {
        proxy_pass <http://localhost:8000/socket.io>;
        
        // websocket 설정
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
    }
}

 


결과

실제 요청해보면 다음과 같이 WS 프로토콜로 전환된 것을 확인할 수 있다. 앞서 설명한 대로 Connection, Upgrade 헤더 값이 설정되어있고 101 상태코드를 갖는다.

 

 
이벤트 통신도 잘 된다.
 

Nginx 설정 파일 최종

server {
    listen 80;
    server_name ec2-domain.com;

    if ($host = ec2-domain.com) {
        return 301 https://$host$request_uri;
    }

   return 404;
}

server {
    server_name ec2-domain.com;

    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/ec2-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ec2-domain.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location / {
        proxy_pass <http://localhost:3000>;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;   
   }

    location ^~ /socket.io {
        proxy_pass <http://localhost:8000/socket.io>;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
    }
}

 


후기

어찌보면 기능 구현보다 더 힘든 과정이었다. 그냥 일반 서버처럼 EC2에 올리기만 하면 되겠지 했는데 아니었다. 처음엔 웹소켓 연결 과정을 모르니 에러가 나도 왜 나는지도 모르겠고 로컬에서는 잘 돌아가던게 왜 그러나 원망스러웠다. 에러와 삽질의 연속이었지만 WS 프로토콜로 어떻게 전환될 수 있는 건지, 왜 헤더 설정을 줘야하는건지 하나하나 알아가면서 마침내 해결할 수 있었다.
 
처음 시도해보고 어디 물어볼 데도 없어서 너무나도 힘들었지만 그만큼 뿌듯함도 크다. 비록 아쉬운 부분이 많긴 하지만 누가 처음부터 다 잘하겠어.. 그래도 웹소켓 좀 재미있는 기술인 거 같다. 다음엔 더 안정적이고 속도도 빠른 서버를 구축해내고싶다!
 
 

참고

https://nginxstore.com/blog/nginx/websocket-proxy-로-nginx-사용하기/