처음엔 프론트엔드와 백엔드 모두 각각의 EC2 인스턴스를 만들어서 서로 연결하여 배포하려고 했으나 어쩌다 보니 둘 다 한 EC2 인스턴스에서 배포하게 됐다.
리액트 배포는 정적인 파일들을 생성한 build 폴더를 만들고 이걸 웹 서버 프로그램으로 배포하는 방식으로 하기로 했다.
빌드 된 폴더(build)는 전체 프로젝트 폴더에 비해 용량이 매우 작아진다.
처음에는 프로젝트 깃헙 저장소를 EC2에서 git clone하고 .env 파일 같이 .gitignore에 포함된 것들만 따로 복사하여 npm install → npm run build 하여 배포하려고 했다.
그런데 EC2 상에서 npm run build
가 잘 되지 않는 문제가 발생했다.
2차 프로젝트와는 다르게 여러가지 라이브러리와 패키지를 활용했기 때문에 전체 프로젝트 폴더 용량이 커졌다.
EC2 프리 티어 서버의 메모리가 1GB뿐인데 프로젝트 폴더 전체 용량은 거의 2GB 정도라서 메모리 부족으로 빌드가 잘 안됐던 것이다.
따라서 로컬에서 빌드하고 결과물로 나온 build 폴더만 ec2에 올려서 배포하면 된다.
ec2 서버에 파일 업로드하는 방법
scp
명령어로 업로드단 빌드할 때 .env 같은 필요한 파일을 반드시 포함한 상태로 빌드해야 한다.
npm에 있는 serve라는 간단한 웹서버 프로그램으로 첫 배포를 성공, 반드시 -s 옵션을 붙여서 실행해야 한다.
serve 실행 명령어, 80번 포트를 사용하려면 sudo가 필요하다.
sudo serve -s build -l 80 &
serve에서 pm2로 변경, pm2는 Node.js의 프로세스를 관리해주는 프로그램이며 예기치 못한 에러로 인해 스레드가 멈춰도 자동으로 프로세스를 재실행 해주기 때문에 편리하다.
pm2 실행 명령어
sudo pm2 serve build 80 --spa
node.js와 express를 활용하여 배포할 수도 있는데 이 방법도 빌드 파일 만들어야 하는 것은 똑같고 과정만 조금 다르다.
npm start는 브라우저로 localhost에 접속 시 리액트로 만든 html을 띄워주는 명령어이다.
pm2로 웹페이지가 잘 나오는지 확인 후 밑에서 설명한 NGINX를 통해 https로 최종 배포했다.
EC2, 도메인 연결 방법
가비아에서 도메인을(withdog.me) 구입했고 가격은 8천원 정도였다.
EC2와 가비아 도메인 연결 방법은 구글링하면 많이 나오고 어렵지 않다, 구입 후 AWS Route 53로 연결했다.
처음에는 도메인 끝에 포트번호(3000)를 붙여줘야 접속이 됐으나 (serve -s build으로 실행)
웹 서버를 80번 포트(기본 http 포트)로 실행하니 포트 번호를 따로 붙이지 않아도 접속 됐다.
Nginx는 가벼우면서 고성능을 목표로 하는 웹 서버 프로그램이다.
클라이언트로부터 요청을 받았을 때 요청에 맞는 정적 파일을 응답해주는 HTTP Web Server로 활용되기도 하고, 리버스 프록시 서버로 활용하여 WAS 서버의 부하를 줄일 수 있는 로드 밸런서로 활용되기도 한다.
Nginx는 Event-Driven 구조로 동작하기 때문에 한 개 또는 고정된 프로세스만 생성하여 사용하고, 비동기 방식으로 요청들을 동시적으로 처리할 수 있다.
Nginx는 새로운 요청이 들어오더라도 새로운 프로세스와 쓰레드를 생성하지 않기 때문에 프로세스와 쓰레드 생성 비용이 존재하지 않고, 적은 자원으로도 효율적인 운용이 가능하다
이러한 Nginx의 장점 덕분에 단일 서버에서도 동시에 많은 연결을 처리할 수 있다.
Gunicorn은 WSGI(Web Server Gateway Interface)의 일종이며, Django 서버 배포를 하기 활용할 수 있다.
WSGI는 CGI(Common Gateway Interface)의 일종으로 파이썬 애플리케이션이(파이썬 스크립트) 웹 서버와 통신하기 위한 인터페이스이다. 웹 서버에서의 요청을 해석하여 파이썬 애플리케이션 쪽으로 전달하는 역할을 수행한다.
정리하자면 server/gateway side(Nginx 쪽)와 application/framework side(Django 쪽)를 둘 다 구현하고 있는 하나의 프로그램이다. 서버에 대해선 어플리케이션 역할을 수행하고, 어플리케이션에 대해선 서버의 역할을 수행한다.
Django에선 개발 목적으로 python manage.py runserver
명령어를 통해 웹 사이트를 띄울 수 있다. 하지만 보안이나 성능적으로 적절하지 않기 때문에 배포 환경에선 Gunicorn을 사용하는 것이다. 즉, production 환경에 적합하다.
참고로 Django에는 이미 WSGI 파일이 포함되어 있다. wsgi.py 는 프로젝트 디렉토리에 위치해 있고, django.core.wsgi.py, django.core.handlers.wsgi.py
역시 내부적으로 구현이 되어 있는 상태이다.
Nginx와 Gunicorn 둘 중 하나만 써도 될까? 라는 질문에 대한 답은 모두 'Yes' 이다.
Gunicorn이 WSGI middleware로서 웹서버 역할을 수행하므로 Gunicorn만 써도 되지만, Nginx가 제공하는 추가적 혜택을 받지 못한다.
Django는 WSGI interface를 이미 어느 정도 구현했기 때문에 Nginx만 써도 되지만, 그럴 경우 seesion / cookie / routing / authentication 등의 기능을 수행하는 middleware가 없어서 이 기능들을 구현하려면 따로 개발 해야 한다. (Do not reinvent the wheel.)
프록시/리버스프록시 기본 개념:
https://losskatsu.github.io/it-infra/reverse-proxy/
기본 nginx 설정 파일을 보면 URL 경로가 /로 시작하여 들어오는 경우, root에 지정된 경로에 따라 일치하는 파일을 웹서버로 뿌려주는 식으로 작동한다는 것을 알 수 있다.
하지만 리버스 프록시를 서버 블록에 적용하면 URL 경로와 적합한 서버 블록을 찾은 후 해당 서버 블록의 정보의 proxy_pass에 따르는 내용을 보여준다.
리버스 프록시를 통해 로컬호스트의 8000번 포트에 접속해야 할 수 있는 것을 퍼블릭IP의 443번 포트(https)에 접속했을 때도 가능하게 만들 수 있다.
리버스 프록시의 장점: 1. 보안 2. 성능 3. 트래픽 분산, 로드 밸런싱
처음에는 AWS에서 제공하는 인증서와 Application Load Balancer(ALB)를 이용하여 적용하려 했으나 이 방법으로는 루트 도메인(https://withdog.me)을 사용할 수 없는 것 같아 Nginx를 통해 https를 적용하는 방법으로 바꿨다.
무료 SSL 인증서(Let's Encrypt)라고 구글링하면 Nginx에 적용하는 방법이 많이 나온다.
인증서 발급은 ec2 인스턴스에 접속한 상태로 진행하였다.
https 적용이 정상적으로 된 것을 확인하면 리액트의 config과 .env 환경변수 파일에 있는 경로들을 모두 https가 들어간 경로로 변경하고 다시 빌드한다.
location 경로 설정을 잘못하면 리버스 프록시 기능이 제대로 작동하지 않으므로 적절한 설정 방법을 찾아야 한다.
밑에 설정 파일을 적용하기 전에 Nginx 기본 설정 파일을 초기화 하는 작업이 필요하다.
options-ssl-nginx.conf 파일과 ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem을 설정에 추가해주면 보안을 강화할 수 있다.
root 권한으로 밑의 curl 명령어 실행해서 파일이 정상적으로 생성된 것을 확인하고 nginx 설정에 추가해주면 된다.
curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > /etc/letsencrypt/options-ssl-nginx.conf
curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > /etc/letsencrypt/ssl-dhparams.pem
Nginx 설정 코드의 앞에 #
를 붙이면 주석이 된다.
# 설정파일 경로 /etc/nginx/sites-available/config.conf
server {
# 기본 http 포트(80)으로 접속하면 https로 리다이렉트 시킴
# 도메인 끝에 추가로 입력한 부분까지 포함하여 리다이렉트 됨
listen 80;
server_name withdog.me;
root html;
location / {
return 301 https://withdog.me$request_uri;
}
}
server {
# SSL/TLS 설정
# https 기본포트(443)로 접속하면 빌드 된 index.html을 제공함
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
ssl on;
# SSL/TLS 인증서 파일 경로
ssl_certificate /etc/letsencrypt/live/withdog.me/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/withdog.me/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf; # 보안 강화 옵션
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # 보안 강화 옵션
server_name withdog.me;
location / {
root /home/ubuntu/build7; # 빌드파일 경로
index index.html index.htm; # index.html->index.htm 순서대로 파일 탐색
try_files $uri /index.html;
}
# 백엔드 서버(Django)를 위한 리버스 프록시 설정
# 클라이언트의 모든 백엔드 요청을 /api로 시작하는 경로로 지정했다
# Django의 엔드포인트 경로에도 모두 앞에 /api를 붙여야 한다
# gunicorn은 localhost 8000번 포트로 실행한다
# 이렇게 할 경우 gunicorn을 -b 0:8000(퍼블릭IP, 외부 접속 가능)으로 실행하면 안되고
# localhost로 실행되게 해야하므로 -b 0:8000 옵션은 쓰지 않는다
location ^~ /api {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
}
# socket.io를 위한 리버스 프록시 설정
# 출처: flask-socketio 공식문서, MDN Protocol upgrade mechanism
location ^~ /socket.io {
include proxy_params;
proxy_http_version 1.1;
proxy_buffering off;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_pass http://127.0.0.1:8000/socket.io;
}
}
Nginx 서비스 실행 명령어
sudo service nginx start
Nginx 서비스 실행 상태 확인
sudo service nginx status
입력 후 터미널에 Active: active (running)라고 뜨면 정상
배포가 정상적으로 됐다면, Django 설정 파일에서 개발 편의를 위해 사용했던 모든 host와 origin을 허용하는 환경변수를 수정해야한다.
settings.py에서 SECURE / CORS / ALLOWED HOSTS에 관련된 환경변수들을 적절하게 수정해주고, socketio 서버에도 적용한다.
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
ALLOWED_HOSTS = ["localhost", "127.0.0.1", "withdog.me"]
CORS_ALLOWED_ORIGINS = ['https://withdog.me']